Chat about this codebase

AI-powered code exploration

Online

Project Overview

This ASP.NET Core Web API implements a fully layered, testable service using Clean Architecture and Domain-Driven Design. It enforces separation of concerns, promotes maintainability, and scales from small APIs to enterprise solutions.

Purpose

Provide a robust starting point for business-critical APIs by:

  • Encapsulating domain logic in isolated layers
  • Enabling flexible feature evolution without coupling presentation to persistence
  • Delivering standardized patterns for command/query segregation and security

Problems Addressed

  • Monolithic controllers with mixed business and infrastructure code
  • Difficult integration testing due to tangled dependencies
  • Inconsistent handling of read vs. write operations
  • Hard-coded authentication rules and manual token management

Core Concepts & Technologies

  • Clean Architecture:
    • Domain project holds entities, value objects, aggregates, and domain services
    • Application project defines use-case handlers, DTOs, interfaces
    • Infrastructure project implements persistence (EF Core, MS SQL), external integrations
    • WebAPI project hosts controllers, configures Host/Startup, dependency injection
  • Domain-Driven Design:
    • Models business concepts explicitly in the Domain layer
    • Applies ubiquitous language across code and documentation
  • CQRS with MediatR:
    • Separates read (queries) from write (commands) workflows
    • Defines IRequest and IRequestHandler for each use case
    • Dispatches via injected IMediator for loose coupling
  • JWT Authentication:
    • Secures endpoints using Microsoft.AspNetCore.Authentication.JwtBearer
    • Issues access tokens on successful login
    • Validates scoped roles/claims in policies and controllers

Solution Structure

WebAPI.sln contains five projects:

  • Domain: Entities, value objects, domain events, repository interfaces
  • Application: Commands, queries, handlers, validators, service interfaces
  • Infrastructure: EF Core DbContext, repository implementations, migrations setup
  • WebAPI: Controllers, middleware, DI registrations, JWT configuration
  • Tests: xUnit suites for unit and integration tests

Getting Started

  1. Open WebAPI.sln in Visual Studio
  2. Set connection string in Infrastructure/appsettings.json
  3. Run migrations:
    Update-Database -Project Infrastructure -StartupProject WebAPI
    
  4. Launch WebAPI; authenticate via /api/auth/login to obtain a JWT

This structure accelerates feature delivery while maintaining code quality, testability, and security.

Getting Started

This guide walks you through cloning the repo, configuring your environment, running the Web API, and making your first authenticated request.

Prerequisites

  • .NET 8 SDK
  • SQL Server instance (local or remote)
  • Visual Studio 2022+ or VS Code with C# extension

1. Clone the Repository

git clone https://github.com/yeucp/CleanArquitecture.git
cd CleanArquitecture

2. Configure Database & Migrations

Update Connection String

Open src/WebAPI/appsettings.Development.json (or appsettings.json) and set your SQL Server connection:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=CleanArchDb;Trusted_Connection=True;"
  },
  "JwtSettings": { /* ... */ }
}

Automatic Migrations on Startup

The app applies pending EF Core migrations during startup via MigrationExtension. No manual step required.

In Program.cs you’ll see:

var app = builder.Build();
app.ApplyMigration();    // Runs pending migrations
app.Run();

Manual Migration (Optional)

dotnet ef migrations add InitialCreate \
  --project src/WebAPI \
  --startup-project src/WebAPI
dotnet ef database update \
  --project src/WebAPI \
  --startup-project src/WebAPI

3. Run the Web API

Using .NET CLI

dotnet run --project src/WebAPI

The API listens by default on:

Using Visual Studio

  • Open solution in Visual Studio
  • Select the WebAPI launch profile
  • Press F5

4. Explore Swagger UI

Navigate to https://localhost:5001/swagger to view and test all endpoints. JWT support is configured under “Authorize”.

5. Obtain a JWT

Via Swagger

  1. Expand AuthenticationPOST /api/v1/Authentication/login
  2. Click Try it out
  3. Provide credentials:
    {
      "username": "admin",
      "password": "P@ssw0rd"
    }
    
  4. Execute and copy the returned token.

Via Curl

curl -X POST https://localhost:5001/api/v1/Authentication/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"P@ssw0rd"}'

Via C# HttpClient

using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

public class LoginRequest { public string Username { get; set; } public string Password { get; set; } }
public class LoginResponse { public string Token { get; set; } public DateTime Expiration { get; set; } }

