Clean Architecture — Layers & Dependency Rule
Clean Architecture organizes code into concentric rings where dependencies always point inward — Domain is the core with no external dependencies. Infrastructure implements interfaces defined in Domain. API is the outermost layer. This makes Domain logic unit-testable without a database and enables swapping EF Core for Dapper or PostgreSQL for SQL Server.
Project Structure & Dependency Inversion
// ━━ Clean Architecture project structure ━━
//
// MyApp.sln
// ├── src/
// │ ├── MyApp.Domain/ → Entities, Value Objects, Domain Events, Interfaces
// │ │ ├── Entities/
// │ │ ├── ValueObjects/
// │ │ ├── Events/
// │ │ └── Interfaces/ → IProductRepository, IUnitOfWork (domain-owned)
// │ │
// │ ├── MyApp.Application/ → Use Cases, Commands, Queries, Validators
// │ │ ├── Products/
// │ │ │ ├── Commands/ → CreateProductCommand, UpdateProductCommand
// │ │ │ ├── Queries/ → GetProductByIdQuery, GetProductsQuery
// │ │ │ └── Validators/
// │ │ ├── Common/ → BaseHandler, Result<T>, PaginatedList<T>
// │ │ └── DependencyInjection.cs
// │ │
// │ ├── MyApp.Infrastructure/ → EF Core, Redis, Email SMTP, S3, Stripe
// │ │ ├── Persistence/ → AppDbContext, Migrations, Repository impls
// │ │ ├── Caching/
// │ │ ├── Messaging/
// │ │ └── DependencyInjection.cs
// │ │
// │ └── MyApp.Api/ → Controllers, Minimal API endpoints, Program.cs
// │ ├── Controllers/
// │ ├── Endpoints/
// │ ├── Middleware/
// │ └── Program.cs
// │
// └── tests/
// ├── MyApp.Domain.Tests/
// ├── MyApp.Application.Tests/
// └── MyApp.Api.IntegrationTests/
// ━━ Dependency Rule (strictly enforced) ━━
// Domain → knows nothing (no NuGet packages!)
// Application→ depends on Domain only
// Infrastructure → depends on Domain + Application interfaces, adds EF Core, Redis, etc.
// Api → depends on Application, references Infrastructure for DI registration
// ━━ Domain project (.csproj) — ZERO external dependencies ━━
// <Project Sdk="Microsoft.NET.Sdk">
// <PropertyGroup>
// <TargetFramework>net8.0</TargetFramework>
// </PropertyGroup>
// <!-- NO <PackageReference> here — pure C# -->
// </Project>
// ━━ Application DI extension ━━
public static class ApplicationDependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
var assembly = typeof(ApplicationDependencyInjection).Assembly;
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));
services.AddValidatorsFromAssembly(assembly);
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
return services;
}
}
// ━━ Infrastructure DI extension ━━
public static class InfrastructureDependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(config.GetConnectionString("DefaultConnection")));
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IEmailService, SmtpEmailService>();
services.AddSingleton<IDateTimeService, UtcDateTimeService>();
return services;
}
}
// ━━ Program.cs — thin, composed from extensions ━━
// builder.Services.AddApplication();
// builder.Services.AddInfrastructure(builder.Configuration);
// builder.Services.AddPresentation(); // from Api layer
// Placeholder types
interface IPipelineBehavior<TReq, TRes> { }
class ValidationBehavior<TReq, TRes> : IPipelineBehavior<TReq, TRes> { }
class LoggingBehavior<TReq, TRes> : IPipelineBehavior<TReq, TRes> { }
interface IProductRepository { }
interface IUnitOfWork { }
interface IEmailService { }
interface IDateTimeService { }
interface IValidator<T> { }
class ProductRepository : IProductRepository { }
class UnitOfWork : IUnitOfWork { }
class SmtpEmailService : IEmailService { }
class UtcDateTimeService : IDateTimeService { }
class AppDbContext { }Tip
Tip
Practice Clean Architecture Layers Dependency Rule in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
.NET provides a unified platform for building various application types
Practice Task
Note
Practice Task — (1) Write a working example of Clean Architecture Layers Dependency Rule from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Clean Architecture Layers Dependency Rule is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready dotnet code.