DEV Community

Cover image for Hexagonal Architectural Pattern in C# – Full Guide 2024 👨🏻‍💻
ByteHide
ByteHide

Posted on • Edited on • Originally published at bytehide.com

Hexagonal Architectural Pattern in C# – Full Guide 2024 👨🏻‍💻

Introduction to Hexagonal Architecture

In this comprehensive guide, we’ll walk you through everything you need to know about this powerful architectural pattern. We will cover basic and core concepts, the benefits of using Hexagonal Architecture, practical examples, testing, and a final FAQs section if you have any further doubts!

By the end, you’ll not only understand Hexagonal Architecture but also be ready to implement it in your C# projects. Let’s dive in!

What is Hexagonal Architecture?

Hexagonal Architecture, also known as Ports and Adapters, is an architectural pattern that promotes the separation of concerns. It aims to make your application easier to maintain and more flexible by isolating the core logic from the external systems.

Benefits of Using Hexagonal Architecture

Why should you care about Hexagonal Architecture? Here are some compelling reasons:

  • Improved maintainability: With a clear separation between core logic and external systems, your code becomes easier to manage.
  • Increased testability: Isolated components make it easier to write unit tests.
  • Enhanced flexibility: Switching out external systems (e.g., databases) becomes a breeze.

Core Concepts of Hexagonal Architecture

In the next sections, we’ll break down the building blocks of Hexagonal Architecture. You’ll get a solid understanding of Ports and Adapters, Dependency Injection, and Separation of Concerns.

Ports and Adapters Explored

At the heart of Hexagonal Architecture are Ports and Adapters. But what exactly are they? Let’s break it down.

Ports are interfaces that define the operations your application can perform. Think of them as the “what” of your application.

Adapters are the implementations of these interfaces. They’re responsible for the “how” – how the operations defined by the ports are carried out.

Here’s a simple example in C#:

// Port: An interface defining a service
public interface IFileStorage
{
    void SaveFile(string fileName, byte[] data);
}

// Adapter: An implementation of the interface
public class LocalFileStorage : IFileStorage
{
    public void SaveFile(string fileName, byte[] data)
    {
        // Saving file locally
        System.IO.File.WriteAllBytes(fileName, data);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, IFileStorage is the port, and LocalFileStorage is the adapter.

The Role of Dependency Injection

Dependency Injection (DI) is a key player in Hexagonal Architecture. It allows us to easily swap out adapters without changing the core logic. Imagine it as a plug-and-play mechanism.

Here’s how you might set up DI in a C# project:

// Configure Dependency Injection in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // Register the IFileStorage interface with its implementation
    services.AddTransient<IFileStorage, LocalFileStorage>();
}
Enter fullscreen mode Exit fullscreen mode

With DI, you can switch from LocalFileStorage to, say, AzureFileStorage.

services.AddTransient<IFileStorage, AzureFileStorage>();
Enter fullscreen mode Exit fullscreen mode

Separation of Concerns

Hexagonal Architecture enforces the Separation of Concerns by decoupling the core logic from external systems. This not only makes your code cleaner but also more robust. Imagine separating the delicious filling of an Oreo without breaking it. With Hexagonal Architecture, this becomes possible.

Implementing Hexagonal Architecture in C

In this section, we’ll dive into setting up a C# project using Hexagonal Architecture.

Setting Up Your C# Project

Let’s start by setting up our C# project structure. You’ll generally have three main layers:

  • Core: Contains the core business logic and ports (interfaces).
  • Infrastructure: Houses the adapters (implementations of the ports).
  • UI: Handles the user interface and interacts with the core via the ports.

Your solution might look like this:

/Solution
  /Core
    /Interfaces
  /Infrastructure
  /UI
Enter fullscreen mode Exit fullscreen mode

Defining Interfaces (Ports)

Let’s define a few interfaces in the Core layer. These will act as our ports.

// IFileStorage.cs
public interface IFileStorage
{
    void SaveFile(string fileName, byte[] data);
}
Enter fullscreen mode Exit fullscreen mode
// IUserRepository.cs
public interface IUserRepository
{
    User GetUserById(int id);
    void SaveUser(User user);
}
Enter fullscreen mode Exit fullscreen mode

Implementing Adapters

Next, we create the adapter classes in the Infrastructure layer.

// LocalFileStorage.cs
public class LocalFileStorage : IFileStorage
{
    public void SaveFile(string fileName, byte[] data)
    {
        System.IO.File.WriteAllBytes(fileName, data);
    }
}
Enter fullscreen mode Exit fullscreen mode
// DatabaseUserRepository.cs
public class DatabaseUserRepository : IUserRepository
{
    private readonly List<User> _users = new List<User>();

