Skip to content

Clean Architecture Analysis & Enforcement

Executive Summary

Status: ✅ COMPLIANT

Your project follows Clean Architecture principles correctly with proper dependency flow and layer separation. The implementation uses modern architectural patterns including CQRS with MediatR, FluentValidation, and the Result pattern for robust, maintainable code.

Current Architecture Patterns

  • ✅ Clean Architecture: Proper dependency inversion with layers pointing toward Domain
  • ✅ CQRS: Commands and Queries with separate handlers via MediatR
  • ✅ MediatR: Mediator pattern for decoupled request/response handling
  • ✅ FluentValidation: Declarative validation with automatic ValidationBehaviour pipeline
  • ✅ Result Pattern: Type-safe error handling without throwing exceptions for business failures
  • ✅ Repository Pattern: Abstract data access through domain interfaces
  • ✅ Unit of Work: Transaction coordination across repositories
  • ✅ Domain Events: Event-driven architecture support (e.g., AnimalCreatedDomainEvent)
  • ✅ Exception Handling Middleware: Centralized exception handling with ProblemDetails

Dependency Analysis

Current Project Structure

┌────────────────────────────────────────────────────┐
│                      API Layer                     │
│  (Petfolio.Service.Api)                            │
│  • Controllers                                     │
│  • Program.cs, Startup configuration               │
│                                                    │
│  Depends on: ↓ Application, ↓ Infrastructure       │
└────────────┬───────────────────────────┬───────────┘
             │                           │
┌────────────▼───────────┐  ┌────────────▼───────────┐
│  Application Layer     │  │  Infrastructure Layer  │
│  (Application)         │  │  (Infrastructure)      │
│  • Services            │  │  • Repositories        │
│  • DTOs                │  │  • DbContext           │
│  • Interfaces          │  │  • EF Core Config      │
│  • Mapping             │  │  • UnitOfWork          │
│                        │  │                        │
│  Depends on: ↓ Domain  │  │  Depends on: ↓ Domain  │
└────────────┬───────────┘  └────────────┬───────────┘
             │                           │
             └─────────────┬─────────────┘
             ┌─────────────▼────────────┐
             │  Domain Layer            │
             │  (Domain)                │
             │  • Entities              │
             │  • Value Objects         │
             │  • Repository Interfaces │
             │  • IUnitOfWork           │
             │                          │
             │  Dependencies: NONE      │
             └──────────────────────────┘

Dependency Verification Results

✅ Domain Layer (Petfolio.Service.Domain)

Project References: None Package References: None External Dependencies: None

Findings:

  • No dependencies on Application, Infrastructure, or API layers
  • No dependencies on external frameworks (EF Core, ASP.NET Core, etc.)
  • Pure business logic with no infrastructure concerns
  • Status: COMPLIANT ✅

✅ Application Layer (Petfolio.Service.Application)

Project References:

  • ✅ Petfolio.Service.Domain

Package References:

  • ✅ AutoMapper (acceptable - mapping library)
  • ✅ FluentValidation (acceptable - validation library)
  • ✅ FluentValidation.DependencyInjectionExtensions (acceptable - DI integration)
  • ✅ MediatR (acceptable - mediator pattern library)

Findings:

  • Only references Domain layer
  • No references to Infrastructure or API layers
  • No EF Core dependencies
  • No ASP.NET Core dependencies
  • Uses IUnitOfWork abstraction instead of DbContext directly
  • Implements CQRS with Commands (CreateAnimalCommand) and Queries (GetAnimalQuery, GetAllAnimalsQuery)
  • All requests/responses handled through MediatR
  • FluentValidation validators for all commands/queries
  • ValidationBehaviour pipeline automatically validates before handler execution
  • Status: COMPLIANT ✅

✅ Infrastructure Layer (Petfolio.Service.Infrastructure)

Project References:

  • ✅ Petfolio.Service.Domain

Package References:

  • ✅ Pomelo.EntityFrameworkCore.MySql (expected for data access)

Findings:

  • Only references Domain layer
  • No references to Application or API layers
  • Implements domain interfaces (IAnimalRepository, IUnitOfWork)
  • Contains EF Core configurations (appropriate for this layer)
  • Status: COMPLIANT ✅

✅ API Layer (Petfolio.Service.Api)

Project References:

  • ✅ Petfolio.Service.Application
  • ✅ Petfolio.Service.Infrastructure

Package References:

  • ✅ Microsoft.AspNetCore.OpenApi
  • ✅ Microsoft.EntityFrameworkCore.Design (for migrations)
  • ✅ Scalar.AspNetCore