async Task<LoginResponse> GetJwtAsync()
{
    using var client = new HttpClient { BaseAddress = new Uri("https://localhost:5001/") };
    var request = new LoginRequest { Username = "admin", Password = "P@ssw0rd" };
    var response = await client.PostAsJsonAsync("api/v1/Authentication/login", request);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<LoginResponse>();
}

You now have a valid JWT. Add it to the Authorization: Bearer {token} header for subsequent API calls.

Architecture & Layered Structure

This section describes the Clean Architecture layout of the system and traces the lifecycle of a "Create Customer" request from the API endpoint down to persistence and back.

Clean Architecture Layers

• Presentation (Web API)
– Defines Controllers, DTOs, filters, problem details
– References Application via MediatR

• Application
– Contains Commands/Queries, Handlers, DTOs, Validation, Pipeline Behaviors
– Depends on Domain abstractions (interfaces, entities)

• Domain
– Defines Entities, Value Objects, Aggregates, Domain Events, Repository interfaces
– Pure business logic, no external dependencies

• Infrastructure
– Implements persistence (EF Core, Dapper), messaging, authentication, external APIs
– References Domain (for entities/interfaces) and Application (for DTO mapping, if needed)

Dependency Graph

Presentation → Application → Domain
Infrastructure → Application & Domain

Request Flow: “Create Customer” Example

1. HTTP POST → Controller

using Application.Customers.Create;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
    private readonly IMediator _mediator;
    public CustomersController(IMediator mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> Create(CreateCustomerRequest dto)
    {
        // Map DTO to MediatR Command
        var command = new CreateCustomerCommand(
            Name: dto.Name,
            LastName: dto.LastName,
            Email: dto.Email,
            PhoneNumber: dto.PhoneNumber,
            CustomerStatusId: dto.CustomerStatusId
        );

        ErrorOr<Guid> result = await _mediator.Send(command);

        return result.Match(
            id => CreatedAtAction(nameof(GetById), new { id }, new { id }),
            errors => Problem(errors)
        );
    }
}

2. MediatR Pipeline

ValidationBehavior<CreateCustomerCommand, ErrorOr<Guid>> runs registered IValidator<CreateCustomerCommand>.
• On failure returns Error.Validation list; on success invokes handler.

3. Application Handler

using Domain.Customers;
using Domain.Common.Errors;
using ErrorOr;

public class CreateCustomerCommandHandler
    : IRequestHandler<CreateCustomerCommand, ErrorOr<Guid>>
{
    private readonly ICustomerRepository _repo;
    public CreateCustomerCommandHandler(ICustomerRepository repo) => _repo = repo;

    public async Task<ErrorOr<Guid>> Handle(CreateCustomerCommand cmd, CancellationToken ct)
    {
        // Domain logic: ensure unique email
        if (await _repo.ExistsByEmailAsync(cmd.Email, ct))
            return Errors.Customer.DuplicateEmail;

        var customer = Customer.Create(
            id: CustomerId.CreateUnique(),
            name: cmd.Name,
            lastName: cmd.LastName,
            email: cmd.Email,
            phoneNumber: PhoneNumber.Create(cmd.PhoneNumber)!,
            statusId: new CustomerStatusId(cmd.CustomerStatusId)
        );

        await _repo.AddAsync(customer, ct);
        return customer.Id.Value;
    }
}

4. Domain Model

public sealed class Customer : AggregateRoot<CustomerId>
{
    public string Name { get; private set; }
    public string LastName { get; private set; }
    public Email Email { get; private set; }
    public PhoneNumber PhoneNumber { get; private set; }
    public CustomerStatusId StatusId { get; private set; }

    private Customer(...) { /* ORM ctor */ }
    private Customer(CustomerId id, ...) : base(id) { /* init */ }

    public static Customer Create(CustomerId id, string name, string lastName,
        string email, PhoneNumber phoneNumber, CustomerStatusId statusId)
    {
        // invariants, events
        return new Customer(id, name, lastName, Email.Create(email), phoneNumber, statusId);
    }
}

5. Infrastructure Persistence

using Microsoft.EntityFrameworkCore;
using Domain.Customers;

public class CustomerRepository : ICustomerRepository
{
    private readonly AppDbContext _db;
    public CustomerRepository(AppDbContext db) => _db = db;

