Clean Architecture Solution Template for .NET

Clean Architecture Solution Template for .NET

6 min read··

clean-architecturedotnetsoftware-architecture

A Clean Architecture solution template for .NET sets up five projects (Domain, Application, Infrastructure, Persistence, Api), inward-only references, CQRS abstractions, base primitives, and architecture tests that enforce the dependency rule. Setting that up by hand is the same hour of ceremony on every new solution: create the projects, wire the references, add MediatR and FluentValidation, set up DI. None of that work is interesting, and all of it is easy to get subtly wrong.

This article walks through the complete template, piece by piece. At the end, you can package it as a dotnet new template and never do the setup by hand again.

Why a Solution Template?

Every time you start a new Clean Architecture project, you repeat the same steps - creating projects, adding references, configuring DI, setting up MediatR, adding FluentValidation. A solution template saves hours and ensures consistency.

Here's the complete setup I use for production projects.

The Solution Structure

src/
  MyApp.Domain/
  MyApp.Application/
  MyApp.Infrastructure/
  MyApp.Persistence/
  MyApp.Api/
tests/
  MyApp.Domain.UnitTests/
  MyApp.Application.UnitTests/
  MyApp.Infrastructure.IntegrationTests/
  MyApp.Api.FunctionalTests/
  MyApp.ArchitectureTests/

Five source projects. Four layers plus a separate Persistence project (you can merge Infrastructure and Persistence if you prefer - I only split them when the infrastructure surface grows).

Project References (The Dependency Rule)

The dependency rule is the non-negotiable constraint. Every project reference points inward, toward Domain:

Project reference graph for the solution: Api references Application, Infrastructure, and Persistence; Infrastructure and Persistence reference Application; Application references Domain; Domain references nothing

Domain depends on nothing. Application depends only on Domain. Infrastructure and Persistence depend on Application. The API project wires everything together.

Domain Project

The Domain layer contains entities, value objects, domain events, and domain exceptions. No NuGet packages:

<!-- MyApp.Domain.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
</Project>
Domain/
  Entities/
    Order.cs
    Customer.cs
    Product.cs
  ValueObjects/
    Money.cs
    Address.cs
    Email.cs
  Events/
    OrderPlacedDomainEvent.cs
    OrderCancelledDomainEvent.cs
  Exceptions/
    DomainException.cs
  Repositories/
    IOrderRepository.cs
    ICustomerRepository.cs
  Primitives/
    Entity.cs
    AggregateRoot.cs
    IDomainEvent.cs
    IUnitOfWork.cs
    Result.cs
    Error.cs

Base classes:

public abstract class Entity
{
    public Guid Id { get; protected init; }
}

public abstract class AggregateRoot : Entity
{
    private readonly List<IDomainEvent> _domainEvents = [];

    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents;

    protected void RaiseDomainEvent(IDomainEvent domainEvent) =>
        _domainEvents.Add(domainEvent);

    public void ClearDomainEvents() => _domainEvents.Clear();
}

public interface IDomainEvent;

Notice that IDomainEvent is a plain marker interface. A lot of templates declare it as IDomainEvent : MediatR.INotification, which quietly gives your Domain project a MediatR dependency and contradicts the "no packages" rule.

You have two honest options:

  1. Keep the domain pure (what I show above) and adapt domain events to MediatR notifications in the Application layer with a generic wrapper.
  2. Accept the tradeoff and reference MediatR.Contracts from Domain. It's a contracts-only package, but it still couples your domain to a library. And with MediatR going commercial, coupling your innermost layer to it deserves a second thought.

Either is workable. Just make the choice deliberately instead of inheriting it from a template.

Application Project

The Application layer contains use cases, validation, CQRS abstractions, and behaviors.

<!-- MyApp.Application.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
    <PackageReference Include="MediatR" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\MyApp.Domain\MyApp.Domain.csproj" />
  </ItemGroup>
</Project>
Application/
  Abstractions/
    Messaging/
      ICommand.cs
      ICommandHandler.cs
      IQuery.cs
      IQueryHandler.cs
    ICurrentUserService.cs
    IDateTimeProvider.cs
    IEmailService.cs
  Behaviors/
    ValidationBehavior.cs
    LoggingBehavior.cs
  Orders/
    PlaceOrder/
      PlaceOrderCommand.cs
      PlaceOrderCommandHandler.cs
      PlaceOrderValidator.cs
    GetOrderById/
      GetOrderByIdQuery.cs
      GetOrderByIdQueryHandler.cs
      OrderResponse.cs
  DependencyInjection.cs

CQRS abstractions:

public interface ICommand : IRequest<Result>;
public interface ICommand<TResponse> : IRequest<Result<TResponse>>;

public interface ICommandHandler<TCommand>
    : IRequestHandler<TCommand, Result>
    where TCommand : ICommand;

public interface ICommandHandler<TCommand, TResponse>
    : IRequestHandler<TCommand, Result<TResponse>>
    where TCommand : ICommand<TResponse>;

public interface IQuery<TResponse> : IRequest<Result<TResponse>>;

public interface IQueryHandler<TQuery, TResponse>
    : IRequestHandler<TQuery, Result<TResponse>>
    where TQuery : IQuery<TResponse>;

These thin interfaces buy you two things: every handler returns a Result, and pipeline behaviors can target commands or queries specifically (validate commands, cache queries).

DI registration:

// Application/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddApplication(
        this IServiceCollection services)
    {
        var assembly = typeof(DependencyInjection).Assembly;

        services.AddMediatR(config =>
        {
            config.RegisterServicesFromAssembly(assembly);
            config.AddOpenBehavior(typeof(ValidationBehavior<,>));
            config.AddOpenBehavior(typeof(LoggingBehavior<,>));
        });

        services.AddValidatorsFromAssembly(assembly);

        return services;
    }
}