Findings:

  • References both Application and Infrastructure (correct for composition root)
  • Controllers use MediatR (IMediator) to send Commands and Queries
  • Controllers delegate to MediatR instead of calling services directly
  • ExceptionHandlingMiddleware provides centralized exception handling with ProblemDetails
  • No direct use of DbContext in controllers
  • Thin controllers that only handle HTTP concerns
  • Status: COMPLIANT ✅

Code Quality Assessment

✅ Domain Layer Quality

Strengths:

  1. Rich domain models with behavior (Animal entity)
  2. Proper validation in entities and value objects
  3. Immutable value objects (Species, Weight, Breed, Gender)
  4. Private setters and factory methods
  5. No anemic models

Example from Animal.cs:

1
2
3
4
5
public void SetCurrentWeight(double? weight, string unit = "kg")
{
    CurrentWeight = weight.HasValue ? Weight.Create(weight.Value, unit) : null;
    UpdateTimestamp();
}

✅ Application Layer Quality

Strengths:

  1. Proper use of Unit of Work pattern
  2. Service methods coordinate operations
  3. Transaction boundaries managed correctly
  4. DTOs separate from domain entities
  5. AutoMapper for entity-DTO conversion

Example from AnimalService.cs:

await _animalRepository.CreateAsync(animal);
await _unitOfWork.SaveChangesAsync(); // Transaction boundary

✅ Infrastructure Layer Quality

Strengths:

  1. Repository pattern implementation
  2. Value object conversions in EF Core configurations
  3. Separate configuration classes for entities
  4. Unit of Work wraps DbContext properly

✅ API Layer Quality

Strengths:

  1. Thin controllers that delegate to MediatR
  2. Uses IMediator to send Commands/Queries
  3. Proper HTTP status codes and Result handling
  4. Centralized exception handling via ExceptionHandlingMiddleware
  5. RESTful design

Example from AnimalsController.cs:

[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] CreateAnimalCommand command)
{
    var result = await mediator.Send(command);

    if (result.IsFailure)
        return BadRequest(result.PetfolioError);

    return CreatedAtAction(nameof(GetById), new { id = result.Value.Id }, result.Value);
}

ExceptionHandlingMiddleware:

  • Catches ValidationException from FluentValidation and returns 400 Bad Request
  • Converts exceptions to ProblemDetails format (RFC 7807)
  • Provides consistent error responses across the API

Architecture Violations: None Found

No violations detected! The project correctly implements Clean Architecture.

Enforcement Strategies

Even though your project is currently compliant, here are strategies to prevent future violations:

1. NetArchTest - Automated Architecture Testing

Installation: Add to a test project:

dotnet add package NetArchTest.Rules

Implementation:

Create Petfolio.Service.ArchitectureTests project:

// ArchitectureTests.cs
using NetArchTest.Rules;
using Xunit;

namespace Petfolio.Service.ArchitectureTests;

public class ArchitectureTests
{
    private const string DomainNamespace = "Petfolio.Service.Domain";
    private const string ApplicationNamespace = "Petfolio.Service.Application";
    private const string InfrastructureNamespace = "Petfolio.Service.Infrastructure";
    private const string ApiNamespace = "Petfolio.Service.Api";