    public Task<bool> ExistsByEmailAsync(string email, CancellationToken ct) =>
        _db.Customers.AnyAsync(c => c.Email.Value == email, ct);

    public async Task AddAsync(Customer customer, CancellationToken ct)
    {
        _db.Customers.Add(customer);
        await _db.SaveChangesAsync(ct);
    }
}

6. Response to Client

• On success returns HTTP 201 with { "id": "{newGuid}" }.
• On validation or domain error returns HTTP 400 with standard ProblemDetails payload.

Dependency Injection Setup

In Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Presentation & Application
builder.Services.AddControllers();
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssemblyContaining<CreateCustomerCommand>());
builder.Services.AddValidatorsFromAssemblyContaining<CreateCustomerCommandValidator>();
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

// Domain abstractions & Infrastructure
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// Authentication, ProblemDetails, etc.
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection(JwtSettings.SectionName));
builder.Services.AddSingleton<IJwtTokenGenerator, JwtTokenGenerator>();
builder.Services.AddSingleton<ProblemDetailsFactory>(sp =>
    new APIProblemDetailsFactory(sp.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value));

var app = builder.Build();
app.MapControllers();
app.Run();

Practical Tips

• Keep Controllers thin: map DTOs to Commands/Queries only.
• Let PipelineBehaviors handle cross-cutting concerns (validation, logging, auth).
• Application layer orchestrates use cases; avoid business rules in Handlers.
• Domain layer remains free of external dependencies.
• Infrastructure implements interfaces, configured by DI.

Core Concepts & Patterns

This section highlights reusable patterns and helper utilities in the yeucp/CleanArchitecture repository. Each subsection explains how to apply or extend these core components in your application.

APIProblemDetailsFactory: Enriching ProblemDetails with Domain Error Codes

Purpose
Automatically surfaces domain validation errors (ErrorOr/Error objects) as an errorCodes extension on every ProblemDetails/ValidationProblemDetails response.

Essential Details

  • Inherits from ASP.NET Core’s ProblemDetailsFactory.
  • Overrides CreateProblemDetails and CreateValidationProblemDetails.
  • In ApplyProblemDetailsDefaults, reads a List<Error> from HttpContext.Items[HttpContextItemKeys.Errors].
  • Exposes each Error.Code under extensions.errorCodes.

Code Snippet

private void ApplyProblemDetailsDefaults(
    HttpContext httpContext,
    ProblemDetails problemDetails,
    int statusCode)
{
    problemDetails.Status ??= statusCode;
    if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
    {
        problemDetails.Title  ??= clientErrorData.Title;
        problemDetails.Type   ??= clientErrorData.Link;
    }

    var traceId = Activity.Current?.Id ?? httpContext.TraceIdentifier;
    if (traceId != null)
        problemDetails.Extensions["traceId"] = traceId;

    var errors = httpContext.Items[HttpContextItemKeys.Errors] as List<Error>;
    if (errors != null)
    {
        problemDetails.Extensions.Add(
            "errorCodes",
            errors.Select(e => e.Code)
        );
    }
}

Registration & Usage

  1. Register the custom factory:
    services.AddSingleton<ProblemDetailsFactory, APIProblemDetailsFactory>();
    services.Configure<ApiBehaviorOptions>(opts => {
      // override client‐error mappings if needed
    });
    
  2. Populate HttpContext.Items[HttpContextItemKeys.Errors] before returning a Problem:
    // Inside a controller or middleware handling ErrorOr<T>
    if (serviceResult.IsError)
    {
        HttpContext.Items[HttpContextItemKeys.Errors] = serviceResult.Errors.ToList();
        return Problem(
            statusCode: 400,
            detail: "One or more validation errors occurred."
        );
    }
    
  3. Sample response payload:
    {
      "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
      "title": "One or more validation errors occurred.",
      "status": 400,
      "instance": "/api/customers",
      "extensions": {
        "traceId": "00-abc123...",
        "errorCodes": ["Customer.PhoneNumber","Customer.StatusNotFound"]
      }
    }
    

ValidationBehavior: Automatic FluentValidation in MediatR Pipeline

Purpose
Intercepts every IRequest<TResponse> in MediatR. If an IValidator<TRequest> exists, runs validation before invoking the handler. On failure, returns ErrorOr errors without executing the handler.

How It Works

  • Generic behavior where TRequest : IRequest<TResponse> and TResponse : IErrorOr.
  • Resolves IValidator<TRequest>. If none, calls next().
  • On validation errors, maps each ValidationFailure to Error.Validation(property, message) and returns them as TResponse.

