Loic.rafaelfgx.Architecture 1.0.0

dotnet add package Loic.rafaelfgx.Architecture --version 1.0.0
                    
NuGet\Install-Package Loic.rafaelfgx.Architecture -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Loic.rafaelfgx.Architecture" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Loic.rafaelfgx.Architecture" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Loic.rafaelfgx.Architecture" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Loic.rafaelfgx.Architecture --version 1.0.0
                    
#r "nuget: Loic.rafaelfgx.Architecture, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#addin nuget:?package=Loic.rafaelfgx.Architecture&version=1.0.0
                    
Install Loic.rafaelfgx.Architecture as a Cake Addin
#tool nuget:?package=Loic.rafaelfgx.Architecture&version=1.0.0
                    
Install Loic.rafaelfgx.Architecture as a Cake Tool

Architecture

alternate text is missing from this package README image

https://rafaelfgx-architecture.herokuapp.com

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!

Technologies

Practices

  • Clean Architecture
  • Clean Code
  • SOLID Principles
  • Separation of Concerns
  • DDD (Domain-Driven Design)

Run

<details> <summary>Command Line</summary>

Prerequisites
Steps
  1. Open directory source\Web\Frontend in command line and execute npm i.
  2. Open directory source\Web in command line and execute dotnet run.
  3. Open https://localhost:8090.

</details>

<details> <summary>Visual Studio Code</summary>

Prerequisites
Steps
  1. Open directory source\Web\Frontend in command line and execute npm i.
  2. Open source directory in Visual Studio Code.
  3. Press F5.

</details>

<details> <summary>Visual Studio</summary>

Prerequisites
Steps
  1. Open directory source\Web\Frontend in command line and execute npm i.
  2. Open source\Architecture.sln in Visual Studio.
  3. Set Architecture.Web as startup project.
  4. Press F5.

</details>

<details> <summary>Docker</summary>

Prerequisites
Steps
  1. Execute docker-compose up --build -d in root directory.
  2. Open http://localhost:8090.

</details>

Nuget Packages

Source: https://github.com/rafaelfgx/DotNetCore

Published: https://www.nuget.org/profiles/rafaelfgx

Layers

Web: Frontend and API.

Application: Flow control.

Domain: Business rules and domain logic.

Model: Data transfer objects.

Database: Data persistence.

Web

Frontend

Service

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);
}

Guard

export class AppGuard implements CanActivate {
    constructor(private readonly appAuthService: AppAuthService) { }

    canActivate() {
        if (this.appAuthService.authenticated()) { return true; }
        this.appAuthService.signin();
        return false;
    }
}

ErrorHandler

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);
    }
}

HttpInterceptor

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);
    }
}

API

Startup

public sealed class Startup
{
    public void Configure(IApplicationBuilder application)
    {
        application.UseException();
        application.UseHttps();
        application.UseRouting();
        application.UseResponseCompression();
        application.UseAuthentication();
        application.UseAuthorization();
        application.UseEndpointsMapControllers();
        application.UseSpa();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSecurity();
        services.AddResponseCompression();
        services.AddControllersWithJsonOptionsAndAuthorizationPolicy();
        services.AddSpa();
        services.AddContext();
        services.AddServices();
    }
}

Controller

[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();
}

Application

Service

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.UpdateStatusAsync(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);

        await _unitOfWork.SaveChangesAsync();

        return Result.Success();
    }
}

Factory

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)
        );
    }
}

Domain

Entity

public sealed class Customer : Entity<long>
{
    public Customer(long id) => Id = id;

    public Customer
    (
        long id,
        Name name,
        Email email
    )
    {
        Id = 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;
}

ValueObject

public sealed record Name(string FirstName, string LastName);

Model

Model

public sealed record CustomerModel
{
    public long Id { get; init; }

    public string FirstName { get; init; }

    public string LastName { get; init; }

    public string Email { get; init; }
}

ModelValidator

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();
}

Database

Context

public sealed class Context : DbContext
{
    public Context(DbContextOptions options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ApplyConfigurationsFromAssembly(typeof(Context).Assembly).Seed();
    }
}

Configuration

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();
        });
    }
}

Repository

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 async Task<IEnumerable<CustomerModel>> ListModelAsync()
    {
        return await Queryable.Select(CustomerExpression.Model).ToListAsync();
    }

    public Task UpdateStatusAsync(Customer customer)
    {
        return UpdatePartialAsync(new { customer.Id, customer.Status });
    }
}

Expression

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;
}
Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net5.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 1 7/31/2021