Skip to main content
Software Development

Structuring .NET Projects with Clean Architecture

Mart 06, 2026 7 dk okuma 51 views Raw
Ayrıca mevcut: tr
Structuring .NET projects with Clean Architecture
İçindekiler

What Is Clean Architecture?

Clean Architecture is an architectural approach introduced by Robert C. Martin (Uncle Bob) that aims to make software projects sustainable, testable, and independently developable. This approach isolates the application's business logic from external dependencies, maximizing the system's flexibility and maintainability.

The fundamental principle of Clean Architecture is the Dependency Rule: source code dependencies must always point inward. Outer layers may depend on inner layers, but inner layers must never know about outer layers. This ensures that business rules remain completely independent of the user interface, database, and other external systems.

Why Should We Use Clean Architecture?

Today's software projects are becoming increasingly complex. As monolithic structures grow, maintenance becomes difficult, testing becomes challenging, and adding new features becomes risky. Clean Architecture provides a systematic solution to these problems.

  • Independent testability: Business rules can be tested without a database or user interface.
  • Framework independence: The application is not tightly coupled to any specific framework. The framework is used as a tool, not as the master of the system.
  • Database independence: Switching from SQL Server to PostgreSQL or MongoDB does not affect business rules.
  • UI independence: The user interface can be changed easily without modifying business logic.
  • Sustainability: As the project grows, code readability and maintainability are preserved.

Clean Architecture Layers

Clean Architecture consists of layers represented as concentric circles. Each layer has a specific responsibility and is designed in accordance with the dependency rule.

Domain Layer (Entities)

The innermost layer, Domain, houses the application's business rules and entities. This layer has no external dependencies and consists entirely of pure C# classes. Components found in the Domain layer include:

  • Entities: Classes representing business objects such as Order, Customer, and Product.
  • Value Objects: Objects defined by their values without identity, such as Money, Address, and Email.
  • Domain Events: Events that occur during business processes.
  • Enumerations: Fixed value sets specific to business logic.
  • Exceptions: Domain-specific error classes.

Application Layer (Use Cases)

The Application layer contains the application's use cases. This layer depends on the Domain layer but is independent of outer layers. Business workflow orchestration takes place here.

  • Interfaces: Abstractions for repositories, services, and other external dependencies.
  • DTOs: Data transfer objects for communication between layers.
  • Validators: Input validation rules.
  • Behaviors: Pipeline behaviors for cross-cutting concerns.
  • Commands and Queries: Command and query objects when applying the CQRS pattern.

Infrastructure Layer

The Infrastructure layer handles interaction with the outside world. All external dependencies such as database access, file system operations, email delivery, and third-party services reside in this layer.

  • Data Access: Entity Framework Core DbContext and repository implementations.
  • External Services: API clients and messaging services.
  • Identity: Authentication and authorization implementations.
  • File Storage: File upload and storage services.

Presentation Layer (Web API / UI)

The outermost layer provides user interaction. ASP.NET Core Web API, MVC, Blazor, or any other presentation technology belongs in this layer. It processes user requests by calling services and commands from the Application layer.

Step-by-Step .NET Project Structuring

Now let us structure a .NET solution according to Clean Architecture principles step by step. This structure is based on proven best practices from real-world projects.

Solution Structure

First, let us create the solution structure. Each layer is defined as a separate project, and dependencies are configured according to the layer rules.

MyApp/
├── src/
│   ├── MyApp.Domain/
│   ├── MyApp.Application/
│   ├── MyApp.Infrastructure/
│   └── MyApp.WebApi/
└── tests/
    ├── MyApp.Domain.Tests/
    ├── MyApp.Application.Tests/
    └── MyApp.Infrastructure.Tests/

Project Dependency Hierarchy

In accordance with the dependency rule, inter-project references should be configured as follows:

  1. MyApp.Domain: References no other project. It is completely independent.
  2. MyApp.Application: References only MyApp.Domain.
  3. MyApp.Infrastructure: References MyApp.Application and indirectly accesses Domain.
  4. MyApp.WebApi: References MyApp.Application and MyApp.Infrastructure.

Building the Domain Layer

Define your entities and business rules in the Domain layer. Avoid using NuGet packages in this layer as much as possible. The goal is to keep this layer purely independent.

