Chat mode imported from mjhoffmeister/asaph-ai (
.github/chatmodes/dotnet-cleanarchitecture.chatmode.md). Copyright stays with the author.
Clean Architecture for .NET
Core Principles
- Dependency Inversion: Dependencies point inward toward the Domain layer
- Independence: Business logic is independent of frameworks, UI, and external concerns
- Testability: Business logic can be tested without UI, database, or external services
- Layer Separation: Clear boundaries between Domain, Application, Infrastructure, and Presentation layers
Dependency Rules Matrix
✅ Infrastructure → Core
✅ Presentation → Core + Bootstrapper
✅ Tests → Core
✅ Bootstrapper → Core + Infrastructure
❌ Core → Infrastructure
❌ Core → Presentation
❌ Core → Bootstrapper
Project Structure Template
{AppName}/
├── {AppName}.sln
├── {AppName}.Bootstrapper/ # Dependency injection configuration
│ └── {AppName}.Bootstrapper.csproj # References: Core + Infrastructure
├── {AppName}.Core/ # Domain + Application layers
│ ├── {AppName}.Core.csproj
│ ├── Domain/ # Entities, Value Objects, Domain Services
│ │ └── {AggregateRoot}/ # One folder per aggregate root
│ ├── ExternalInterfaces/ # Repository and service contracts
│ └── UseCases/ # Application use cases
│ └── IUseCaseInteractor.cs
├── {AppName}.Core.UnitTests/ # Domain and Application tests
│ ├── {AppName}.Core.UnitTests.csproj # References: Core only
│ ├── Domain/ # Test entities and value objects
│ └── UseCases/ # Test application use cases
├── {AppName}.Infrastructure/ # Main infrastructure implementations
│ └── {AppName}.Infrastructure.csproj # References: Core only
├── {AppName}.Infrastructure.EntityFramework/ # Specific data implementation (optional)
├── {AppName}.Infrastructure.OpenAI/ # Specific AI service implementation (optional)
├── {AppName}.Infrastructure.IntegrationTests/
│ └── {AppName}.Infrastructure.IntegrationTests.csproj
└── {AppName}.WebApi/ # Presentation layer (example)
└── {AppName}.WebApi.csproj # References: Core + Bootstrapper
Project Setup Commands
# Create solution
dotnet new sln -n {AppName}
# Create projects
dotnet new classlib -n {AppName}.Core
dotnet new classlib -n {AppName}.Infrastructure
dotnet new classlib -n {AppName}.Bootstrapper
dotnet new webapi -minimal -n {AppName}.WebApi
dotnet new xunit -n {AppName}.Core.UnitTests
dotnet new xunit -n {AppName}.Infrastructure.IntegrationTests
# Add to solution
dotnet sln add **/*.csproj
# Set up references
dotnet add {AppName}.Bootstrapper reference {AppName}.Core {AppName}.Infrastructure
dotnet add {AppName}.WebApi reference {AppName}.Core {AppName}.Bootstrapper
dotnet add {AppName}.Infrastructure reference {AppName}.Core
dotnet add {AppName}.Core.UnitTests reference {AppName}.Core
dotnet add {AppName}.Infrastructure.IntegrationTests reference {AppName}.Infrastructure
# Add required packages
dotnet add {AppName}.Core package FluentResults
dotnet add {AppName}.Core.UnitTests package FluentAssertions
dotnet add {AppName}.Core.UnitTests package Moq
Mandatory Cleanup: Delete Class1.cs from new class libraries and UnitTest1.cs from test projects.
Domain Layer
The domain layer is the center of a Clean Architecture application. It contains the business logic and domain entities that are independent of any external concerns.
Use DDD principles when creating domain-layer aggregates, entities, and value objects, and use the FluentResults NuGet package to implement TryCreate methods for them.
Entities (use classes)
/// <summary>
/// Example entity representing a domain object.
/// </summary>
public class SomeEntity
{
/// <summary>
/// Creates a new instance of the <see cref="SomeEntity"/> class.
/// </summary>
private SomeEntity(string value) => Value = value;
/// <summary>
/// Value.
/// </summary>
public string Value { get; private set; }
/// <summary>
/// Tries to create a new instance of the <see cref="SomeEntity"/> class.
/// </summary>
public static Result<SomeEntity> TryCreate(string someValue)
{
if (string.IsNullOrWhiteSpace(someValue))
return Result.Fail<SomeEntity>("Some value cannot be null or empty.");
return Result.Ok(new SomeEntity(someValue));
}
}
Value Objects (use records)
/// <summary>
/// Value object representing an email address.
/// </summary>
public record EmailAddress
{
/// <summary>
/// Creates a new instance of the <see cref="EmailAddress"/> record.
/// </summary>
private EmailAddress(string value) => Value = value;
/// <summary>
/// Gets the email address value.
/// </summary>
public string Value { get; init; }
/// <summary>
/// Tries to create a new instance of the <see cref="EmailAddress"/> record.
/// </summary>
public static Result<EmailAddress> TryCreate(string email)
{
if (string.IsNullOrWhiteSpace(email) || !IsValidEmail(email))
return Result.Fail<EmailAddress>("Invalid email format.");
return Result.Ok(new EmailAddress(email));
}
/// <summary>
/// Validates the email format.
/// </summary>
private static bool IsValidEmail(string email)
{
return email.Contains("@") && email.Contains(".");
}
}
Requirements:
- Prefer records for value objects and classes for entities
- Follow the concept of aggregate roots for related domain objects
- Aggregate roots and their children should be placed in their own folders
- MUST use FluentResults for TryCreate methods with Result return type
Application Layer
The application layer is responsible for orchestrating the flow of data between the presentation layer and the domain layer. It contains use case interactors that implement the application business logic. The application layer should be encapsulated in a "Core" project.
Use Case Interactor Interface
/// <summary>
/// Use case interactor interface.
/// </summary>
/// <typeparam name="TRequest">Request type.</typeparam>
/// <typeparam name="TOutput">Output type.</typeparam>
public interface IUseCaseInteractor<TRequest, TOutput>
{
/// <summary>
/// Handles the request.
/// </summary>
/// <param name="request">Request.</param>
/// <returns>Output.</returns>
Task<TOutput> HandleAsync(TRequest request);
}
Use Case Folder Structure
Each use case should be defined in its own folder under the UseCases folder, and each use case should have its own request and response records and a boundary interface. For example, if the use case is called "CreateUser", the folder structure would look like this:
UseCases/
└── CreateUser/
├── CreateUserRequest.cs
├── CreateUserResponse.cs
├── ICreateUserBoundary.cs
└── CreateUserInteractor.cs
Boundary Interface Pattern
Use case boundary interfaces should have a TOutput type parameter and contain methods for relevant use case events. For example, if the use case is called "CreateUser", the boundary interface would look like this:
/// <summary>
/// Boundary interface for the Create User use case.
/// </summary>
public interface ICreateUserBoundary<TOutput>
{
/// <summary>
/// Outputs a successful user creation operation.
/// </summary>
TOutput UserCreated(CreateUserResponse response, TOutput output);
/// <summary>
/// Outputs a user already exists operation.
/// </summary>
TOutput UserAlreadyExists(CreateUserResponse response, TOutput output);
/// <summary>
/// Outputs a validation failure operation.
/// </summary>
TOutput ValidationFailed(CreateUserResponse response, TOutput output);
}
Complete Use Case Example
/// <summary>
/// Use case interactor for the Create User use case.
/// </summary>
public class CreateUserInteractor : IUseCaseInteractor<CreateUserRequest, Task>
{
// User repository
private readonly IUserRepository _userRepository;
// Boundary
private readonly ICreateUserBoundary<Task> _boundary;
/// <summary>
/// Creates a new instance of the <see cref="CreateUserInteractor"/> class.
/// </summary>
public CreateUserInteractor(
IUserRepository userRepository, ICreateUserBoundary<Task> boundary)
{
_userRepository = userRepository;
_boundary = boundary;
}
/// <inheritdoc/>
public async Task<Task> HandleAsync(CreateUserRequest request)
{
// Try to create the email value object
var emailResult = EmailAddress.TryCreate(request.Email);
// If the creation failed, return validation error
if (emailResult.IsFailed)
{
return _boundary.ValidationFailed(
new CreateUserResponse(emailResult.Errors.First().Message),
Task.CompletedTask);
}
// Check if the user already exists
var existingUser = await _userRepository.GetByEmailAsync(
emailResult.Value);
// If the user already exists, return user already exists error
if (existingUser != null)
{
return _boundary.UserAlreadyExists(
new CreateUserResponse("User already exists"),
Task.CompletedTask);
}
// Try to create the user entity
var userResult = User.TryCreate(request.Name, emailResult.Value);
// If the creation failed, return validation error
if (userResult.IsFailed)
{
return _boundary.ValidationFailed(
new CreateUserResponse(userResult.Errors.First().Message),
Task.CompletedTask);
}
// Save the new user
await _userRepository.SaveAsync(userResult.Value);
// Return success
return _boundary.UserCreated(
new CreateUserResponse("User created successfully"),
Task.CompletedTask);
}
}
External Interfaces
External interfaces are defined in the ExternalInterfaces folder of the Core project. These interfaces should be implemented by the Infrastructure layer to provide access to external systems, such as databases, message queues, and web services.
Example repository interface:
/// <summary>
/// User repository interface.
/// </summary>
public interface IUserRepository
{
/// <summary>
/// Gets a user by email.
/// </summary>
Task<User?> GetByEmailAsync(EmailAddress email);
/// <summary>
/// Gets a user by ID.
/// </summary>
Task<User?> GetByIdAsync(UserId id);
/// <summary>
/// Saves a user.
/// </summary>
Task SaveAsync(User user);
/// <summary>
/// Deletes a user by ID.
/// </summary>
Task DeleteAsync(UserId id);
}
Infrastructure Layer
The infrastructure layer is responsible for providing technical capabilities to the application, such as data access, messaging, and external service integration. It contains implementations of the external interfaces defined in the Core project. It can be comprised of many projects depending on the specific technical requirements.
For example, if there's an external interface in the Core project, IChatCompletionService.cs, there could be a project named {AppName}.Infrastructure.SKChatCompletionService.csproj, which communicates that Semantic Kernel is used to implement the interface.
Repository Implementation Example
/// <summary>
/// User repository implementation using Entity Framework Core.
/// </summary>
public class UserRepository : IUserRepository
{
// Database context
private readonly DbContext _context;
/// <summary>
/// Creates a new instance of the <see cref="UserRepository"/> class.
/// </summary>
public UserRepository(DbContext context)
{
_context = context;
}
/// <inheritdoc/>
public async Task<User?> GetByEmailAsync(EmailAddress email)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Email.Value == email.Value);
}
/// <inheritdoc/>
public async Task SaveAsync(User user)
{
_context.Users.Add(user);
await _context.SaveChangesAsync();
}
}
Presentation Layer
The presentation layer is responsible for handling user interactions and displaying information to the user or downstream systems. It contains the user interface components and is typically implemented as a separate project, such as a Web API or console app.
Presenters should be placed in folders corresponding to Use Case for easy discovery.
For Web APIs, use ASP.NET Core minimal APIs.
Presenter Example
/// <summary>
/// API presenter for the Create User use case.
/// </summary>
public class CreateUserPresenter : ICreateUserBoundary<IResult>
{
/// <inheritdoc/>
public IResult UserCreated(CreateUserResponse response, IResult output)
{
return Results.Created("/users/{id}", new { Message = response.Message });
}
public IResult UserAlreadyExists(CreateUserResponse response, IResult output)
{
return Results.Conflict(new { Error = response.Message });
}
/// <inheritdoc/>
public IResult ValidationFailed(CreateUserResponse response, IResult output)
{
return Results.BadRequest(new { Error = response.Message });
}
}
Minimal API Example
// Map the Create User endpoint
app.MapPost(
"/users",
async (
CreateUserRequest request,
IUseCaseInteractor<CreateUserRequest, Task> interactor) =>
{
return await interactor.HandleAsync(request);
});
Bootstrapper
As the infrastructure layer shouldn't be directly referenced by the presentation layer, a bootstrapper project is created to wire up the dependencies between these layers. The bootstrapper project is responsible for configuring the application's services and dependencies at runtime.
Important: The Bootstrapper cannot know about presentation-layer output types (like IResult, IActionResult, etc.). There are several approaches to handle this:
Extension Methods
The Bootstrapper registers only Core and Infrastructure services. The presentation layer handles its own presenter registrations.
/// <summary>
/// Extension methods for IServiceCollection to register application services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds application services to the service collection.
/// </summary>
/// <param name="services"><see cref="IServiceCollection"/>.</param>
/// <returns>The service collection.</returns>
public static IServiceCollection AddMyAppServices(
this IServiceCollection services)
{
// Register domain services and repositories
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IEmailService, EmailService>();
// Register use case interactors (without output type binding)
services.AddScoped<CreateUserInteractor>();
services.AddScoped<GetUserInteractor>();
// Return the service collection
return services;
}
}
Then in the presentation layer (e.g. Program.cs for Web API):
var builder = WebApplication.CreateBuilder(args);
// Register core services
builder.Services.AddMyAppServices();
// Register presentation-specific services
builder.Services.AddScoped<
IUseCaseInteractor<CreateUserRequest, Task>, CreateUserInteractor>();
builder.Services.AddScoped<ICreateUserBoundary<IResult>, CreateUserPresenter>();
var app = builder.Build();
Testing
Use xUnit for tests and Moq for mocking external interfaces in use case unit tests. Ensure there's only one assertion per test.
Required Packages
- xUnit: Testing framework
- Moq: Mocking external interfaces
- FluentAssertions: Readable assertions
Test Structure Example
/// <summary>
/// Tests creating a valid email address.
/// </summary>
[Fact]
public void TryCreate_WithValidValue_ShouldReturnSuccess()
{
// Arrange
var validValue = "test@example.com";
// Act
var result = EmailAddress.TryCreate(validValue);
// Assert
result.IsSuccess.Should().BeTrue();
}
/// <summary>
/// Tests the Create User use case.
/// </summary>
[Fact]
public void HandleAsync_WithValidRequest_ShouldCallUserCreated()
{
// Arrange
var request = new CreateUserRequest("John", "john@example.com");
var mockRepository = new Mock<IUserRepository>();
var mockBoundary = new Mock<ICreateUserBoundary<Task>>();
var interactor = new CreateUserInteractor(
mockRepository.Object, mockBoundary.Object);
// Act
await interactor.HandleAsync(request);
// Assert
mockBoundary.Verify(b =>
b.UserCreated(
It.IsAny<CreateUserResponse>(), It.IsAny<Task>()), Times.Once);
}
After creating tests, ensure they pass before moving on.
Validation Checklist
Use this checklist to verify compliance with Clean Architecture principles:
- Dependencies: All dependencies point inward toward Core
- FluentResults: Used for all domain validation with Result return type
- Use Case Structure: Each use case has exactly 4 files (Request, Response, Boundary, Interactor)
- Project References: No Core project references to Infrastructure or Presentation
- Testing: All tests pass before proceeding to next step
- TDD: Tests written before implementation
- Domain Logic: No external dependencies in Domain layer
- Aggregate Structure: Related entities grouped under aggregate root folders
- Boundary Interfaces: Presenter interfaces properly implemented
- Bootstrapper: Dependency injection properly configured without presentation-layer types
Common Mistakes to Avoid
- ❌ Referencing Infrastructure from Core
- ❌ Putting business logic in presentation layer
- ❌ Skipping FluentResults for domain validation
- ❌ Multiple assertions in single test
- ❌ Direct database calls from use cases
- ❌ Missing boundary interfaces for use cases
- ❌ Forgetting to delete default Class1.cs and UnitTest1.cs files
- ❌ Registering presentation-layer types in Bootstrapper
Project structure
Use the following structure when asked to creating or modifying an app according to Clean Architecture principles, where {AppName} is the name of the app. Use the top-level app folder as your workspace.
Project Structure Reference
src/{AppName}/
├── {AppName}.sln
├── {AppName}.Bootstrapper/
│ └── {AppName}.Bootstrapper.csproj (reference {AppName}.Core and {AppName}.Infrastructure)
├── {AppName}.Core/
│ ├── {AppName}.Core.csproj
│ ├── Domain/ (folder where DDD aggregates, entities, and value objects are defined)
│ | └── {Name}AggregateRoot/ (folder where the aggregate root for the {Name} aggregate is defined)
│ ├── ExternalInterfaces/ (folder where external interfaces are defined)
│ └── UseCases/ (folder where application use cases are defined)
│ └── IUseCaseInteractor.cs
├── {AppName}.Core.UnitTests/
│ ├── {AppName}.Core.UnitTests.csproj (reference {AppName}.Core)
│ ├── Domain/ (folder where DDD entities and value objects are tested; use subfolders for each entity)
│ └── UseCases/ (folder where application use cases are tested)
├── {AppName}.Infrastructure.{ExternalInterfaceImpl}/
│ └── {AppName}.Infrastructure{ExternalInterfaceImpl}.csproj (reference {AppName}.Core)
├── {AppName}.Infrastructure.IntegrationTests/
│ └── {AppName}.Infrastructure.IntegrationTests.csproj
└── {AppName}.WebApi/ (for example, when the presentation layer is a Web API)
└── {AppName}.WebApi.csproj (reference {AppName}.Core and {AppName}.Bootstrapper)
Delete default Class1.cs and UnitTest1.cs files.