Persistence Project

Handles EF Core configuration:

<!-- MyApp.Persistence.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" />
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\MyApp.Application\MyApp.Application.csproj" />
  </ItemGroup>
</Project>
Persistence/
  ApplicationDbContext.cs
  Configurations/
    OrderConfiguration.cs
    CustomerConfiguration.cs
  Repositories/
    OrderRepository.cs
    CustomerRepository.cs
  DependencyInjection.cs
// Persistence/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddPersistence(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseNpgsql(
                configuration.GetConnectionString("Database")));

        services.AddScoped<IUnitOfWork>(sp =>
            sp.GetRequiredService<ApplicationDbContext>());

        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<ICustomerRepository, CustomerRepository>();

        return services;
    }
}

Infrastructure Project

The Infrastructure layer handles external concerns - email, caching, authentication, file storage:

Infrastructure/
  Authentication/
    CurrentUserService.cs
    JwtConfiguration.cs
  Email/
    EmailService.cs
  Caching/
    CacheService.cs
  Time/
    DateTimeProvider.cs
  DependencyInjection.cs
// Infrastructure/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddScoped<ICurrentUserService, CurrentUserService>();
        services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
        services.AddTransient<IEmailService, EmailService>();

        services.AddJwtAuthentication(configuration);

        return services;
    }
}

API Project

The entry point. Wires everything together:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddApplication()
    .AddPersistence(builder.Configuration)
    .AddInfrastructure(builder.Configuration);

builder.Services.AddOpenApi();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseAuthentication();
app.UseAuthorization();

app.MapOrderEndpoints();
app.MapCustomerEndpoints();

app.Run();

A few deliberate choices here:

  • AddOpenApi()/MapOpenApi() is the built-in OpenAPI support in .NET 9+; no Swashbuckle package needed.
  • The exception handler is registered as a service (IExceptionHandler) and UseExceptionHandler() sits at the top of the pipeline, so it catches failures from everything after it.
  • Each layer contributes exactly one AddX() extension method. Program.cs stays readable at a glance.

The Architecture Tests Project

The template isn't complete without tests that keep it honest. The MyApp.ArchitectureTests project enforces the dependency rule in CI:

[Fact]
public void Domain_Should_Not_Reference_Other_Projects()
{
    var result = Types
        .InAssembly(typeof(Entity).Assembly)
        .ShouldNot()
        .HaveDependencyOnAny("MyApp.Application", "MyApp.Infrastructure",
            "MyApp.Persistence", "MyApp.Api")
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}

Without this, the template's project references are just a suggestion. I shared my five go-to rules in 5 architecture tests you should add to your .NET projects.

Should You Use an Existing Template?

There are excellent public templates (Jason Taylor's Clean Architecture template and Ardalis' Clean Architecture solution are the best known). They're great for learning the patterns.

But I'd still encourage you to build your own, for two reasons:

  1. Public templates encode someone else's defaults. Identity setup, mapping libraries, front-end scaffolding - you'll spend the first day deleting things.
  2. The best template is a project you already shipped. Strip out the business logic, keep the skeleton, and you have a starting point that matches how your team actually works.

My own version of this setup is the foundation of Pragmatic Clean Architecture, where I walk through every one of these decisions in depth.

Creating the Template

To turn the solution into a dotnet new template, create a .template.config/template.json file in the solution root:

{
  "$schema": "http://json.schemastore.org/template",
  "author": "Your Name",
  "classifications": ["Web", "Clean Architecture"],
  "identity": "CleanArchitecture.Template",
  "name": "Clean Architecture Solution",
  "shortName": "cleanarch",
  "sourceName": "MyApp",
  "tags": {
    "language": "C#",
    "type": "solution"
  }
}

The sourceName means every occurrence of "MyApp" in filenames and file content gets replaced with whatever name you pass via -n.

Then install the template and create new solutions from it:

dotnet new install ./path/to/template
dotnet new cleanarch -n MyApp

Summary

A Clean Architecture solution template for .NET needs:

  1. Five projects - Domain, Application, Infrastructure, Persistence, Api
  2. Strict dependency rule - references only flow inward
  3. CQRS abstractions - ICommand, IQuery, handlers, pipeline behaviors
  4. DI extension methods - one AddX() per project
  5. Base primitives - Entity, AggregateRoot, IDomainEvent, Result
  6. Architecture tests - the dependency rule enforced in CI, not just in a diagram

Set it up once and reuse it across every project.

Thanks for reading, and stay awesome!


Frequently Asked Questions

How many projects should a Clean Architecture solution have?

Four is the common baseline: Domain, Application, Infrastructure, and a Presentation project like an API. Some teams split persistence out of Infrastructure into a fifth project. More than that is usually over-engineering for a single deployable.

Which project should reference which in Clean Architecture?

Domain references nothing. Application references only Domain. Infrastructure and Persistence reference Application. The API project references Application and Infrastructure so it can wire up dependency injection. References only flow inward.

Should the Domain project reference MediatR?

Ideally no. Keep IDomainEvent as a plain marker interface so the Domain project has zero package dependencies. If you want MediatR to dispatch domain events, adapt them in the Application or Infrastructure layer, or accept the pragmatic tradeoff of referencing the MediatR contracts package.

Should I use an existing Clean Architecture template or build my own?

Start from an existing template to learn the patterns, but expect to trim it. Public templates ship with choices you may not want. Building your own dotnet new template from a project you have shipped gives you a starting point that matches how your team actually works.

How do I turn my solution into a dotnet new template?

Add a .template.config/template.json file with a sourceName property, then run dotnet new install on the folder. The sourceName token gets replaced in file names and content when someone runs dotnet new with the -n option.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.