| Onion Architecture in .NET :👈 | 👉:Micro-Frontends |
Hexagonal Architecture |
π· Hexagonal Architecture Overview
Hexagonal Architecture, coined by Alistair Cockburn, is a software design pattern that separates the core business logic from external systems like databases, UIs, or APIs. It promotes flexibility, testability, and adaptability by using ports and adapters.
π Key Concepts
- Core Logic: The heart of the application containing business rules
- Ports: Interfaces that define how external systems interact with the core
- Adapters: Implementations of ports that connect external systems
βοΈ The Key Components of Hexagonal Architecture
- ποΈ Core: The central part of the application, often a class library project, that contains all the business logic and domain entities. It is technology-agnostic and has no knowledge of the external systems it interacts with.
-
π Ports: Interfaces or contracts defined within the core that represent the entry and exit points for data and functionality. There are two types:
- π― Driving/Primary Ports (inbound): APIs that the core exposes for external actors (users, other applications) to drive it.
- β‘ Driven/Secondary Ports (outbound): Interfaces that the core uses to interact with external systems.
- π Adapters: Implementations of the ports that handle communication with specific external systems. They act as a bridge, converting data from the external system's format into a format the core understands, and vice versa.
π§ͺ Example: Banking Application
- Core: AccountService with methods like deposit, withdraw
- Inbound Adapter: REST API controller calling AccountService
- Outbound Adapter: Repository implementation using SQL Server
- Ports: IAccountRepository, INotificationService
π§ͺ Example in .NET Core: A Simple Product Service
1οΈβ£ Project Structure
The solution is typically split into multiple projects to enforce dependency rules.
- ProductService.Core (Class Library): Contains the domain entities, application services, and port interfaces.
- ProductService.Infrastructure (Class Library): Contains adapters for specific external systems (e.g., a database).
- ProductService.Api (Web API): The entry point that hosts the driving adapters (e.g., controllers).
2οΈβ£ Core Layer (ProductService.Core)
This project contains the Product entity, the IProductRepository port, and a CreateProductUseCase application service.
// ProductService.Core/Entities/Product.cs
public class Product
{
public Guid Id { get; init; } = Guid.NewGuid();
public string Name { get; private set; }
public decimal Price { get; private set; }
public Product(string name, decimal price)
{
// Business rules and validation
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Product name cannot be empty.", nameof(name));
if (price <= 0)
throw new ArgumentException("Price must be greater than zero.", nameof(price));
Name = name;
Price = price;
}
}
// ProductService.Core/Ports/IProductRepository.cs
// This is a driven (outbound) port.
public interface IProductRepository
{
Task AddAsync(Product product);
Task<Product?> GetByIdAsync(Guid id);
}
// ProductService.Core/Application/CreateProductUseCase.cs
public class CreateProductUseCase
{
private readonly IProductRepository _productRepository;
public CreateProductUseCase(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task Execute(string name, decimal price)
{
var product = new Product(name, price);
await _productRepository.AddAsync(product);
}
}
3οΈβ£ Infrastructure Layer (ProductService.Infrastructure)
This project, which references the ProductService.Core project, provides the concrete implementation of the IProductRepository port.
// ProductService.Infrastructure/Adapters/Persistence/ProductRepository.cs
public class ProductRepository : IProductRepository
{
// In a real application, this would interact with a database
private readonly Dictionary<Guid, Product> _products = new();
public Task AddAsync(Product product)
{
_products[product.Id] = product;
return Task.CompletedTask;
}
public Task<Product?> GetByIdAsync(Guid id)
{
_products.TryGetValue(id, out var product);
return Task.FromResult(product);
}
}
4οΈβ£ API Layer (ProductService.Api)
This project, referencing both the Core and Infrastructure layers, serves as the entry point and wires everything together.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register Core services
builder.Services.AddScoped<CreateProductUseCase>();
// Register Adapters
builder.Services.AddScoped<IProductRepository, ProductRepository>();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapPost("/products", async (string name, decimal price, CreateProductUseCase useCase) =>
{
await useCase.Execute(name, price);
return Results.Ok();
});
app.Run();
β Advantages
- π§ͺ High testability: The core business logic can be tested in isolation from external dependencies using mock or fake adapters.
- π Decoupling: External dependencies can be replaced without affecting core logic.
- π Flexibility and adaptability: Adding new interfaces (like REST or gRPC) requires only a new adapter.
- π§ Technology independence: The core is free of framework-specific code.
- β³ Deferred decisions: Core business logic can be developed before choosing external technologies.
- Facilitates easier replacement of external systems
β οΈ Disadvantages
- π Increased complexity: May be overkill for small applications.
- π Steeper learning curve: Requires understanding ports, adapters, and dependency inversion.
- βοΈ Performance overhead: Additional abstraction layers may slightly affect performance.
- π Initial effort: Setup and structure increase early development time.
- Requires discipline to maintain boundaries
π When to Use
- π’ Complex, long-lived applications.
- π§ͺ High testability requirements.
- π Multiple interfaces or integrations.
- π Preparing for technology changes.
- Applications with multiple input/output channels
- Systems requiring high test coverage
- Projects expected to evolve or scale
π« When Not to Use
- π§© Simple or short-lived applications.
- β° Tight deadlines and limited resources.
- πΆ Inexperienced teams.
- ποΈ CRUD-heavy or I/O-driven systems.
π§ Precautions
- π« Avoid "leaky" abstractions β keep the core independent.
- π Enforce dependency rules β outer layers can reference inner layers, not vice versa.
- π Manage data mapping carefully β consider tools like AutoMapper.
- βοΈ Guard against over-abstraction β only create ports where needed.
ποΈ Best Practices and Tips
- π Use dependency injection (DI) with .NET Coreβs built-in container.
- ποΈ Embrace Domain-Driven Design (DDD).
- π§± Prefer constructor injection for clarity.
- π§© Keep adapters focused and thin.
- π Use templates for consistency.
- Define clear interfaces (ports) for all external interactions
- Document adapter responsibilities clearly
- Continuously refactor to maintain boundaries
π‘ Tips
- Start with core logic and build outward
- Use hexagonal diagrams to visualize dependencies
- Refactor legacy systems incrementally into hexagonal structure
Sources: GeeksforGeeks, Java Code Geeks, Everestek Blog
| Onion Architecture in .NET :👈 | 👉:Micro-Frontends |
Comments and Discussion