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!
- Clean Architecture
- Clean Code
- SOLID Principles
- Separation of Concerns
- DDD (Domain-Driven Design)
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 in root directory.
- Open http://localhost:8090.
Source: https://github.com/rafaelfgx/DotNetCore
Published: https://www.nuget.org/profiles/rafaelfgx
Web: Frontend and API.
Application: Flow control.
Domain: Business rules and domain logic.
Model: Data transfer objects.
Database: Data persistence.
export class AppCustomerService {
constructor(private readonly http: HttpClient, private readonly gridService: GridService) { }
add = (customer: Customer) => this.http.post<number>("customers", customer);
delete = (id: number) => this.http.delete(`customers/${id}`);
get = (id: number) => this.http.get<Customer>(`customers/${id}`);
grid = (parameters: GridParameters) => this.gridService.get<Customer>("customers/grid", parameters);
inactivate = (id: number) => this.http.patch(`customers/${id}/inactivate`, {});
list = () => this.http.get<Customer[]>("customers");
update = (customer: Customer) => this.http.put(`customers/${customer.id}`, customer);
}
export class AppGuard implements CanActivate {
constructor(private readonly appAuthService: AppAuthService) { }
canActivate() {
if (this.appAuthService.authenticated()) { return true; }
this.appAuthService.signin();
return false;
}
}
export class AppErrorHandler implements ErrorHandler {
constructor(private readonly appModalService: AppModalService) { }
handleError(error: any) {
if (error instanceof HttpErrorResponse) {
switch (error.status) {
case 422: { this.appModalService.alert(error.error); return; }
}
}
console.error(error);
}
}
export class AppHttpInterceptor implements HttpInterceptor {
constructor(private readonly appAuthService: AppAuthService) { }
intercept(request: HttpRequest<any>, next: HttpHandler) {
request = request.clone({
setHeaders: { Authorization: `Bearer ${this.appAuthService.token()}` }
});
return next.handle(request);
}
}
public sealed class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseException();
application.UseHttps();
application.UseRouting();
application.UseResponseCompression();
application.UseAuthentication();
application.UseAuthorization();
application.UseEndpoints();
application.UseSpa();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSecurity();
services.AddResponseCompression();
services.AddControllersMvcJsonOptions();
services.AddSpa();
services.AddContext();
services.AddServices();
}
}
[ApiController]
[Route("customers")]
public sealed class CustomerController : ControllerBase
{
private readonly ICustomerService _customerService;
public CustomerController(ICustomerService customerService) => _customerService = customerService;
[HttpPost]
public IActionResult Add(CustomerModel model) => _customerService.AddAsync(model).ApiResult();
[HttpDelete("{id}")]
public IActionResult Delete(long id) => _customerService.DeleteAsync(id).ApiResult();
[HttpGet("{id}")]
public IActionResult Get(long id) => _customerService.GetAsync(id).ApiResult();
[HttpGet("grid")]
public IActionResult Grid([FromQuery] GridParameters parameters) => _customerService.GridAsync(parameters).ApiResult();
[HttpPatch("{id}/inactivate")]
public IActionResult Inactivate(long id) => _customerService.InactivateAsync(id).ApiResult();
[HttpGet]
public IActionResult List() => _customerService.ListAsync().ApiResult();
[HttpPut("{id}")]
public IActionResult Update(CustomerModel model) => _customerService.UpdateAsync(model).ApiResult();
}
public sealed class CustomerService : ICustomerService
{
private readonly ICustomerFactory _customerFactory;
private readonly ICustomerRepository _customerRepository;
private readonly IUnitOfWork _unitOfWork;
public CustomerService
(
ICustomerFactory customerFactory,
ICustomerRepository customerRepository,
IUnitOfWork unitOfWork
)
{
_customerFactory = customerFactory;
_customerRepository = customerRepository;
_unitOfWork = unitOfWork;
}
public async Task<IResult<long>> AddAsync(CustomerModel model)
{
var validation = new AddCustomerModelValidator().Validation(model);
if (validation.Failed) return validation.Fail<long>();
var customer = _customerFactory.Create(model);
await _customerRepository.AddAsync(customer);
await _unitOfWork.SaveChangesAsync();
return customer.Id.Success();
}
public async Task<IResult> DeleteAsync(long id)
{
await _customerRepository.DeleteAsync(id);
await _unitOfWork.SaveChangesAsync();
return Result.Success();
}
public Task<CustomerModel> GetAsync(long id)
{
return _customerRepository.GetModelAsync(id);
}
public Task<Grid<CustomerModel>> GridAsync(GridParameters parameters)
{
return _customerRepository.GridAsync(parameters);
}
public async Task<IResult> InactivateAsync(long id)
{
var customer = new Customer(id);
customer.Inactivate();
await _customerRepository.InactivateAsync(customer);
await _unitOfWork.SaveChangesAsync();
return Result.Success();
}
public Task<IEnumerable<CustomerModel>> ListAsync()
{
return _customerRepository.ListModelAsync();
}
public async Task<IResult> UpdateAsync(CustomerModel model)
{
var validation = new UpdateCustomerModelValidator().Validation(model);
if (validation.Failed) return validation;
var customer = _customerFactory.Create(model);
await _customerRepository.UpdateAsync(customer.Id, customer);
await _unitOfWork.SaveChangesAsync();
return Result.Success();
}
}
public sealed class CustomerFactory : ICustomerFactory
{
public Customer Create(CustomerModel model)
{
return new Customer
(
model.Id,
new Name(model.FirstName, model.LastName),
new Email(model.Email)
);
}
}
public sealed class Customer : Entity<long>
{
public Customer(long id) : base(id) { }
public Customer
(
long id,
Name name,
Email email
)
: base(id)
{
Name = name;
Email = email;
Activate();
}
public Name Name { get; private set; }
public Email Email { get; private set; }
public Status Status { get; private set; }
public void Activate()
{
Status = Status.Active;
}
public void Inactivate()
{
Status = Status.Inactive;
}
}
public sealed record Name(string FirstName, string LastName);
public sealed record CustomerModel
{
public long Id { get; init; }
public string FirstName { get; init; }
public string LastName { get; init; }
public string Email { get; init; }
}
public abstract class CustomerModelValidator : AbstractValidator<CustomerModel>
{
public void Id() => RuleFor(customer => customer.Id).NotEmpty();
public void FirstName() => RuleFor(customer => customer.FirstName).NotEmpty();
public void LastName() => RuleFor(customer => customer.LastName).NotEmpty();
public void Email() => RuleFor(customer => customer.Email).EmailAddress();
}
public sealed class AddCustomerModelValidator : CustomerModelValidator
{
public AddCustomerModelValidator() => FirstName(); LastName(); Email();
}
public sealed class UpdateCustomerModelValidator : CustomerModelValidator
{
public UpdateCustomerModelValidator() => Id(); FirstName(); LastName(); Email();
}
public sealed class Context : DbContext
{
public Context(DbContextOptions options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfigurationsFromAssembly(typeof(Context).Assembly).Seed();
}
}
public sealed class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.ToTable(nameof(Customer), nameof(Customer));
builder.HasKey(customer => customer.Id);
builder.Property(customer => customer.Id).ValueGeneratedOnAdd().IsRequired();
builder.Property(customer => customer.Status).IsRequired();
builder.OwnsOne(customer => customer.Name, customerName =>
{
customerName.Property(name => name.FirstName).HasColumnName(nameof(Name.FirstName)).HasMaxLength(100).IsRequired();
customerName.Property(name => name.LastName).HasColumnName(nameof(Name.LastName)).HasMaxLength(200).IsRequired();
});
builder.OwnsOne(customer => customer.Email, customerEmail =>
{
customerEmail.Property(email => email.Value).HasColumnName(nameof(User.Email)).HasMaxLength(300).IsRequired();
customerEmail.HasIndex(email => email.Value).IsUnique();
});
}
}
public sealed class CustomerRepository : EFRepository<Customer>, ICustomerRepository
{
public CustomerRepository(Context context) : base(context) { }
public Task<CustomerModel> GetModelAsync(long id)
{
return Queryable.Where(CustomerExpression.Id(id)).Select(CustomerExpression.Model).SingleOrDefaultAsync();
}
public Task<Grid<CustomerModel>> GridAsync(GridParameters parameters)
{
return Queryable.Select(CustomerExpression.Model).GridAsync(parameters);
}
public Task InactivateAsync(Customer customer)
{
return UpdatePartialAsync(customer.Id, new { customer.Status });
}
public async Task<IEnumerable<CustomerModel>> ListModelAsync()
{
return await Queryable.Select(CustomerExpression.Model).ToListAsync();
}
}
public static class CustomerExpression
{
public static Expression<Func<Customer, CustomerModel>> Model => customer => new CustomerModel
{
Id = user.Id,
FirstName = user.Name.FirstName,
LastName = user.Name.LastName,
Email = user.Email.Value
};
public static Expression<Func<Customer, bool>> Id(long id) => customer => customer.Id == id;
}