This project is an example of architecture using new technologies and best practices.
The goal is to share knowledge and use it as reference for new projects.
Thanks for enjoying!
- .NET Core 3.1
- ASP.NET Core 3.1
- Entity Framework Core 3.1
- C# 8.0
- Angular 10
- UIkit
- Docker
- Azure DevOps
- Clean Code
- SOLID Principles
- DDD (Domain-Driven Design)
- Separation of Concerns
- DevOps
- Code Analysis
Command Line
- Open directory source\Web\Frontend in command line and execute npm run restore.
- Open directory source\Web in command line and execute dotnet run.
- Open https://localhost:8090.
Visual Studio Code
- Open directory source\Web\Frontend in command line and execute npm run restore.
- Open source directory in Visual Studio Code.
- Press F5.
Visual Studio
- Open directory source\Web\Frontend in command line and execute npm run restore.
- Open source\Architecture.sln in Visual Studio.
- Set Architecture.Web as startup project.
- Press F5.
Docker
- Execute docker-compose up --build -d --force-recreate in root directory.
- Open http://localhost:8095.
Books
- Clean Code: A Handbook of Agile Software Craftsmanship - Robert C. Martin (Uncle Bob)
- Clean Architecture: A Craftsman's Guide to Software Structure and Design - Robert C. Martin (Uncle Bob)
- Implementing Domain-Driven Design - Vaughn Vernon
- Domain-Driven Design Distilled - Vaughn Vernon
- Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans
- Domain-Driven Design Reference: Definitions and Pattern Summaries - Eric Evans
Visual Studio Code Extensions
Source: https://github.com/rafaelfgx/DotNetCore
Published: https://www.nuget.org/profiles/rafaelfgx
Web: API and Frontend (Angular).
Application: Flow control.
Domain: Business rules and domain logic.
Model: Data transfer objects.
Database: Database persistence.
<form [formGroup]="form">
<fieldset class="uk-fieldset">
<div class="uk-margin">
<app-label for="login" text="Login"></app-label>
<app-input-text formControlName="login" text="Login" [autofocus]="true"></app-input-text>
</div>
<div class="uk-margin">
<app-label for="password" text="Password"></app-label>
<app-input-password formControlName="password" text="Password"></app-input-password>
</div>
<div class="uk-margin uk-text-center">
<app-button text="Sign in" [disabled]="form.invalid" (click)="signin()"></app-button>
</div>
</fieldset>
</form>
@Component({ selector: "app-signin", templateUrl: "./signin.component.html" })
export class AppSigninComponent {
form = this.formBuilder.group({
login: ["", Validators.required],
password: ["", Validators.required]
});
constructor(
private readonly formBuilder: FormBuilder,
private readonly appAuthService: AppAuthService) {
}
signin() {
this.appAuthService.signin(this.form.value);
}
}
export class SignInModel {
login!: string;
password!: string;
}
@Injectable({ providedIn: "root" })
export class AppUserService {
constructor(
private readonly http: HttpClient,
private readonly gridService: GridService) { }
add(model: UserModel) {
return this.http.post<number>("users", model);
}
delete(id: number) {
return this.http.delete(`users/${id}`);
}
get(id: number) {
return this.http.get<UserModel>(`users/${id}`);
}
grid(parameters: GridParametersModel) {
return this.gridService.get<UserModel>("users/grid", parameters);
}
list() {
return this.http.get<UserModel[]>("users");
}
update(model: UserModel) {
return this.http.put(`users/${model.id}`, model);
}
}
@Injectable({ providedIn: "root" })
export class AppRouteGuard implements CanActivate {
constructor(
private readonly router: Router,
private readonly appStorageService: AppStorageService) { }
canActivate() {
if (this.appStorageService.any("token")) { return true; }
this.router.navigate(["/login"]);
return false;
}
}
@Injectable({ providedIn: "root" })
export class AppErrorHandler implements ErrorHandler {
constructor(private readonly injector: Injector) { }
handleError(error: any) {
if (error instanceof HttpErrorResponse) {
switch (error.status) {
case 401: {
const router = this.injector.get<Router>(Router);
router.navigate(["/login"]);
return;
}
case 422: {
const appModalService = this.injector.get<AppModalService>(AppModalService);
appModalService.alert(error.error);
return;
}
}
}
console.error(error);
}
}
@Injectable({ providedIn: "root" })
export class AppHttpInterceptor implements HttpInterceptor {
constructor(private readonly appStorageService: AppStorageService) { }
intercept(request: HttpRequest<any>, next: HttpHandler) {
const token = this.appStorageService.get("token");
request = request.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next.handle(request);
}
}
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseException();
application.UseHttps();
application.UseRouting();
application.UseStaticFiles();
application.UseResponseCompression();
application.UseAuthentication();
application.UseAuthorization();
application.UseEndpoints();
application.UseSpa();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSecurity();
services.AddResponseCompression();
services.AddControllersDefault();
services.AddSpa();
services.AddContext();
services.AddServices();
}
}
[ApiController]
[Route("Users")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public Task<IActionResult> AddAsync(UserModel model)
{
return _userService.AddAsync(model).ResultAsync();
}
[HttpDelete("{id}")]
public Task<IActionResult> DeleteAsync(long id)
{
return _userService.DeleteAsync(id).ResultAsync();
}
[HttpGet("{id}")]
public Task<IActionResult> GetAsync(long id)
{
return _userService.GetAsync(id).ResultAsync();
}
[HttpPatch("{id}/inactivate")]
public Task InactivateAsync(long id)
{
return _userService.InactivateAsync(id);
}
[HttpGet("grid")]
public Task<IActionResult> ListAsync([FromQuery]GridParameters parameters)
{
return _userService.ListAsync(parameters).ResultAsync();
}
[HttpGet]
public Task<IActionResult> ListAsync()
{
return _userService.ListAsync().ResultAsync();
}
[HttpPut("{id}")]
public Task<IActionResult> UpdateAsync(UserModel model)
{
return _userService.UpdateAsync(model).ResultAsync();
}
}
public sealed class UserService : IUserService
{
private readonly IAuthService _authService;
private readonly IUnitOfWork _unitOfWork;
private readonly IUserRepository _userRepository;
public UserService
(
IAuthService authService,
IUnitOfWork unitOfWork,
IUserRepository userRepository
)
{
_authService = authService;
_unitOfWork = unitOfWork;
_userRepository = userRepository;
}
public async Task<IResult<long>> AddAsync(UserModel model)
{
var validation = await new AddUserModelValidator().ValidateAsync(model);
if (validation.Failed)
{
return Result<long>.Fail(validation.Message);
}
var authResult = await _authService.AddAsync(model.Auth);
if (authResult.Failed)
{
return Result<long>.Fail(authResult.Message);
}
var user = UserFactory.Create(model, authResult.Data);
await _userRepository.AddAsync(user);
await _unitOfWork.SaveChangesAsync();
return Result<long>.Success(user.Id);
}
public async Task<IResult> DeleteAsync(long id)
{
var authId = await _userRepository.GetAuthIdByUserIdAsync(id);
await _userRepository.DeleteAsync(id);
await _authService.DeleteAsync(authId);
await _unitOfWork.SaveChangesAsync();
return Result.Success();
}
public Task<UserModel> GetAsync(long id)
{
return _userRepository.GetByIdAsync(id);
}
public async Task InactivateAsync(long id)
{
var user = new User(id);
user.Inactivate();
await _userRepository.UpdateStatusAsync(user);
await _unitOfWork.SaveChangesAsync();
}
public Task<Grid<UserModel>> ListAsync(GridParameters parameters)
{
return _userRepository.Queryable.Select(UserExpression.Model).ListAsync(parameters);
}
public async Task<IEnumerable<UserModel>> ListAsync()
{
return await _userRepository.Queryable.Select(UserExpression.Model).ToListAsync();
}
public async Task<IResult> UpdateAsync(UserModel model)
{
var validation = await new UpdateUserModelValidator().ValidateAsync(model);
if (validation.Failed)
{
return Result.Fail(validation.Message);
}
var user = await _userRepository.GetAsync(model.Id);
if (user == default)
{
return Result.Success();
}
user.UpdateFullName(model.Name, model.Surname);
user.UpdateEmail(model.Email);
await _userRepository.UpdateAsync(user.Id, user);
await _unitOfWork.SaveChangesAsync();
return Result.Success();
}
}
public static class UserFactory
{
public static User Create(UserModel model, Auth auth)
{
return new User
(
new FullName(model.Name, model.Surname),
new Email(model.Email),
auth
);
}
}
public class User : Entity<long>
{
public User
(
FullName fullName,
Email email,
Auth auth
)
{
FullName = fullName;
Email = email;
Auth = auth;
Activate();
}
public User(long id) : base(id) { }
public FullName FullName { get; private set; }
public Email Email { get; private set; }
public Status Status { get; private set; }
public Auth Auth { get; private set; }
public void Activate()
{
Status = Status.Active;
}
public void Inactivate()
{
Status = Status.Inactive;
}
public void UpdateFullName(string name, string surname)
{
FullName = new FullName(name, surname);
}
public void UpdateEmail(string email)
{
Email = new Email(email);
}
}
public sealed class FullName : ValueObject
{
public FullName(string name, string surname)
{
Name = name;
Surname = surname;
}
public string Name { get; }
public string Surname { get; }
protected override IEnumerable<object> Equals()
{
yield return Name;
yield return Surname;
}
}
public class SignInModel
{
public string Login { get; set; }
public string Password { get; set; }
}
public sealed class SignInModelValidator : Validator<SignInModel>
{
public SignInModelValidator()
{
RuleFor(x => x.Login).NotEmpty();
RuleFor(x => x.Password).NotEmpty();
}
}
public sealed class Context : DbContext
{
public Context(DbContextOptions options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfigurationsFromAssembly(typeof(Context).Assembly);
builder.Seed();
}
}
public sealed class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users", "User");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedOnAdd().IsRequired();
builder.Property(x => x.Status).IsRequired();
builder.OwnsOne(x => x.FullName, y =>
{
y.Property(x => x.Name).HasColumnName(nameof(FullName.Name)).HasMaxLength(100).IsRequired();
y.Property(x => x.Surname).HasColumnName(nameof(FullName.Surname)).HasMaxLength(200).IsRequired();
});
builder.OwnsOne(x => x.Email, y =>
{
y.Property(x => x.Value).HasColumnName(nameof(User.Email)).HasMaxLength(300).IsRequired();
y.HasIndex(x => x.Value).IsUnique();
});
builder.HasOne(x => x.Auth);
}
}
public sealed class UserRepository : EFRepository<User>, IUserRepository
{
public UserRepository(Context context) : base(context) { }
public Task<long> GetAuthIdByUserIdAsync(long id)
{
return Queryable.Where(UserExpression.Id(id)).Select(UserExpression.AuthId).SingleOrDefaultAsync();
}
public Task<UserModel> GetByIdAsync(long id)
{
return Queryable.Where(UserExpression.Id(id)).Select(UserExpression.Model).SingleOrDefaultAsync();
}
public Task UpdateStatusAsync(User user)
{
return UpdatePartialAsync(user.Id, new { user.Status });
}
}