Skip to content

Application layer

The Application layer orchestrates business operations using the CQRS (Command Query Responsibility Segregation) pattern with MediatR. It sits between the API Layer and the Domain Layer, depending only on Domain.

Clean Architecture - use case orchestration

The Application layer contains no business rules and no infrastructure concerns. It coordinates work: receiving commands and queries, validating input, calling domain logic, and returning results.

Responsibility

  • Commands: Write operations that change state (e.g. CreateAnimalCommand)
  • Queries: Read operations that return data (e.g. GetAnimalQuery, GetAllAnimalsQuery)
  • Handlers: Process commands and queries (e.g. CreateAnimalCommandHandler)
  • Validators: FluentValidation validators for input validation (e.g. CreateAnimalCommandValidator)
  • DTOs: Data transfer objects for external communication (e.g. AnimalDto, WeightDto)
  • Mapping: AutoMapper profiles for entity-to-DTO transformations
  • ValidationBehaviour: MediatR pipeline behaviour that validates all requests before handling

Components

graph LR
    subgraph "Commands - write operations"
        CMD1[CreateAnimalCommand<br>+ Handler<br>+ Validator]
        CMD2[CreateAccountCommand<br>+ Handler<br>+ Validator]
    end

    subgraph "Queries - read operations"
        Q1[GetAnimalQuery<br>+ Handler]
        Q2[GetAllAnimalsQuery<br>+ Handler]
        Q3[GetAccountQuery<br>+ Handler]
        Q4[GetUserQuery<br>+ Handler]
    end

    subgraph "Cross-cutting"
        VB[Validation Behaviour<br>MediatR Pipeline<br>Auto-validates before handlers]
        MAP[AutoMapper Profile<br>Entity to DTO mapping]
    end

    subgraph "DTOs"
        DTO1[AnimalDto]
        DTO2[AccountDto]
        DTO3[UserDto]
        DTO4[WeightDto]
    end

    CMD1 --> VB
    CMD2 --> VB
    Q1 --> MAP
    Q2 --> MAP
    MAP --> DTO1
    MAP --> DTO2
    MAP --> DTO3

Commands and queries

Commands and queries are strictly separated. Commands modify state; queries only read. Both are routed through the MediatR pipeline, which runs validation automatically before the handler executes.

Validation pipeline

PetFolio uses FluentValidation with MediatR Pipeline Behaviours to automatically validate all commands and queries before they reach their handlers. This ensures consistent validation across the entire application without manual checks in controllers or handlers.

How validation is configured

Location: Petfolio.Service.Application/DependencyInjection.cs

  1. AddValidatorsFromAssembly - automatically discovers and registers all AbstractValidator<T> classes
  2. AddOpenBehavior(typeof(ValidationBehaviour<,>)) - adds validation to the MediatR pipeline
  3. Validators are registered as scoped services and automatically injected into ValidationBehaviour

How validation works

Controller
    | mediator.Send(command)
MediatR Pipeline
    |
[1] ValidationBehaviour - Runs FIRST for all IBaseCommand requests
    | Collects all validators for TRequest
    | Executes all validators in parallel
    | If any validation fails -> throws ValidationException
    | If validation passes -> continues
[2] Command/Query Handler - Only executes if validation passed
    | Return Result<T>

Creating validators

Location: Petfolio.Service.Application/{Feature}/Validators/

internal sealed class CreateEntityCommandValidator : AbstractValidator<CreateEntityCommand>
{
    public CreateEntityCommandValidator()
    {
        RuleFor(x => x.Name)
            .NotNull().WithMessage(Validation.Name.NullOrEmptyError)
            .NotEmpty().WithMessage(Validation.Name.NullOrEmptyError)
            .MinimumLength(Validation.Name.MinLength).WithMessage(Validation.Name.LengthError)
            .MaximumLength(Validation.Name.MaxLength).WithMessage(Validation.Name.LengthError)
            .Matches(Validation.Name.Pattern).WithMessage(Validation.Name.PatternError);
    }
}

Rules for validators:

  • Use internal sealed class (they are implementation details)
  • Inherit from AbstractValidator<TCommand> or AbstractValidator<TQuery>
  • Define all validation rules in the constructor
  • No manual registration needed - auto-discovered by AddValidatorsFromAssembly
  • Use centralised validation constants from Validation class in Domain layer
  • Validators automatically execute before the command handler runs

Validation error handling

When validation fails, ValidationBehaviour throws ValidationException with all collected errors. Controllers handle this via the Result pattern.

Benefits of this approach:

  • Zero boilerplate: No if (!ModelState.IsValid) checks in controllers
  • Consistent validation: All commands validated the same way
  • Single responsibility: Controllers only handle HTTP, validators only validate
  • Easy testing: Test validators independently, no need to mock validation logic
  • Fail fast: Invalid requests never reach handlers
  • Centralised error handling: All validation errors collected and returned together