Tutorials ASP.NET Core MVC Mastery
Clean Architecture
On this page
Clean Architecture in ASP.NET Core β Enterprise Structure
When applications grow, traditional 3-tier architectures (UI β BLL β DAL) often collapse into spaghetti code where the business rules become hopelessly tangled with UI frameworks and database technologies. Clean Architecture (aka Onion or Hexagonal Architecture) flips dependencies inward, ensuring your core business logic knows absolutely nothing about databases, frameworks, or APIs.
1. The Dependency Rule
The single most important principle of Clean Architecture is The Dependency Rule: Dependencies can only flow inward. Inner layers must have zero knowledge of outer layers. You achieve this by heavily utilizing Interfaces and Dependency Injection.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Presentation (ASP.NET Core MVC) β <-- Outermost layer
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Infrastructure β β
β β ββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Application β β β
β β β ββββββββββββββββββββββββββββββββββββββ β β β
β β β β Domain (Core) β β β β
β β β ββββββββββββββββββββββββββββββββββββββ β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Breaking Down the Layers
Layer 1: Domain (The Core)
This is the center. It has zero dependencies on any other layer or framework (no EF Core, no ASP.NET, no NuGets if possible). It contains absolute business truth.
- Entities: Plain C# classes with business behavior (e.g.,
Order,Customer). - Value Objects: Immutable concepts (e.g.,
Money,Address). - Enums & Exceptions: Domain-specific logic.
- Repository Interfaces: It defines
IProductRepository, but does not implement it.
Layer 2: Application
This layer implements "Use Cases". It orchestrates business workflows using the Domain entities. It depends only on the Domain layer.
- Services/Interactors: e.g.,
OrderProcessingService. - CQRS Handlers: MediatR Commands and Queries.
- DTOs: Data Transfer Objects mapping to Domain entities.
- Validation: FluentValidation rules for incoming DTOs.
- Interfaces for Infrastructure: It defines
IEmailService, but doesn't write the SMTP code.
Layer 3: Infrastructure
This is where the actual "dirty work" happens. It depends on the Application and Domain layers. This is where frameworks live.
- Data Access: Entity Framework Core,
DbContext, Migrations. - Repository Implementations: The actual SQL/EF code implementing
IProductRepository. - External Services: Implementations of
IEmailService(SendGrid API),IPaymentGateway(Stripe API). - File System/Logging: Writing to disk, Serilog integrations.
Layer 4: Presentation (Web / API)
The entry point. It depends on Application and Infrastructure (for DI registration). It translates HTTP requests into Application use cases.
- Controllers / Endpoints: Parsing REST/HTTP data.
- Views: Razor pages or frontend framework logic.
- Configuration:
Program.cs, Dependency Injection setup, Middleware.
3. REAL-WORLD PRODUCTION EXAMPLE
1. The Domain Interface (Project: MyApp.Domain)
public interface IOrderRepository
{
// The core defines WHAT it needs, not HOW it happens
Task<Order> GetByIdAsync(int id);
Task SaveAsync(Order order);
}
2. The Application Use Case (Project: MyApp.Application)
public class CheckoutService
{
private readonly IOrderRepository _orderRepo;
private readonly IPaymentGateway _paymentGateway; // Interface defined in Application
// Uses Dependency Injection to heavily decouple code
public CheckoutService(IOrderRepository orderRepo, IPaymentGateway paymentGateway)
{
_orderRepo = orderRepo;
_paymentGateway = paymentGateway;
}
public async Task ProcessOrderAsync(int orderId)
{
// 1. Retrieves Domain Entity utilizing the abstracted interface
var order = await _orderRepo.GetByIdAsync(orderId);
// 2. Execute core domain logic
if (!order.CanBeProcessed()) throw new DomainException("Order is invalid.");
// 3. Utilize abstracted external infrastructure
await _paymentGateway.ChargeAsync(order.TotalAmount);
// 4. Save state
order.MarkAsPaid();
await _orderRepo.SaveAsync(order);
}
}
3. The Infrastructure Implementation (Project: MyApp.Infrastructure)
// The ONLY project that references Microsoft.EntityFrameworkCore
public class SqlOrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public SqlOrderRepository(ApplicationDbContext context) => _context = context;
public async Task<Order> GetByIdAsync(int id)
=> await _context.Orders.FindAsync(id);
public async Task SaveAsync(Order order)
{
_context.Update(order);
await _context.SaveChangesAsync();
}
}
// Payment implementation utilizing an external NuGet package (e.g. Stripe)
public class StripePaymentGateway : IPaymentGateway
{
public async Task ChargeAsync(decimal amount) { /* ... */ }
}
4. Presentation Bootstrapping (Project: MyApp.Web)
// Program.cs orchestrates the Dependency Injection wire-up
// This is where we bind the Interfaces to their Infrastructure implementations
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<CheckoutService>();
4. The Power of Swappability
Why do we do this? Suppose 2 years from now, SQL Server is too expensive, and the CTO demands a switch to MongoDB. Because IOrderRepository shields your Application and Domain logic, you do not change a single line of business logic. You simply create a MongoOrderRepository in the Infrastructure, update one line in Program.cs, and the entire app seamlessly transitions databases.
5. Interview Mastery
Q: "In a standard N-Tier architecture, the UI depends on the BLL (Business Logic Layer), and the BLL depends on the DAL (Data Access Layer). How is Clean Architecture different?"
Architect Answer: "Traditional N-Tier has a fatal flaw: the Business Logic Layer physically depends on the Data Access Layer (e.g. Entity Framework). This means database technicalities inevitably leak into business rules, making the business layer untestable without a database connection. Clean Architecture uses the Dependency Inversion Principle. The Domain/Application layers don't reference the Infrastructure; instead, they declare Interfaces. The Infrastructure layer references the Application layer to *implement* those interfaces. By pointing the dependency arrows inward, the core business logic remains pristine, framework-agnostic, and 100% unit-testable in isolation using mock infrastructures."