public abstract class BaseEntity
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}

public class Order : BaseEntity
{
    public string OrderNumber { get; private set; }
    public decimal TotalAmount { get; private set; }
    public OrderStatus Status { get; private set; }
    
    public void Complete()
    {
        if (Status != OrderStatus.Processing)
            throw new DomainException("Only processing orders can be completed.");
        Status = OrderStatus.Completed;
    }
}

Configuring the Application Layer

In the Application layer, you can implement the CQRS pattern using the MediatR library. This approach clearly separates commands and queries, improving code readability.

public interface IOrderRepository
{
    Task<Order> GetByIdAsync(Guid id);
    Task<IEnumerable<Order>> GetAllAsync();
    Task AddAsync(Order order);
    Task UpdateAsync(Order order);
}

public record CreateOrderCommand(
    string CustomerName,
    List<OrderItemDto> Items
) : IRequest<Guid>;

public class CreateOrderCommandHandler 
    : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IOrderRepository _orderRepository;
    
    public CreateOrderCommandHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }
    
    public async Task<Guid> Handle(
        CreateOrderCommand request, 
        CancellationToken cancellationToken)
    {
        var order = new Order(request.CustomerName, request.Items);
        await _orderRepository.AddAsync(order);
        return order.Id;
    }
}

Applying SOLID Principles

Clean Architecture naturally aligns with the SOLID principles. Let us examine each principle's role within the architecture.

Single Responsibility Principle

Each class and module should have only one responsibility. In Clean Architecture, this principle is naturally enforced through the separation of layers. The Domain layer concerns itself solely with business rules, while the Application layer handles only use cases.

Open/Closed Principle

Software entities should be open for extension but closed for modification. Adding a new use case means creating new handler classes rather than modifying existing code. The MediatR pipeline supports this principle excellently.

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions. In Clean Architecture, the Application layer defines interfaces while the Infrastructure layer implements them. This keeps business logic completely independent of database technology.

Dependency Injection and Registration

ASP.NET Core's built-in Dependency Injection mechanism works seamlessly with Clean Architecture. Each layer should provide an extension method to register its own services.

// Application layer registration
public static class DependencyInjection
{
    public static IServiceCollection AddApplication(
        this IServiceCollection services)
    {
        services.AddMediatR(cfg => 
            cfg.RegisterServicesFromAssembly(
                Assembly.GetExecutingAssembly()));
        services.AddValidatorsFromAssembly(
            Assembly.GetExecutingAssembly());
        return services;
    }
}

// Infrastructure layer registration
public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services, 
        IConfiguration configuration)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                configuration.GetConnectionString("Default")));
        services.AddScoped<IOrderRepository, OrderRepository>();
        return services;
    }
}

Common Mistakes to Avoid

There are critical points to consider when implementing Clean Architecture. Knowing these mistakes helps keep your project healthy.

  • Framework dependencies in the Domain layer: Entity Framework attributes or ASP.NET Core dependencies should never be added to the Domain layer.
  • Over-abstraction: Abstracting everything creates unnecessary complexity. Only abstract components that are likely to change.
  • Anemic Domain Model: Entities should not be mere data carriers. Define business rules within your entities.
  • Layer skipping: The Presentation layer should never access the Infrastructure layer directly. Every request must pass through the Application layer.
  • Unnecessary layers: Applying all layers for small projects can be over-engineering. Decide based on project size and requirements.

Testing Strategy

Clean Architecture makes it straightforward to implement comprehensive testing strategies. Different test types can be applied for each layer.

Unit Tests

Business logic in the Domain and Application layers can be tested without any external dependencies. Use mock objects to simulate repository and service behaviors.

Integration Tests

Database access and external service integrations in the Infrastructure layer should be verified with integration tests. Libraries such as TestContainers make it easy to test against real database environments.

Conclusion

Clean Architecture provides a powerful framework for building sustainable, testable, and flexible structures in .NET projects. By correctly applying the Dependency Rule, you can keep code complexity under control as your project grows. When combined with SOLID principles, Clean Architecture increases your team's productivity and minimizes technical debt. However, remember that architectural decisions should always be evaluated based on the project's size and requirements.

Bu yazıyı paylaş