Skip to content

The petfolio-service repository

The PetFolio Service is a .NET 9 backend API built with Clean Architecture and CQRS pattern for managing pet portfolios. The architecture emphasises separation of concerns, testability, and maintainability through strict layering and dependency rules.

Technology Stack

Technology Category Purpose
.NET 9 Framework Modern, high-performance web API framework
ASP.NET Core Web Framework RESTful API hosting and HTTP layer
MySQL Database Relational data storage
Entity Framework ORM Database access and migrations
MediatR Mediator CQRS implementation and request handling
FluentValidation Validation Input validation with fluent API
AutoMapper Mapping DTO to entity mapping
xUnit Testing Test framework for all test types
Shouldly Assertions Fluent assertion library
NSubstitute Mocking Test doubles for dependencies
Testcontainers Integration Tests Docker-based test database provisioning
Bogus Test Data Fake data generation for tests
Swashbuckle Documentation OpenAPI/Swagger documentation

Design Patterns

File Naming Conventions

File Type Pattern Example
Entity PascalCase Animal.cs
Value Object PascalCase Name.cs, Weight.cs
Command [Verb][Entity]Command CreateAnimalCommand.cs
Query Get[Entity(s)]Query GetAnimalQuery.cs
Handler [Command/Query]Handler CreateAnimalCommandHandler.cs
Validator [Command/Query]Validator CreateAnimalCommandValidator.cs
DTO [Entity]Dto AnimalDto.cs
Repository I[Entity]Repository IAnimalRepository.cs
Error Definitions [Entity]Errors AnimalErrors.cs
Test [ClassUnderTest]Tests AnimalTests.cs

Rules:

  • One class per file, file name matches class name
  • Tests mirror source structure exactly
  • Use cases grouped under feature folders

Clean Architecture Layers

Dependencies flow inward toward the Domain layer, ensuring the core business logic has no external dependencies:

graph TD
    A[API Layer] --> B[Application Layer]
    A --> C[Infrastructure Layer]
    B --> D[Domain Layer]
    C --> D

    style D fill:#e8f5e9
    style B fill:#fff3e0
    style C fill:#e3f2fd
    style A fill:#fce4ec

    subgraph "Petfolio.Service.Api"
        A
    end

    subgraph "Petfolio.Service.Application"
        B
    end

    subgraph "Petfolio.Service.Infrastructure"
        C
    end

    subgraph "Petfolio.Service.Domain"
        D
    end

Layer Responsibilities

  1. Domain - Core business logic, entities, value objects, repository interfaces (NO dependencies)
  2. Application - Use cases (Commands/Queries), handlers, validators, DTOs (depends on Domain only)
  3. Infrastructure - External concerns like database, EF Core configurations (depends on Domain)
  4. API - HTTP endpoints, controllers, middleware (depends on Application and Infrastructure)

CQRS Pattern

Commands and Queries are strictly separated for clarity and scalability:

graph LR
    A[Client Request] --> B{Command or Query?}
    B -->|Write| C[Command Handler]
    B -->|Read| D[Query Handler]
    C --> E[Repository Write]
    D --> F[Repository Read]
    E --> G[Database]
    F --> G

    style C fill:#ffcdd2
    style D fill:#c8e6c9

Commands and Queries

  • Commands: Write operations (Create, Update, Delete) returning Result<T>
  • Queries: Read operations returning Result<T>
  • All requests routed through MediatR pipeline with automatic validation

Result Pattern

Error handling uses the Result pattern to avoid exceptions in the happy path:

  • Domain layer: Throws exceptions for validation and business rule violations
  • Application layer: Catches domain exceptions and converts to Result<T> or Result<T, Error>
  • API layer: Converts Result<T> to appropriate HTTP status codes
// Application layer handler returns Result
public async Task<Result<AnimalDto>> Handle(GetAnimalQuery query, CancellationToken cancellationToken)
{
    var animal = await _repository.GetByIdAsync(query.Id);

    if (animal is null)
        return Result.Failure<AnimalDto>(AnimalErrors.NotFound);

    return Result.Success(_mapper.Map<AnimalDto>(animal));
}

Value Objects

Immutable types enforcing business invariants:

  • Always sealed record with init accessors
  • Private parameterless constructor for EF Core (marked [ExcludeFromCodeCoverage])
  • Static Create() factory method throwing ArgumentException on validation failure

Two-Layer Validation

  1. Domain Validation: Business invariants enforced in value objects and entities (throws exceptions)
  2. Application Validation: FluentValidation validators run automatically via ValidationBehaviour pipeline

Security

=== Input Validation

  • FluentValidation: Automatic validation pipeline prevents invalid data reaching handlers
  • Domain Validation: Business rules enforced at the domain boundary
  • DTO Pattern: External data never directly mapped to entities

Database Security

  • Parameterised Queries: EF Core prevents SQL injection by default
  • Connection String Management: Stored in appsettings (development) or environment variables (production)
  • Migrations: Version-controlled schema changes

API Security

  • HTTPS Only: Enforced in production
  • CORS Configuration: Controlled cross-origin access
  • Swagger: Disabled in production builds

Performance

Database Optimisations

  • Async/Await: All database operations are asynchronous
  • Query Efficiency: Repository pattern allows optimised queries
  • Connection Pooling: Built-in ADO.NET connection pooling

Build Performance

  • Centrally Managed Packages: Directory.Packages.props ensures consistent versions
  • Incremental Builds: Only recompile changed projects
  • Minimal API Surface: Thin controllers delegate to MediatR

Test Performance

  • Shared Test Container: Integration tests reuse single MySQL container
  • Parallel Execution: xUnit runs tests in parallel where possible
  • Fast Feedback: Full test suite runs in ~1.5 seconds