    [Fact]
    public void Domain_Should_Not_HaveDependencyOnOtherLayers()
    {
        // Arrange
        var assembly = typeof(Domain.Entities.Animal).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .Should()
            .NotHaveDependencyOn(ApplicationNamespace)
            .And()
            .NotHaveDependencyOn(InfrastructureNamespace)
            .And()
            .NotHaveDependencyOn(ApiNamespace)
            .GetResult();

        // Assert
        Assert.True(result.IsSuccessful,
            $"Domain layer has dependencies on: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
    }

    [Fact]
    public void Domain_Should_Not_HaveDependencyOnExternalFrameworks()
    {
        // Arrange
        var assembly = typeof(Domain.Entities.Animal).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .Should()
            .NotHaveDependencyOn("EntityFrameworkCore")
            .And()
            .NotHaveDependencyOn("Microsoft.AspNetCore")
            .GetResult();

        // Assert
        Assert.True(result.IsSuccessful,
            "Domain layer should not depend on external frameworks");
    }

    [Fact]
    public void Application_Should_Not_HaveDependencyOnInfrastructure()
    {
        // Arrange
        var assembly = typeof(Application.Services.AnimalService).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .Should()
            .NotHaveDependencyOn(InfrastructureNamespace)
            .And()
            .NotHaveDependencyOn(ApiNamespace)
            .GetResult();

        // Assert
        Assert.True(result.IsSuccessful,
            $"Application layer has dependencies on: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
    }

    [Fact]
    public void Application_Should_Not_ReferenceDatabaseConcerns()
    {
        // Arrange
        var assembly = typeof(Application.Services.AnimalService).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .Should()
            .NotHaveDependencyOn("EntityFrameworkCore")
            .GetResult();

        // Assert
        Assert.True(result.IsSuccessful,
            "Application layer should not reference Entity Framework");
    }

    [Fact]
    public void Infrastructure_Should_Not_HaveDependencyOnApplication()
    {
        // Arrange
        var assembly = typeof(Infrastructure.Repositories.AnimalRepository).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .Should()
            .NotHaveDependencyOn(ApplicationNamespace)
            .And()
            .NotHaveDependencyOn(ApiNamespace)
            .GetResult();

        // Assert
        Assert.True(result.IsSuccessful,
            $"Infrastructure layer has dependencies on: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
    }

    [Fact]
    public void Controllers_Should_OnlyDependOnApplicationServices()
    {
        // Arrange
        var assembly = typeof(Api.Controllers.AnimalsController).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .That()
            .ResideInNamespace("Petfolio.Service.Api.Controllers")
            .Should()
            .NotHaveDependencyOn("DbContext")
            .And()
            .NotHaveDependencyOn("Petfolio.Service.Infrastructure.Repositories")
            .GetResult();

        // Assert
        Assert.True(result.IsSuccessful,
            "Controllers should not directly depend on repositories or DbContext");
    }

    [Fact]
    public void Repositories_Should_ImplementDomainInterfaces()
    {
        // Arrange
        var assembly = typeof(Infrastructure.Repositories.AnimalRepository).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .That()
            .ResideInNamespace("Petfolio.Service.Infrastructure.Repositories")
            .And()
            .HaveNameEndingWith("Repository")
            .Should()
            .ImplementInterface(typeof(Domain.Repositories.IAnimalRepository).GetInterfaces().FirstOrDefault() ?? typeof(object))
            .GetResult();

        // Assert - This is informational, repositories should implement domain interfaces
        // but we can't test this generically without reflection
    }

    [Fact]
    public void Domain_Entities_Should_InheritFromBaseEntity()
    {
        // Arrange
        var assembly = typeof(Domain.Entities.Animal).Assembly;

        // Act
        var result = Types.InAssembly(assembly)
            .That()
            .ResideInNamespace("Petfolio.Service.Domain.Entities")
            .And()
            .AreClasses()
            .And()
            .DoNotHaveName("BaseEntity")
            .Should()
            .Inherit(typeof(Domain.Common.BaseEntity))
            .GetResult();

        // Assert
        Assert.True(result.IsSuccessful,
            $"Entities not inheriting from BaseEntity: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
    }

    [Fact]
    public void ValueObjects_Should_BeRecords()
    {
        // Arrange
        var assembly = typeof(Domain.ValueObjects.Species).Assembly;

        // Act
        var types = assembly.GetTypes()
            .Where(t => t.Namespace == "Petfolio.Service.Domain.ValueObjects")
            .ToList();

        var nonRecords = types.Where(t => !t.IsValueType && !t.IsClass).ToList();

        // Assert - Value objects should typically be records for immutability
        // This is a guideline rather than strict rule
    }
}

Add to CI/CD Pipeline:

# .github/workflows/architecture-tests.yml
name: Architecture Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '9.0.x'
      - name: Run Architecture Tests
        run: dotnet test Petfolio.Service.ArchitectureTests/Petfolio.Service.ArchitectureTests.csproj

2. EditorConfig Rules

Add to .editorconfig:

# .editorconfig (add to root)
root = true

[*.cs]
# Enforce naming conventions
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = warning
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.symbols = interface
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.style = begins_with_i

# Async methods should end with Async
dotnet_naming_rule.async_methods_end_in_async.severity = warning
dotnet_naming_rule.async_methods_end_in_async.symbols = async_methods
dotnet_naming_rule.async_methods_end_in_async.style = ends_with_async

# Private fields should be _camelCase
dotnet_naming_rule.private_members_with_underscore.symbols  = private_fields
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_members_with_underscore.severity = warning

3. Code Analysis Rules (Roslyn Analyzers)

Add to Directory.Build.props:

Create Directory.Build.props in the solution root:

<Project>
  <PropertyGroup>
    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>

Add layer-specific rules:

Create Petfolio.Service.Domain/Directory.Build.props:

1
2
3
4
5
6
7
8
<Project>
  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

  <PropertyGroup>
    <!-- Domain should have no external dependencies -->
    <NoWarn>$(NoWarn)</NoWarn>
  </PropertyGroup>
</Project>

4. Pre-commit Hooks with Husky.NET

Installation:

1
2
3
dotnet new tool-manifest
dotnet tool install Husky
dotnet husky install

Create .husky/pre-commit:

1
2
3
4
5
6
7
8
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# Run architecture tests
dotnet test Petfolio.Service.ArchitectureTests/Petfolio.Service.ArchitectureTests.csproj --no-build --no-restore

# Run build to check for analyzer warnings
dotnet build --no-restore /p:TreatWarningsAsErrors=true

5. Project Reference Validation Script

Create scripts/validate-architecture.ps1:

# validate-architecture.ps1
$ErrorActionPreference = "Stop"

Write-Host "Validating Clean Architecture..." -ForegroundColor Cyan

# Check Domain has no project references
$domainRefs = Select-Xml -Path "Petfolio.Service.Domain/Petfolio.Service.Domain.csproj" -XPath "//ProjectReference"
if ($domainRefs) {
    Write-Error "Domain layer should have no project references!"
    exit 1
}

# Check Application only references Domain
$appRefs = Select-Xml -Path "Petfolio.Service.Application/Petfolio.Service.Application.csproj" -XPath "//ProjectReference/@Include"
$invalidAppRefs = $appRefs | Where-Object { $_.Node.Value -notlike "*Domain.csproj" }
if ($invalidAppRefs) {
    Write-Error "Application layer should only reference Domain!"
    exit 1
}

# Check Infrastructure only references Domain
$infraRefs = Select-Xml -Path "Petfolio.Service.Infrastructure/Petfolio.Service.Infrastructure.csproj" -XPath "//ProjectReference/@Include"
$invalidInfraRefs = $infraRefs | Where-Object { $_.Node.Value -notlike "*Domain.csproj" }
if ($invalidInfraRefs) {
    Write-Error "Infrastructure layer should only reference Domain!"
    exit 1
}

Write-Host "Clean Architecture validation passed!" -ForegroundColor Green

Add to CI:

- name: Validate Architecture
  run: pwsh ./scripts/validate-architecture.ps1

6. Documentation and Team Guidelines

Create CONTRIBUTING.md:

# Contributing Guidelines

## Clean Architecture Rules

### Never Do This:
- Add Infrastructure references to Application layer
- Add DbContext to Application services
- Put business logic in controllers
- Create anemic domain models
- Skip validation in entities

### Always Do This:
- Use IUnitOfWork for transaction management
- Put business logic in domain entities
- Use factory methods for entity creation
- Validate in value objects and entities
- Keep controllers thin

### Before Submitting PR:
- [ ] Run architecture tests: `dotnet test`
- [ ] Check for analyzer warnings: `dotnet build`
- [ ] Review dependency graph
- [ ] Update documentation if needed

7. Dependency Graph Visualization

Install dependency visualization tool:

dotnet tool install --global dotnet-depends

Generate and review dependency graph:

1
2
3
4
5
# Generate graph
dotnet depends --export graph Petfolio.Service.sln

# This creates a .dgml file you can open in Visual Studio
# Or export to other formats

Add to PR template:

1
2
3
4
5
## Architecture Checklist
- [ ] No circular dependencies
- [ ] Dependencies flow toward Domain
- [ ] Architecture tests pass
- [ ] No warnings from analyzers

Monitoring and Maintenance

Regular Architecture Reviews

Monthly Checklist:

  • Run architecture tests
  • Review dependency graph
  • Check for new analyzer warnings
  • Review new code for patterns
  • Update documentation

Metrics to Track

  1. Dependency Health:
  2. Number of project references per layer
  3. External package dependencies per layer

  4. Code Quality:

  5. Cyclomatic complexity
  6. Test coverage
  7. Number of architecture test failures

  8. Compliance:

  9. Percentage of entities with validation
  10. Percentage of controllers following thin pattern
  11. Repository pattern adherence
Tool Purpose Priority
NetArchTest.Rules Automated architecture testing High
Roslyn Analyzers Code quality enforcement High
Husky.NET Pre-commit validation Medium
dotnet-depends Dependency visualization Medium
EditorConfig Code style enforcement Medium

Implementation Priority

Phase 1 (Immediate - Week 1)

  1. ✅ Add NetArchTest.Rules
  2. ✅ Create architecture test project
  3. ✅ Add to CI/CD pipeline

Phase 2 (Short-term - Week 2-3)

  1. Configure Roslyn analyzers
  2. Add EditorConfig
  3. Create validation scripts

Phase 3 (Long-term - Month 1)

  1. Set up Husky pre-commit hooks
  2. Create contribution guidelines
  3. Establish regular review schedule

Conclusion

Your project correctly implements Clean Architecture with:

  • ✅ Proper dependency flow (toward Domain)
  • ✅ Unit of Work pattern for transactions
  • ✅ Repository pattern implementation
  • ✅ Rich domain models
  • ✅ Thin controllers
  • ✅ No architecture violations

Recommended Next Steps:

  1. Implement NetArchTest for continuous validation
  2. Add architecture tests to CI/CD
  3. Configure code analyzers
  4. Document architecture decisions

This will ensure your project maintains Clean Architecture as it grows and new developers join the team.