Key Code

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : IErrorOr
{
    private readonly IValidator<TRequest>? _validator;

    public ValidationBehavior(IValidator<TRequest>? validator = null)
        => _validator = validator;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        if (_validator == null)
            return await next();

        var result = await _validator.ValidateAsync(request, ct);
        if (result.IsValid)
            return await next();

        var errors = result.Errors
            .ConvertAll(f => Error.Validation(f.PropertyName, f.ErrorMessage));
        return (dynamic)errors;
    }
}

Configuration

services
  .AddValidatorsFromAssembly(typeof(CreateCustomerCommandValidator).Assembly)
  .AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

Ensure commands/queries implement IRequest<IErrorOr<YourResponse>>:

public record CreateCustomerCommand(
    string Name,
    string LastName,
    string Email,
    string PhoneNumber
) : IRequest<IErrorOr<CustomerResult>>;

Handling Results

var result = await mediator.Send(cmd);
if (result.IsError)
{
    foreach (var err in result.Errors)
        Console.WriteLine($"Validation failed: {err.Description}");
    return;
}
// proceed with result.Value

JwtTokenGenerator: Issuing Signed JWTs

Purpose
Configure and use JwtTokenGenerator to issue HMAC-SHA256 JWTs based on JwtSettings.

Configuration

  1. Add to appsettings.json:
    {
      "JwtSettings": {
        "SecretKey": "your-256-bit-secret",
        "ExpiryMinutes": 60,
        "Issuer": "YourAppName",
        "Audience": "YourAppClients"
      }
    }
    
  2. Register in DI:
    builder.Services
      .Configure<JwtSettings>(configuration.GetSection(JwtSettings.SectionName))
      .AddSingleton<IJwtTokenGenerator, JwtTokenGenerator>();
    

How It Works

  • Reads settings via IOptions<JwtSettings>.
  • GenerateToken(string userId, string username) builds signing credentials, sets standard claims (sub, given_name), and issues a JwtSecurityToken:
    public string GenerateToken(string userId, string username) {
      var credentials = new SigningCredentials(
        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_JwtSettings.SecretKey)),
        SecurityAlgorithms.HmacSha256);
    
      var claims = new[] {
        new Claim(JwtRegisteredClaimNames.Sub, userId),
        new Claim(JwtRegisteredClaimNames.GivenName, username)
      };
    
      var token = new JwtSecurityToken(
        issuer: _JwtSettings.Issuer,
        audience: _JwtSettings.Audience,
        expires: DateTime.Now.AddDays(_JwtSettings.ExpiryMinutes),
        claims: claims,
        signingCredentials: credentials);
    
      return new JwtSecurityTokenHandler().WriteToken(token);
    }
    

Practical Usage

// Inside a login handler/service
var jwt = _jwtTokenGenerator.GenerateToken(user.Id, user.UserName);
return new LoginResult { Token = jwt };

Tip: Use AddMinutes instead of AddDays if you intend minute-level expiry.

Raising and Dispatching Domain Events

Purpose
Illustrates how aggregates raise domain events and how ApplicationDbContext dispatches them via MediatR after a successful save.

  1. Raising Events in Aggregates

    • AgregateRoot provides Raise(DomainEvent event) to queue events.
    public record CustomerActivated(Guid CustomerId) : DomainEvent(CustomerId);
    
    public class Customer : AgregateRoot
    {
        public Guid Id { get; private set; }
        public bool IsActive { get; private set; }
    
        public void Activate()
        {
            if (IsActive) return;
            IsActive = true;
            Raise(new CustomerActivated(Id));
        }
    }
    
    • DomainEvent implements INotification:
      public record DomainEvent(Guid Id) : INotification;
      
  2. Publishing in ApplicationDbContext

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        var domainEvents = ChangeTracker
            .Entries<AgregateRoot>()
            .SelectMany(e => e.Entity.GetDomainEvents())
            .ToList();
    
        var result = await base.SaveChangesAsync(cancellationToken);
    
        foreach (var domainEvent in domainEvents)
            await _publisher.Publish(domainEvent, cancellationToken);
    
        return result;
    }
    
  3. Writing an Event Handler

    public class CustomerActivatedHandler : INotificationHandler<CustomerActivated>
    {
        public Task Handle(CustomerActivated notification, CancellationToken cancellationToken)
        {
            Console.WriteLine($"Customer {notification.CustomerId} activated");
            return Task.CompletedTask;
        }
    }
    
  4. DI Setup

    services
      .AddDbContext<ApplicationDbContext>(options => /* ... */)
      .AddMediatR(typeof(ApplicationDbContext).Assembly);
    