    public User GetUserById(int id)
    {
        return _users.FirstOrDefault(u => u.Id == id);
    }

    public void SaveUser(User user)
    {
        _users.Add(user);
    }
}
Enter fullscreen mode Exit fullscreen mode

Structuring Your C# Project for Hexagonal Architecture

By now, you should have a clear structure in place. Here’s what your solution should look like:

/Solution
  /Core
    /Interfaces
      IFileStorage.cs
      IUserRepository.cs
  /Infrastructure
    LocalFileStorage.cs
    DatabaseUserRepository.cs
  /UI
    // Your application logic and UI
Enter fullscreen mode Exit fullscreen mode

This structure keeps everything neat and organized, making it easy to navigate and maintain.

Practical Examples

Building a Simple Application Using Hexagonal Architecture in C

Let’s build a basic application that saves user data using Hexagonal Architecture.

Step 1: Define the Core Logic

// User.cs in Core layer
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement Repositories in Infrastructure

// DatabaseUserRepository.cs
public class DatabaseUserRepository : IUserRepository
{
    private readonly List<User> _users = new List<User>();

    public User GetUserById(int id)
    {
        return _users.FirstOrDefault(u => u.Id == id);
    }

    public void SaveUser(User user)
    {
        _users.Add(user);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Services in Core

// UserService.cs
public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void AddUser(User user)
    {
        _userRepository.SaveUser(user);
    }

    public User GetUser(int id)
    {
        return _userRepository.GetUserById(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Integrate with UI

Finally, integrate the service with a simple UI.

// Program.cs in UI
class Program
{
    static void Main(string[] args)
    {
        // Setup Dependency Injection
        var services = new ServiceCollection();
        services.AddTransient<IUserRepository, DatabaseUserRepository>();
        services.AddTransient<UserService>();
        var serviceProvider = services.BuildServiceProvider();

        // Get UserService
        var userService = serviceProvider.GetService<UserService>();

        // Add a user
        var user = new User { Id = 1, Name = "John Doe" };
        userService.AddUser(user);

        // Retrieve the user
        var retrievedUser = userService.GetUser(1);
        Console.WriteLine($"User retrieved: {retrievedUser.Name}");
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above example, the Program class in our UI layer interacts with the UserService from the Core layer, which in turn uses IUserRepository from the Infrastructure layer.

Real-World Use Cases

Hexagonal Architecture is highly versatile and can be applied to various kinds of projects, from simple console applications to complex enterprise systems. Whether you’re building an ecommerce platform, an online banking application, or a social network, Hexagonal Architecture can help keep your codebase clean and maintainable.

Migration Strategies from Traditional Architectures

If you’re working with a legacy system, migrating to Hexagonal Architecture can seem difficult. But don’t worry, here’s a simple strategy:

  • Identify Core Logic: Start by identifying the core logic of your application.
  • Define Ports: Create interfaces for the identified logic.
  • Create Adapters: Implement the interfaces as adapters.
  • Refactor Gradually: Refactor the system gradually, replacing direct dependencies with abstractions.

This incremental approach helps you adopt Hexagonal Architecture without causing significant disruptions.

Testing in Hexagonal Architecture

One of the biggest wins with Hexagonal Architecture is enhanced testability. In this section, we’ll explore different types of testing.

Unit Testing

Unit testing focuses on individual components. Here’s a test for UserService:

// UserServiceTests.cs
using Moq;

public class UserServiceTests
{
    [Fact]
    public void AddUser_ShouldSaveUser()
    {
        // Arrange
        var userRepositoryMock = new Mock<IUserRepository>();
        var userService = new UserService(userRepositoryMock.Object);
        var user = new User { Id = 1, Name = "Jane Doe" };

        // Act
        userService.AddUser(user);

        // Assert
        userRepositoryMock.Verify(r => r.SaveUser(user), Times.Once);
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration Testing

Integration tests verify the interaction between different components. Here’s an example:

// UserIntegrationTests.cs
public class UserIntegrationTests
{
    private ServiceProvider serviceProvider;

    public UserIntegrationTests()
    {
        // Setup Dependency Injection
        var services = new ServiceCollection();
        services.AddTransient<IUserRepository, DatabaseUserRepository>();
        services.AddTransient<UserService>();
        serviceProvider = services.BuildServiceProvider();
    }

    [Fact]
    public void UserService_ShouldRetrieveSavedUser()
    {
        // Arrange
        var userService = serviceProvider.GetService<UserService>();
        var user = new User { Id = 1, Name = "John Doe" };
        userService.AddUser(user);

        // Act
        var retrievedUser = userService.GetUser(1);

        // Assert
        Assert.Equal("John Doe", retrievedUser.Name);
    }
}
Enter fullscreen mode Exit fullscreen mode

End-to-End Testing

End-to-end (E2E) tests evaluate the system as a whole. They mimic real user interactions and ensure that the entire application works as expected.

Best Practices and Common Pitfalls

Here, we’ll share some best practices and common mistakes to avoid when implementing Hexagonal Architecture in C#.

Best Practices for C# Hexagonal Architecture

  • Keep It Simple: Don’t over-engineer. Start with a simple structure and refine as needed.
  • Use Dependency Injection: Make good use of DI to manage dependencies.
  • Write Tests: Test your core logic and adapters thoroughly.

Common Pitfalls and How to Avoid Them

  • Overcomplicating the Design: Avoid creating too many layers and abstractions. Keep it straightforward.
  • Neglecting Tests: Skipping tests can lead to bugs and difficult-to-maintain code. Write tests regularly.
  • Ignoring Performance: Ensure your design doesn’t introduce performance bottlenecks.

Performance Considerations

To keep your application performant:

  • Minimize Layer Hopping: Too many layers can slow down your application. Keep layers minimal and focused.
  • Optimize Data Access: Use efficient data access patterns and databases.

Advanced Topics

Let’s take it up a notch with advanced topics like microservices, event-driven design, and transaction management.

Using Hexagonal Architecture with Microservices

Hexagonal Architecture and microservices are a match made in heaven. Each microservice can be designed using Hexagonal Architecture, making them independent and easily replaceable.

Event-Driven Design with Hexagonal Architecture

An event-driven design can enhance the flexibility of your system. For example, you can use an event bus to decouple components further.

// EventBus.cs
public class EventBus
{
    private readonly List<IEventListener> listeners = new List<IEventListener>();

    public void Subscribe(IEventListener listener)
    {
        listeners.Add(listener);
    }

    public void Publish(Event e)
    {
        foreach(var listener in listeners)
        {
            listener.Handle(e);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Managing Transactions in Hexagonal Architecture

Managing transactions is crucial. Use Unit of Work patterns to ensure transactional integrity.

// UnitOfWork.cs
public interface IUnitOfWork
{
    void Commit();
    void Rollback();
}

public class EFUnitOfWork : IUnitOfWork
{
    private readonly DbContext context;

    public EFUnitOfWork(DbContext context)
    {
        this.context = context;
    }

    public void Commit()
    {
        context.SaveChanges();
    }

    public void Rollback()
    {
        // Implementation to rollback transaction
    }
}
Enter fullscreen mode Exit fullscreen mode

Tools and Libraries

Here are some tools and libraries that can make your life easier when working with Hexagonal Architecture in C#.

Popular Libraries for C# Hexagonal Architecture

  • AutoMapper: For object-to-object mapping.
  • Moq: For mocking in unit tests.
  • MediatR: For implementing the mediator pattern.

IDE and Plugin Recommendations

  • Visual Studio: The powerhouse IDE for C# development.
  • ReSharper: Boost your productivity with this amazing plugin.
  • NCrunch: Automated, continuous testing within Visual Studio.

Conclusion

By now, you should have a solid understanding of Hexagonal Architecture, its benefits, and how to implement it in C#. Ready to revolutionize your coding practice?

Recap of Key Takeaways

  • Hexagonal Architecture separates core logic from external systems.
  • Ports (interfaces) and Adapters (implementations) are key components.
  • Dependency Injection is crucial for flexibility.
  • Testing becomes a breeze with Hexagonal Architecture.

FAQs About C# Hexagonal Architecture

What are the main principles of Hexagonal Architecture?

  • Separation of Concerns
  • Dependency Injection
  • Ports and Adapters pattern

How does Hexagonal Architecture differ from other patterns?

Hexagonal Architecture focuses on decoupling the core logic from the external systems, unlike layered architectures which may tightly couple components.

Is Hexagonal Architecture suitable for all types of projects?

While it’s highly versatile, small projects may find it overkill. It’s best for medium to large-scale applications where maintainability and testability are critical.

Top comments (0)