C4 Architecture Model - Petfolio Service¶
This document describes the architecture of the Petfolio Service using the C4 model (Context, Containers, Components, Code).
Table of Contents¶
- Level 1: System Context
- Level 2: Container
- Level 3: Component
- Level 4: Code
- CQRS Request Flow
- Summary
Level 1: System Context Diagram¶
Shows how the Petfolio Service fits into the broader ecosystem and its external dependencies.
C4Context
title System Context Diagram - Petfolio Service
Person(petOwner, "Pet Owner", "Manages their pets' portfolios via web/mobile")
Person(vetPro, "Veterinarian/Pet Professional", "Manages client animals and records")
System(petfolioService, "Petfolio Service", "Manages pet portfolios, health records, and account information")
System_Ext(mysql, "MySQL Database", "Stores accounts, users, animals, and weight records")
System_Ext(oauth, "OAuth Providers", "Google, Microsoft, Facebook authentication (Planned)")
Rel(petOwner, petfolioService, "Uses", "HTTPS/REST API")
Rel(vetPro, petfolioService, "Uses", "HTTPS/REST API")
Rel(petfolioService, mysql, "Reads from and writes to", "MySQL Protocol:3306")
Rel(petfolioService, oauth, "Authenticates users via", "OAuth 2.0 (Future)")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
External Actors and Systems¶
| Actor/System | Description | Protocol/Interface |
|---|---|---|
| Pet Owners | Primary users managing their pets' health and information | HTTPS REST API (Port 7195) |
| Veterinarians/Pet Professionals | Manage client animals, track health records | HTTPS REST API (Port 7195) |
| MySQL Database | Primary data store for all entities | MySQL Protocol (Port 3306) |
| OAuth Providers | Future: Google, Microsoft, Facebook for authentication | OAuth 2.0 (Planned) |
Level 2: Container Diagram¶
Shows the applications and data stores that make up the Petfolio system.
C4Container
title Container Diagram - Petfolio Service
Person(user, "User", "Pet owner or professional")
Container_Boundary(petfolioSystem, "Petfolio System") {
Container(api, "API Application", ".NET 9, C#", "Provides REST API for managing pet portfolios. Four-layer clean architecture: API, Application, Domain, Infrastructure")
ContainerDb(db, "Database", "MySQL 8.0+", "Stores accounts, users, animals, and weight records")
}
Rel(user, api, "Uses", "HTTPS/JSON (Port 7195)")
Rel(api, db, "Reads/Writes", "MySQL Protocol (Port 3306)")
UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1")
Container Details¶
API Application (.NET 9)¶
Technology Stack:
- ASP.NET Core 9
- MediatR 13.1.0 (CQRS)
- FluentValidation 12.1.0
- AutoMapper 15.0.1
- Entity Framework Core 9.0
- Swashbuckle (Swagger/OpenAPI)
Architecture Layers:
- API Layer - Controllers, middleware, HTTP concerns
- Application Layer - CQRS (commands/queries), handlers, validators, DTOs
- Domain Layer - Entities, value objects, business logic (zero dependencies)
- Infrastructure Layer - EF Core, repositories, database access
Endpoints:
https://localhost:7195/api/Animals/*https://localhost:7195/api/Accounts/*https://localhost:7195/api/Users/*https://localhost:7195/swagger/index.html(API documentation)
MySQL Database¶
Version: 8.0+ Provider: Pomelo.EntityFrameworkCore.MySql 9.0
Tables:
- Accounts - Business accounts for pet owners/professionals
- Users - User profiles with OAuth authentication info
- Animals - Pet records (species, breed, age, etc.)
- Weight - Historical weight records (owned by Animal)
Level 3: Component Diagram¶
Shows the major components within the API Application container.
3.1 Clean Architecture Layer Dependencies¶
graph TB
subgraph "API Layer"
A1[Animals Controller]
A2[Accounts Controller]
A3[Users Controller]
A4[Exception Middleware]
A5[Program.cs / DI Setup]
end
subgraph "Application Layer"
B1[Commands & Queries]
B2[MediatR Handlers]
B3[FluentValidation Validators]
B4[DTOs & AutoMapper]
B5[Validation Behaviour Pipeline]
end
subgraph "Infrastructure Layer"
C1[PetfolioDbContext EF Core]
C2[Repository Implementations]
C3[EF Core Configurations]
C4[Unit of Work]
end
subgraph "Domain Layer CORE"
D1[Entities Account, Animal, User]
D2[Value Objects Email, Name, Weight]
D3[Repository Interfaces]
D4[Domain Events]
D5[Result Pattern & Errors]
end
subgraph "External"
E1[(MySQL Database)]
end
A1 --> B1
A2 --> B1
A3 --> B1
A1 --> B2
A2 --> B2
A3 --> B2
B1 --> D1
B2 --> D1
B2 --> D3
B4 --> D1
B4 --> D2
C1 --> D1
C2 --> D3
C3 --> D1
C3 --> D2
C1 --> E1
C2 --> C1
C4 --> C1
style D1 fill:#e1f5ff
style D2 fill:#e1f5ff
style D3 fill:#e1f5ff
style D4 fill:#e1f5ff
style D5 fill:#e1f5ff
3.2 API Layer Components¶
graph TB
subgraph "API Layer - Controllers"
AC[Animals Controller<br/>POST /Create<br/>GET /Get/{id}<br/>GET /GetAll]
ACC[Accounts Controller<br/>POST /Create<br/>GET /Get/{id}<br/>GET /GetAll]
UC[Users Controller<br/>GET /Get/{id}<br/>GET /GetAll]
end
subgraph "Middleware"
EXC[Exception Handling Middleware<br/>Catches exceptions<br/>Returns ProblemDetails]
end
subgraph "MediatR"
SENDER[ISender<br/>Send commands/queries]
end
AC --> SENDER
ACC --> SENDER
UC --> SENDER
SENDER --> EXC
3.3 Application Layer Components (CQRS)¶
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
3.4 Domain Layer Components¶
graph TB
subgraph "Entities Rich Domain Models"
E1[Account<br/>+ Create<br/>+ AddUser<br/>+ RemoveUser]
E2[Animal<br/>+ Create<br/>+ SetDateOfBirth<br/>+ SetCurrentWeight]
E3[User<br/>+ Create<br/>+ SetEmailAddress]
end
subgraph "Value Objects Immutable"
VO1[Email<br/>RFC 5322 validation]
VO2[Name<br/>1-100 chars]
VO3[Weight<br/>Kg/Lb/G conversion]
VO4[Species]
VO5[Breed]
VO6[Age<br/>Calculated from DOB]
end
subgraph "Repository Interfaces"
R1[IAnimalRepository]
R2[IAccountRepository]
R3[IUserRepository]
R4[IUnitOfWork]
end
subgraph "Domain Events"
DE1[AnimalCreatedEvent]
DE2[AccountCreatedEvent]
DE3[AccountUserAddedEvent]
end
subgraph "Common Types"
CT1[Result<T><br/>Success/Failure pattern]
CT2[PetfolioError<br/>Code + Message]
CT3[Entity Base Class]
end
E2 --> VO1
E2 --> VO2
E2 --> VO3
E3 --> VO1
E1 --> VO2
E2 --> DE1
E1 --> DE2
3.5 Infrastructure Layer Components¶
graph TB
subgraph "Data Access"
DBC[PetfolioDbContext<br/>DbSet<Account><br/>DbSet<User><br/>DbSet<Animal><br/>SaveChangesAsync]
end
subgraph "EF Core Configurations"
CFG1[AccountConfiguration<br/>Table mapping<br/>Relationships<br/>Indexes]
CFG2[AnimalConfiguration<br/>Value object conversions<br/>Owned Weight records]
CFG3[UserConfiguration<br/>Email index<br/>OAuth indexes]
end
subgraph "Repository Implementations"
REPO1[AnimalRepository<br/>implements IAnimalRepository]
REPO2[AccountRepository<br/>implements IAccountRepository]
REPO3[UserRepository<br/>implements IUserRepository]
REPO4[UnitOfWork<br/>Transaction coordination]
end
DBC --> CFG1
DBC --> CFG2
DBC --> CFG3
REPO1 --> DBC
REPO2 --> DBC
REPO3 --> DBC
REPO4 --> DBC
DBC --> DB[(MySQL Database)]
Level 4: Code Diagram¶
Selected examples showing the internal structure of key components.
4.1 Animal Entity - Rich Domain Model¶
classDiagram
class Entity {
<<abstract>>
+Guid Id
+DateTime CreatedAtUtc
+DateTime UpdatedAtUtc
#List~IDomainEvent~ _domainEvents
+IReadOnlyList~IDomainEvent~ DomainEvents
}
class Animal {
-Guid _id
-Guid _accountId
-Name _name
-Species _species
-Breed _breed
-Gender _gender
-Age? _age
-Description? _description
-List~Weight~ _weightRecords
+Guid Id
+Guid AccountId
+Name Name
+Species Species
+Breed Breed
+Gender Gender
+Age? Age
+Description? Description
+IReadOnlyList~Weight~ WeightRecords
+Create(accountId, name, species, breed, gender, description)$ Animal
+SetDateOfBirth(DateTime dateOfBirth) void
+SetCurrentWeight(Weight weight) void
-Animal()
}
class Name {
<<sealed record>>
+string Value
+string Normalised
+Create(string value)$ Name
-Name()
-Name(string value, string normalised)
}
class Weight {
<<sealed record>>
+double Value
+UnitOfWeight Unit
+DateTime RecordedOn
+Create(double value, UnitOfWeight unit, DateTime recordedOn)$ Weight
+ConvertTo(UnitOfWeight targetUnit) Weight
-Weight()
}
class Species {
<<sealed record>>
+string Value
+string Normalised
+Create(string value)$ Species
}
class Gender {
<<enumeration>>
Male
Female
Unknown
}
Entity <|-- Animal
Animal "1" *-- "1" Name
Animal "1" *-- "1" Species
Animal "1" *-- "0..*" Weight
Animal "1" *-- "1" Gender
4.2 Value Object Pattern - Email¶
classDiagram
class Email {
<<sealed record>>
+string Value
+string Normalised
+Create(string value)$ Email
-Email()
-Email(string value, string normalised)
-ValidateEmail(string email)$ void
}
note for Email "Immutable value object\n- Private parameterless constructor for EF Core\n- Create() factory validates RFC 5322\n- Max length 254 chars\n- Normalises to lowercase\n- Throws ArgumentException if invalid"
4.3 CQRS Pattern - Command and Handler¶
classDiagram
class ICommand~TResponse~ {
<<interface>>
}
class ICommandHandler~TCommand, TResponse~ {
<<interface>>
+Handle(TCommand command, CancellationToken ct) Task~Result~TResponse~~
}
class CreateAnimalCommand {
+Guid AccountId
+string Name
+string Species
+string Breed
+string Gender
+string? Description
}
class CreateAnimalCommandHandler {
-IAnimalRepository _animalRepository
-IUnitOfWork _unitOfWork
+CreateAnimalCommandHandler(repository, unitOfWork)
+Handle(CreateAnimalCommand, CancellationToken) Task~Result~Guid~~
}
class CreateAnimalCommandValidator {
+CreateAnimalCommandValidator()
+RuleFor(x => x.Name)
+RuleFor(x => x.Species)
+RuleFor(x => x.AccountId)
}
ICommand~Guid~ <|.. CreateAnimalCommand
ICommandHandler~CreateAnimalCommand, Guid~ <|.. CreateAnimalCommandHandler
CreateAnimalCommandHandler ..> CreateAnimalCommand : handles
4.4 Repository Pattern¶
classDiagram
class IAnimalRepository {
<<interface>>
+GetByIdAsync(Guid id, CancellationToken ct) Task~Animal?~
+GetAllAsync(CancellationToken ct) Task~List~Animal~~
+CreateAsync(Animal animal, CancellationToken ct) Task
}
class AnimalRepository {
-PetfolioDbContext _context
+AnimalRepository(PetfolioDbContext context)
+GetByIdAsync(Guid id, CancellationToken ct) Task~Animal?~
+GetAllAsync(CancellationToken ct) Task~List~Animal~~
+CreateAsync(Animal animal, CancellationToken ct) Task
}
class IUnitOfWork {
<<interface>>
+SaveChangesAsync(CancellationToken ct) Task~int~
}
class UnitOfWork {
-PetfolioDbContext _context
+UnitOfWork(PetfolioDbContext context)
+SaveChangesAsync(CancellationToken ct) Task~int~
}
IAnimalRepository <|.. AnimalRepository
IUnitOfWork <|.. UnitOfWork
4.5 Result Pattern¶
classDiagram
class Result~T~ {
<<sealed>>
+bool IsSuccess
+bool IsFailure
+T Value
+PetfolioError Error
+Success(T value)$ Result~T~
+Failure(PetfolioError error)$ Result~T~
-Result(T value, bool isSuccess, PetfolioError error)
}
class PetfolioError {
+string Code
+string Message
+PetfolioError(string code, string message)
+None$ PetfolioError
}
Result~T~ "1" *-- "1" PetfolioError
note for Result~T~ "Type-safe error handling\n- Success: Contains value\n- Failure: Contains error\n- Used at Application layer only\n- Domain throws exceptions"
CQRS Request Flow¶
Complete Request Pipeline for CreateAnimalCommand¶
sequenceDiagram
actor User
participant Controller as AnimalsController
participant MediatR as MediatR Pipeline
participant Validator as ValidationBehaviour
participant FluentVal as CreateAnimalCommandValidator
participant Handler as CreateAnimalCommandHandler
participant Domain as Animal.Create()
participant Repo as IAnimalRepository
participant UoW as IUnitOfWork
participant DB as MySQL Database
User->>Controller: POST /api/Animals/Create<br/>{name, species, breed, ...}
Controller->>MediatR: _sender.Send(CreateAnimalCommand)
rect rgb(255, 245, 230)
Note over MediatR,FluentVal: Step 1: Automatic Validation
MediatR->>Validator: Execute ValidationBehaviour
Validator->>FluentVal: Validate(command)
alt Validation Fails
FluentVal-->>Validator: Validation errors
Validator-->>Controller: throw ValidationException
Controller-->>User: 400 Bad Request<br/>{errors: [...]}
else Validation Passes
FluentVal-->>Validator: Valid
end
end
rect rgb(230, 245, 255)
Note over MediatR,DB: Step 2: Command Handler Execution
MediatR->>Handler: Handle(CreateAnimalCommand)
Handler->>Domain: Animal.Create(accountId, name, ...)
alt Domain Validation Fails
Domain-->>Handler: throw ArgumentException
Handler->>Handler: Catch exception
Handler-->>MediatR: Result.Failure(error)
else Domain Validation Passes
Domain->>Domain: Raise AnimalCreatedDomainEvent
Domain-->>Handler: Return Animal entity
Handler->>Repo: CreateAsync(animal)
Repo->>Repo: _context.Animals.Add(animal)
Repo-->>Handler: Task completed
Handler->>UoW: SaveChangesAsync()
UoW->>DB: INSERT INTO Animals...
DB-->>UoW: Success
UoW-->>Handler: Changes saved
Handler-->>MediatR: Result.Success(animalId)
end
end
MediatR-->>Controller: Result<Guid>
alt Result.IsSuccess
Controller-->>User: 201 Created<br/>{id: "guid", ...}
else Result.IsFailure
Controller-->>User: 400 Bad Request<br/>{error: {...}}
end
Query Request Flow¶
sequenceDiagram
actor User
participant Controller as AnimalsController
participant MediatR as MediatR Pipeline
participant Handler as GetAnimalQueryHandler
participant Repo as IAnimalRepository
participant DB as MySQL Database
participant Mapper as AutoMapper
User->>Controller: GET /api/Animals/Get/{id}
Controller->>MediatR: _sender.Send(GetAnimalQuery)
MediatR->>Handler: Handle(GetAnimalQuery)
Handler->>Repo: GetByIdAsync(id)
Repo->>DB: SELECT * FROM Animals WHERE Id = @id
alt Animal Not Found
DB-->>Repo: null
Repo-->>Handler: null
Handler-->>MediatR: Result.Failure(AnimalErrors.NotFound)
MediatR-->>Controller: Result<AnimalDto> (failure)
Controller-->>User: 404 Not Found
else Animal Found
DB-->>Repo: Animal entity
Repo-->>Handler: Animal entity
Handler->>Mapper: Map<AnimalDto>(animal)
Mapper-->>Handler: AnimalDto
Handler-->>MediatR: Result.Success(animalDto)
MediatR-->>Controller: Result<AnimalDto> (success)
Controller-->>User: 200 OK<br/>{id, name, species, ...}
end
Summary¶
C4 Architecture Overview¶
| Level | Focus | Key Elements |
|---|---|---|
| Context | System in environment | Users (pet owners, vets), Petfolio Service, MySQL, OAuth (planned) |
| Container | Applications & data stores | .NET 9 API (4-layer architecture), MySQL 8.0 database |
| Component | Major structural components | Controllers, CQRS handlers, entities, repositories, EF Core |
| Code | Class structure & patterns | Rich domain models, value objects, Result pattern, MediatR pipeline |
Key Architectural Patterns¶
Clean Architecture (Dependency Rule)¶
Dependency Flow:
- API -> Application + Infrastructure
- Application -> Domain only
- Infrastructure -> Domain only
- Domain -> NOTHING (zero dependencies)
CQRS (Command Query Responsibility Segregation)¶
Commands (Write operations):
- Create, Update, Delete operations
- Return
Result<T>(success/failure) - Include validation via FluentValidation
- Modify state and persist changes
Queries (Read operations):
- Read-only operations
- Return
Result<T>with DTOs - No state modification
- Optimised for data retrieval
Validation Strategy (Two-Layer)¶
Layer 1: Domain Validation
- Value objects:
Create()factory methods throwArgumentException - Entities: Business rule validation throws exceptions
- Enforces invariants and business rules
- Example: Email must be valid RFC 5322, Name 1-100 chars
Layer 2: Application Validation
- FluentValidation validators for DTOs/Commands
- Runs automatically via
ValidationBehaviourMediatR pipeline - Executes BEFORE handler (handler never runs if validation fails)
- Example: Required fields, format checks, business constraints
Result Pattern¶
Purpose: Type-safe error handling without exceptions at application layer
Usage:
- Application layer returns
Result<T>(Success or Failure) - Domain layer throws exceptions (converted by handlers)
- Controllers convert
Result<T>to HTTP status codes
Benefits:
- Explicit error handling
- No silent failures
- Clear success/failure states
- Type-safe error information
Technology Stack¶
| Layer | Technologies |
|---|---|
| API | ASP.NET Core 9, Swashbuckle (Swagger) |
| Application | MediatR 13.1, FluentValidation 12.1, AutoMapper 15.0 |
| Domain | Pure C# (no dependencies) |
| Infrastructure | EF Core 9.0, Pomelo MySQL Provider 9.0 |
| Testing | xUnit, Shouldly, NSubstitute, Bogus, Testcontainers |
Database Schema¶
Relationships:
Key Features:
- GUID primary keys
- RESTRICT delete on Account relationships (prevent orphans)
- CASCADE delete on Weight (owned by Animal)
- Indexes on foreign keys and email
- Check constraints on Weight values
- Unique constraint on (AuthProvider, AuthProviderUserId)
API Endpoints¶
| Resource | Method | Endpoint | Description |
|---|---|---|---|
| Animals | POST | /api/Animals/Create |
Create new animal |
| Animals | GET | /api/Animals/Get/{id} |
Get animal by ID |
| Animals | GET | /api/Animals/GetAll |
List all animals |
| Accounts | POST | /api/Accounts/Create |
Create new account |
| Accounts | GET | /api/Accounts/Get/{id} |
Get account by ID |
| Accounts | GET | /api/Accounts/GetAll |
List all accounts |
| Users | GET | /api/Users/Get/{id} |
Get user by ID |
| Users | GET | /api/Users/GetAll |
List all users |
Base URL: https://localhost:7195
API Documentation: https://localhost:7195/swagger/index.html
Design Principles Applied¶
SOLID Principles
- Single Responsibility: Each class has one reason to change
- Open/Closed: Extensible via interfaces, closed for modification
- Liskov Substitution: Interfaces and abstract classes enable substitution
- Interface Segregation: Focused repository interfaces
- Dependency Inversion: All layers depend on Domain abstractions
Domain-Driven Design
- Rich domain models with behaviour
- Immutable value objects
- Aggregate roots (Account, Animal, User)
- Domain events for cross-aggregate communication
- Ubiquitous language in code
Clean Code
- One class per file
- Feature folders organisation
- Consistent naming conventions
- English UK spelling throughout
- Comprehensive testing (unit + integration)