By following these patterns, you maintain consistency in error handling, validation, authentication, and domain event dispatch across your application.

API Reference & Usage

This section describes available REST endpoints, sample requests/responses, and the underlying MediatR commands, queries, and handlers. Register all services in your Startup.cs or Program.cs to enable dependency injection.


Authentication

POST /Authentication/login

Verifies credentials and returns a JWT.

Request
• URL: /Authentication/login
• Method: POST
• Headers:
- Content-Type: application/json
• Body:

{ "username": "alice", "password": "P@ssw0rd!" }

Successful Response (200 OK)

{ "token": "<JWT>" }

Error Response (400 Bad Request)

{
  "code": "Login.Failure",
  "description": "Invalid credentials"
}

cURL Example

curl -X POST https://api.example.com/Authentication/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"P@ssw0rd!"}'

Handler: LoginCommandHandler

public sealed class LoginCommandHandler 
  : IRequestHandler<LoginCommand, ErrorOr<LoginResult>>
{
    private readonly ILoginRepository _loginRepository;
    private readonly IJwtTokenGenerator _jwtTokenGenerator;

    public LoginCommandHandler(
        ILoginRepository loginRepository,
        IJwtTokenGenerator jwtTokenGenerator)
    {
        _loginRepository = loginRepository;
        _jwtTokenGenerator = jwtTokenGenerator;
    }

    public async Task<ErrorOr<LoginResult>> Handle(
        LoginCommand command, CancellationToken ct)
    {
        if (!await _loginRepository
            .ValidateUser(command.Username, command.Password))
        {
            return Error.Unauthorized(
              code: "Login.Failure", 
              description: "Invalid credentials");
        }

        var token = _jwtTokenGenerator.GenerateToken(
            subject: command.Username, 
            username: command.Username);

        return new LoginResult(token);
    }
}

public record LoginResult(string Token);

Controller Integration

[ApiController]
[Route("Authentication")]
public class AuthenticationController : ControllerBase
{
    private readonly ISender _mediator;

    public AuthenticationController(ISender mediator) 
      => _mediator = mediator;

    [HttpPost("login")]
    public async Task<IActionResult> Login(LoginCommand command)
    {
        var result = await _mediator.Send(command);

        if (result.IsError)
            return BadRequest(new {
                Code = result.FirstError.Code,
                Description = result.FirstError.Description
            });

        return Ok(result.Value);
    }
}

Customers

Endpoints for creating, updating, and deleting customer records. All handlers return ErrorOr<Unit>.

POST /customers

Create a new customer.

Request
• URL: /customers
• Method: POST
• Headers:
- Content-Type: application/json
- Authorization: Bearer {token}
• Body:

{
  "name": "Alice",
  "lastName": "Smith",
  "email": "alice@example.com",
  "phoneNumber": "+15551234567",
  "customerStatus": "d290f1ee-6c54-4b01-90e6-d701748f0851"
}

Response (201 Created)
No body.

Handler: CreateCustomerCommandHandler

public sealed class CreateCustomerCommandHandler
  : IRequestHandler<CreateCustomerCommand, ErrorOr<Unit>>
{
  public async Task<ErrorOr<Unit>> Handle(
    CreateCustomerCommand cmd, CancellationToken ct)
  {
    if (PhoneNumber.Create(cmd.PhoneNumber) is not PhoneNumber phone)
      return Errors.Customer.PhoneNumberWithBadFormat;

    if (await _statusRepo
        .GetByIdAsync(new CustomerStatusId(cmd.CustomerStatus))
      is not CustomerStatus status)
      return Errors.Customer.CustomerStatusNotFound;

    var customer = new Customer(
      new CustomerId(Guid.NewGuid()),
      cmd.Name, cmd.LastName, cmd.Email, true, phone, status);

    _repo.Add(customer);
    await _uow.SaveChangesAsync(ct);

    return Unit.Value;
  }
}

PUT /customers/{id}

Update an existing customer.

