Back to Blog

Clean Architecture in .NET

Understanding and implementing Clean Architecture principles for maintainable and scalable applications

Introduction to Clean Architecture

Clean Architecture is a software design philosophy that separates the elements of a design into ring levels. The main goal is to create systems that are testable, maintainable, and independent of frameworks, databases, and external interfaces. This approach was popularized by Robert C. Martin (Uncle Bob) and has become a cornerstone of modern software development.

In the context of .NET applications, Clean Architecture provides a robust foundation for building scalable and maintainable systems. It promotes separation of concerns, dependency inversion, and testability, making your codebase more resilient to change and easier to understand.

Why Clean Architecture?

Clean Architecture helps you build applications that are independent of frameworks, testable, independent of UI, independent of databases, and independent of any external agency. This leads to more maintainable and flexible codebases.

The Four Layers of Clean Architecture

Presentation Layer (UI)

Contains controllers, views, and other UI components. This layer handles user interaction and presents data to the user. It depends on the Application layer.

Application Layer (Use Cases)

Contains business logic and use cases. It orchestrates the flow of data to and from the entities and directs those entities to use their business rules to achieve the goals of the use case.

Domain Layer (Business Logic)

Contains entities, value objects, and domain services. This is the heart of the application and contains the business rules that are independent of any external concerns.

Infrastructure Layer (Data Access)

Contains implementations for external concerns like databases, web services, file systems, etc. This layer implements interfaces defined in the inner layers.

Implementing Clean Architecture in .NET

1. Project Structure

Start by organizing your solution into separate projects that correspond to the architectural layers:


MyApplication.Solution/
├── src/
│ ├── MyApplication.Domain/ // Entities, Value Objects
│ ├── MyApplication.Application/ // Use Cases, Interfaces
│ ├── MyApplication.Infrastructure/ // Data Access, External Services
│ └── MyApplication.Web/ // Controllers, Views
└── tests/
    ├── MyApplication.Domain.Tests/
    ├── MyApplication.Application.Tests/
    └── MyApplication.Integration.Tests/

2. Domain Layer Implementation

The Domain layer contains your business entities and core business logic. Here's an example of a domain entity:


// Domain/Entities/Customer.cs
public class Customer : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public Email Email { get; private set; }
    public DateTime CreatedAt { get; private set; }

    private Customer() { } // For EF Core

    public Customer(string firstName, string lastName, Email email)
    {
        FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
        LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
        Email = email ?? throw new ArgumentNullException(nameof(email));
        CreatedAt = DateTime.UtcNow;
    }

    public void UpdateName(string firstName, string lastName)
    {
        FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
        LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
    }
}

3. Application Layer Implementation

The Application layer contains use cases and interfaces. It defines what the system can do:


// Application/Interfaces/ICustomerRepository.cs
public interface ICustomerRepository
{
    Task<Customer> GetByIdAsync(int id);
    Task<IEnumerable<Customer>> GetAllAsync();
    Task AddAsync(Customer customer);
    Task UpdateAsync(Customer customer);
    Task DeleteAsync(int id);
}

// Application/UseCases/CreateCustomerUseCase.cs
public class CreateCustomerUseCase
{
    private readonly ICustomerRepository _repository;

    public CreateCustomerUseCase(ICustomerRepository repository)
    {
        _repository = repository;
    }

    public async Task<CustomerDto> ExecuteAsync(CreateCustomerRequest request)
    {
        var email = new Email(request.Email);
        var customer = new Customer(request.FirstName, request.LastName, email);

        await _repository.AddAsync(customer);

        return new CustomerDto(customer);
    }
}

Dependency Injection and IoC Container

Clean Architecture relies heavily on dependency inversion. In .NET, we use the built-in IoC container to manage dependencies:


// Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // Application Services
    services.AddScoped<CreateCustomerUseCase>();
    services.AddScoped<GetCustomerUseCase>();

    // Infrastructure Services
    services.AddScoped<ICustomerRepository, CustomerRepository>();

    // Database Context
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(connectionString));
}

Testing in Clean Architecture

Unit Testing Domain Logic

Since the domain layer has no dependencies, it's easy to unit test:


[Test]
public void Customer_Creation_Should_Set_Properties_Correctly()
{
    // Arrange
    var firstName = "John";
    var lastName = "Doe";
    var email = new Email("john.doe@example.com");

    // Act
    var customer = new Customer(firstName, lastName, email);

    // Assert
    Assert.AreEqual(firstName, customer.FirstName);
    Assert.AreEqual(lastName, customer.LastName);
    Assert.AreEqual(email, customer.Email);
    Assert.IsTrue(customer.CreatedAt <= DateTime.UtcNow);
}

Testing Use Cases

Use cases can be tested by mocking their dependencies:


[Test]
public async Task CreateCustomer_Should_Call_Repository_Add()
{
    // Arrange
    var mockRepository = new Mock<ICustomerRepository>();
    var useCase = new CreateCustomerUseCase(mockRepository.Object);
    var request = new CreateCustomerRequest
    {
        FirstName = "John",
        LastName = "Doe",
        Email = "john.doe@example.com"
    };

    // Act
    await useCase.ExecuteAsync(request);

    // Assert
    mockRepository.Verify(r => r.AddAsync(It.IsAny<Customer>()), Times.Once);
}

Benefits and Challenges

Benefits

  • Testability: Each layer can be tested in isolation
  • Maintainability: Clear separation of concerns makes code easier to maintain
  • Flexibility: Easy to swap out infrastructure components
  • Framework Independence: Business logic is not tied to any specific framework
  • Database Independence: Can easily switch between different databases

Challenges

  • Initial Complexity: More setup required compared to simpler architectures
  • Over-engineering: May be overkill for simple applications
  • Learning Curve: Requires understanding of SOLID principles and design patterns
  • More Files: Results in more projects and files to manage

Conclusion

Clean Architecture provides a robust foundation for building scalable and maintainable .NET applications. While it requires more initial setup and understanding of design principles, the long-term benefits in terms of testability, maintainability, and flexibility make it worthwhile for complex applications.

The key to successfully implementing Clean Architecture is to start with a clear understanding of your domain and business requirements, then structure your code to reflect these boundaries. Remember that architecture should serve your business needs, not the other way around.

As you implement Clean Architecture in your .NET projects, focus on the principles rather than rigid adherence to a specific structure. The goal is to create code that is easy to understand, test, and modify as your application evolves.