How Not to Drown in Spaghetti: Hexagonal Architecture in C#

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

When Your Project Turns into a Mess

Have you ever opened a C# project written a year ago and realized with horror that nothing can be changed without risking breaking everything? Business logic is tightly coupled with Entity Framework, ASP.NET controllers hold database secrets, and unit tests turn into a nightmare of integration with a real database. Sound familiar?

This is a classic symptom of lacking architecture. The good news: there's a proven recipe — Hexagonal Architecture (also known as Ports & Adapters). This pattern, invented by Alistair Cockburn, is not just a trendy term. It's a lifesaver for medium and large C# projects. Let's break down how it works and write some code right away.



The Essence of Hexagonal Architecture: Core and Periphery

Imagine a hexagon. In the center is your application with pure business logic, written in plain C# without external dependencies. On the edges are ports (interfaces) through which the core communicates with the outside world. Adapters are the concrete implementations: ASP.NET controllers, EF Core repositories, RabbitMQ clients.

The main rule: the core knows nothing about the outside world. It only knows about its ports. This allows you to change the database, UI, or message queues without modifying the business logic.



Why Does a C# Developer Need This?

  • Testability: Business logic can be tested without a database, HTTP, or file system.
  • Flexibility: Want to replace PostgreSQL with MongoDB? Just change the adapter.
  • Readability: The code starts to look like a requirements specification rather than a tangle of dependencies.
  • SOLID Compliance: Especially the Dependency Inversion Principle.


Project Structure in C#

In Hexagonal Architecture, three main layers are typically distinguished. For C#, it's convenient to organize them as separate projects within one solution:



MyApp.sln├── MyApp.Domain          // Core: entities, value objects, ports (interfaces)├── MyApp.Application     // Use cases (application services)├── MyApp.Infrastructure  // Adapters: EF Core, external APIs, logging└── MyApp.Api             // Input adapter: ASP.NET Controllers / Minimal API


Note: Domain and Application are pure class libraries without references to Microsoft.AspNetCore or EntityFramework. Only System and System.Threading.Tasks.



Writing Code: A Real Example

Let's say we're building an order system. We need to create an order and save it.



Step 1. Core (Domain)

This is where entities and ports live — interfaces that define how the core communicates with the world.



// MyApp.Domain/Entities/Order.cspublic class Order{    public Guid Id { get; private set; }    public string CustomerName { get; private set; }    public decimal TotalAmount { get; private set; }    public DateTime CreatedAt { get; private set; }

public Order(string customerName, decimal totalAmount) { Id = Guid.NewGuid(); CustomerName = customerName; TotalAmount = totalAmount; CreatedAt = DateTime.UtcNow; }}

// MyApp.Domain/Ports/IOrderRepository.cs (Output Port)public interface IOrderRepository{ Task<Order> GetByIdAsync(Guid id); Task AddAsync(Order order);}

// MyApp.Domain/Ports/IOrderNotifier.cs (Output Port — notifications)public interface IOrderNotifier{ Task NotifyOrderCreatedAsync(Order order);}


Notice: IOrderRepository and IOrderNotifier are ports. The core defines what it wants, but not how it's implemented.



Step 2. Use Cases

Blogs

Book Recommendations