Request
• URL: /customers/{id}
• Method: PUT
• Headers:
- Content-Type: application/json
- Authorization: Bearer {token}
• Body:

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "name": "Alice",
  "lastName": "Johnson",
  "email": "alice.j@example.com",
  "active": false,
  "phoneNumber": "+15557654321",
  "customerStatus": "f7a3b1c3-9c1c-4bf5-8ffe-123456789abc"
}

Response (200 OK)
No body.

Handler: UpdateCustomerCommandHandler

public sealed class UpdateCustomerCommandHandler
  : IRequestHandler<UpdateCustomerCommand, ErrorOr<Unit>>
{
  public async Task<ErrorOr<Unit>> Handle(
    UpdateCustomerCommand cmd, CancellationToken ct)
  {
    if (!await _repo.ExistsAsync(new CustomerId(cmd.Id)))
      return Errors.Customer.CustomerNotFound;

    if (PhoneNumber.Create(cmd.PhoneNumber) is not PhoneNumber phone)
      return Errors.Customer.PhoneNumberWithBadFormat;

    if (await _statusRepo
        .GetByIdAsync(new CustomerStatusId(cmd.CustomerStatus))
      is not CustomerStatus status)
      return Errors.Customer.CustomerStatusNotFound;

    var updated = Customer.UpdateCustomer(
      cmd.Id, cmd.Name, cmd.LastName, cmd.Email,
      cmd.Active, phone, status);

    _repo.Update(updated);
    await _uow.SaveChangesAsync(ct);

    return Unit.Value;
  }
}

DELETE /customers/{id}

Delete a customer.

Request
• URL: /customers/{id}
• Method: DELETE
• Headers:
- Authorization: Bearer {token}

Response (200 OK)
No body.

Handler: DeleteCustomerCommandHandler

public sealed class DeleteCustomerCommandHandler
  : IRequestHandler<DeleteCustomerCommand, ErrorOr<Unit>>
{
  public async Task<ErrorOr<Unit>> Handle(
    DeleteCustomerCommand cmd, CancellationToken ct)
  {
    var id = new CustomerId(cmd.Id);
    if (await _repo.GetByIdAsync(id) is not Customer toDelete)
      return Error.NotFound("Customer.Id", "Customer not found");

    _repo.Delete(toDelete);
    await _uow.SaveChangesAsync(ct);

    return Unit.Value;
  }
}

CustomersController

[ApiController]
[Route("customers")]
public class CustomersController : ControllerBase
{
    private readonly ISender _mediator;
    public CustomersController(ISender mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> Post(
      [FromBody] CreateCustomerCommand cmd)
    {
      var result = await _mediator.Send(cmd);
      return result.Match(
        _ => Created("", null),
        errs => Problem(errs));
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Put(
      Guid id, [FromBody] UpdateCustomerCommand cmd)
    {
      if (id != cmd.Id)
        return Problem(new[] {
          Error.Validation("Customer.Id","URL id and body id must match")
        });

      var result = await _mediator.Send(cmd);
      return result.Match(_ => Ok(), errs => Problem(errs));
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(Guid id)
    {
      var result = await _mediator.Send(new DeleteCustomerCommand(id));
      return result.Match(_ => Ok(), errs => Problem(errs));
    }
}

Customer Status

Endpoints for listing and creating customer statuses. All handlers return ErrorOr<…>.

GET /customer-status

Fetch all statuses.

Request
• URL: /customer-status
• Method: GET
• Headers:
- Authorization: Bearer {token}

Response (200 OK)

[
  { "id": "d290f1ee-6c54-4b01-90e6-d701748f0851", "description": "Active" },
  { "id": "f7a3b1c3-9c1c-4bf5-8ffe-123456789abc", "description": "Inactive" }
]

cURL Example

curl -H "Authorization: Bearer $TOKEN" \
     -X GET https://api.example.com/customer-status

Handler: GetAllCustomerStatusQueryHandler

public async Task<ErrorOr<IReadOnlyList<CustomerStatusResponse>>> Handle(
  GetAllCustomerStatusQuery _, CancellationToken ct)
{
  var statuses = await _customerStatusRepository.GetAllAsync();
  return statuses
    .Select(cs => new CustomerStatusResponse(
        cs.Id.Value, cs.Description))
    .ToList();
}

POST /customer-status

Create a new status.

Request
• URL: /customer-status
• Method: POST
• Headers:
- Content-Type: application/json
- Authorization: Bearer {token}
• Body:

{ "description": "VIP" }

Response (200 OK)
No body.

cURL Example

curl -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"description":"VIP"}' \
     -X POST https://api.example.com/customer-status

Handler: CreateCustomerStatusCommandHandler

public async Task<ErrorOr<Unit>> Handle(
  CreateCustomerStatusCommand command, CancellationToken ct)
{
  var customerStatus = new CustomerStatus(
    new CustomerStatusId(Guid.NewGuid()), 
    command.Description);

  _customerStatusRepository.Add(customerStatus);
  await _unitOfWork.SaveChangesAsync(ct);
  return Unit.Value;
}

Controller Integration

[ApiController]
[Route("customer-status")]
public class CustomerStatusController : ControllerBase
{
    private readonly ISender _mediator;
    public CustomerStatusController(ISender mediator) => _mediator = mediator;

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
      var result = await _mediator.Send(new GetAllCustomerStatusQuery());
      return result.Match(
        list => Ok(list),
        errs => Problem(errs));
    }

    [HttpPost]
    public async Task<IActionResult> Create(
      CreateCustomerStatusCommand cmd)
    {
      var result = await _mediator.Send(cmd);
      return result.Match(_ => Ok(), errs => Problem(errs));
    }
}

Dependency Injection

services.AddScoped<ILoginRepository, LoginRepository>();
services.AddSingleton<IJwtTokenGenerator, JwtTokenGenerator>();
services.AddScoped<ICustomerRepository, CustomerRepository>();
services.AddScoped<ICustomerStatusRepository, CustomerStatusRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddMediatR(typeof(LoginCommandHandler).Assembly);
## Development & Deployment Guide

This guide covers common development tasks, configuration tweaks, automated database migrations, testing patterns, and service registration in the yeucp/CleanArquitecture solution.

---

### Configuring JwtSettings

Define, bind and consume the `JwtSettings` section from configuration for token generation and validation.

#### 1. JwtSettings class

`Infrastructure/Authentication/JwtSettings.cs` maps to the `"JwtSettings"` section:

```csharp
public class JwtSettings
{
    public const string SectionName = "JwtSettings";
    public string SecretKey { get; init; } = null!;
    public int ExpiryMinutes { get; init; }
    public string Issuer { get; init; } = null!;
    public string Audience { get; init; } = null!;
}
  • SecretKey: long, random string.
  • ExpiryMinutes: token lifetime (> 0).
  • Issuer & Audience: used in validation.

2. appsettings.json

Add under root:

"JwtSettings": {
  "SecretKey": "your-very-long-random-secret-key",
  "ExpiryMinutes": 60,
  "Issuer": "MyApi",
  "Audience": "MyApiClients"
}

3. Binding in Program.cs

using Infrastructure.Authentication;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

// Bind JwtSettings
builder.Services
  .Configure<JwtSettings>(
    builder.Configuration.GetSection(JwtSettings.SectionName)
  );

// Register concrete JwtSettings for direct injection
builder.Services.AddSingleton(sp =>
    sp.GetRequiredService<IOptions<JwtSettings>>().Value
);

// Add authentication, controllers, etc.

4. Consuming in AuthService

using Infrastructure.Authentication;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

public class AuthService
{
    private readonly JwtSettings _jwt;
    public AuthService(JwtSettings jwtSettings) => _jwt = jwtSettings;

    public string GenerateToken(string userId)
    {
        var key    = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.SecretKey));
        var creds  = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, userId) };

        var token = new JwtSecurityToken(
            issuer:             _jwt.Issuer,
            audience:           _jwt.Audience,
            claims:             claims,
            expires:            DateTime.UtcNow.AddMinutes(_jwt.ExpiryMinutes),
            signingCredentials: creds
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Tips:

  • Always inject JwtSettings via DI.
  • Use identical settings in JWT middleware for validation.
  • Rotate SecretKey by updating configuration and restarting.

Applying EF Core Migrations at Startup

Automatically apply pending migrations when the API starts.

MigrationExtension

src/WebAPI/Extensions/MigrationExtension.cs:

using Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace WebAPI.Extensions
{
    public static class MigrationExtension
    {
        public static void ApplyMigration(this WebApplication app)
        {
            using var scope = app.Services.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            dbContext.Database.Migrate();
        }
    }
}

Usage in Program.cs

var builder = WebApplication.CreateBuilder(args);

// 1. Register DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
);

var app = builder.Build();

// 2. Apply migrations before handling requests
app.ApplyMigration();

app.MapControllers();
app.Run();

Guidance:

  • Call ApplyMigration() immediately after Build().
  • For production, consider controlled migration workflows (CI/CD).

Unit Testing CreateCustomerCommandHandler

Verify validation rules and dependency interactions for CreateCustomerCommandHandler.

1. Test Class Setup

using Application.Customers.Create;
using Domain.CustomerStatuses;
using FluentAssertions;
using Moq;
using Xunit;

namespace Application.Customers.UnitTests.Create
{
    public class CreateCustomerCommandHandlerTests
    {
        private readonly Mock<ICustomerRepository> _customerRepo = new();
        private readonly Mock<ICustomerStatusRepository> _statusRepo = new();
        private readonly Mock<IUnitOfWork> _unitOfWork = new();
        private readonly CreateCustomerCommandHandler _handler;

        public CreateCustomerCommandHandlerTests()
        {
            _handler = new CreateCustomerCommandHandler(
                _customerRepo.Object,
                _statusRepo.Object,
                _unitOfWork.Object
            );
        }

        // ... test methods ...
    }
}

2. Validation Failure Test

[Fact]
public async Task Handle_InvalidPhoneFormat_ReturnsValidationError()
{
    var command = new CreateCustomerCommand(
        "Yeudi", "Carazo", "yex@gmail.com",
        phone: "71941273",  // invalid format
        statusId: Guid.Parse("70158537-D4A0-477A-8A5D-CEC1A7E76816")
    );

    var result = await _handler.Handle(command, CancellationToken.None);

    result.IsError.Should().BeTrue();
    var error = result.FirstError;
    error.Type.Should().Be(ErrorType.Validation);
    error.Code.Should().Be(Errors.Customer.PhoneNumberWithBadFormat.Code);
}

3. Successful Creation Test

[Fact]
public async Task Handle_ValidCommand_CreatesCustomerAndCommits()
{
    var validStatus = new CustomerStatus(Guid.NewGuid(), "Active");
    _statusRepo.Setup(r => r.GetByIdAsync(validStatus.Id))
               .ReturnsAsync(validStatus);

    Customer? createdCustomer = null;
    _customerRepo.Setup(r => r.AddAsync(It.IsAny<Customer>()))
                 .Callback<Customer>(c => createdCustomer = c)
                 .Returns(Task.CompletedTask);

    var command = new CreateCustomerCommand(
        "Jane", "Doe", "jane.doe@example.com", "0712345678", validStatus.Id
    );

    var result = await _handler.Handle(command, CancellationToken.None);

    result.IsError.Should().BeFalse();
    _customerRepo.Verify(r => r.AddAsync(It.IsAny<Customer>()), Times.Once);
    _unitOfWork.Verify(u => u.CommitAsync(), Times.Once);

    createdCustomer!.Email.Value.Should().Be("jane.doe@example.com");
}

Tips:

  • Reuse mocks in the constructor.
  • Name tests Handle_[Condition]_[ExpectedOutcome].
  • Use FluentAssertions for clear assertions.
  • Verify repository and unit-of-work calls on happy paths.

Registering Application Layer Services

Centralize MediatR handlers and FluentValidation behaviors.

// src/Application/DependencyInjection.cs
using Application.Common.Behavior;
using FluentValidation;
using MediatR;
using Microsoft.Extensions.DependencyInjection;

namespace Application
{
    public static class DependencyInjection
    {
        public static IServiceCollection AddApplication(this IServiceCollection services)
        {
            // MediatR scans this assembly
            services.AddMediatR(cfg =>
                cfg.RegisterServicesFromAssemblyContaining<ApplicationAssemblyReference>());

            // FluentValidation pipeline
            services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

            // Register all validators
            services.AddValidatorsFromAssemblyContaining<ApplicationAssemblyReference>();

            return services;
        }

        internal class ApplicationAssemblyReference { }
    }
}

Usage in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Register CQRS + validation
builder.Services.AddApplication();

// ... register Infrastructure, Presentation, etc.
var app = builder.Build();
app.Run();

Tips:

  • Add an IValidator<T> for every command/query.
  • To introduce custom behaviors (logging, metrics), register additional IPipelineBehavior<,> after AddApplication().
  • Keep Application layer free of external dependencies beyond MediatR and FluentValidation.