menu
sluiten
terug naar overzicht

09/09/21

insight

.NET Applicatieontwikkeling

Albert Starreveld

+31 (0)35 539 09 09


09/09/21

Implementing Clean Architecture, DDD-style, with .NET Core

In 2017 Uncle Bob wrote a great book about clean architecture. It explains the principles of a good software architecture. The book contains lots of information about the SOLID principles, about boundaries in the application, about screaming architecture, and so forth. What’s great about the book, is that it isn’t dogmatic. It doesn’t have code samples explaining how to implement a use case or a controller. However, at some point you need to start coding. This leads to the question: What could a clean architecture look like? In this article I’ll explain my take on clean architecture.

The Clean Architecture

Robert C. Martin wrote a book and a webpage about clean architecture. The schematic representation of the clean architecture looks like this:

The Clean Architecture

In this schematic representation, they architecture looks like an onion. It has layers. Each layer has a boundary. All dependencies point inwards. As a result, the closer you get to the core, the fewer dependencies the code has.

1.) Entities

In contrast to older architectures, the database is not the centre of the application any more. The business rules are.

It’s quite abstract. In the clean architecture, business-rules are implemented at the core of the application: in the use-cases, and in entities. But when is something an entity? And what is a use-case?

Assume we’re working on a cab dispatching application. Dispatching needs to dispatch a cab to a location. A location is defined as a longitude and a latitude. A valid longitude must be + or — 180 degrees. This is an example of a business rule. Implementing this as an entity at in the core of the application could result in this struct:

public readonly struct Location
{
    public decimal Longitude { get; }
    public decimal Latitude { get; }

    public static Location Create(decimal longitude, decimal latitude)
    {
        return new(longitude, latitude);
    }
    
    private Location(decimal longitude, decimal latitude)
    {
        if (longitude > 180 || longitude < -180)
        {
            throw new ArgumentException(nameof(longitude));
        }
        
        if (latitude > 90 || latitude < -90)
        {
            throw new ArgumentException(nameof(longitude));
        }

        Longitude = longitude;
        Latitude = latitude;
    }
}

A cab dispatching application contains several other domain concepts too. Like a cab, a cab driver, and a passenger for example. There’s a difference between these objects. Either their state changes, or they are immutable. A location, for example, is immutable. A cab, on the other hand, is not. It has an increasing mileage. And it is either taken or not. As a result, a cab entity is probably not a struct, but a class, and it might look like this:

public class Cab
{   
    private readonly int _cabSize;
    private readonly List<Passenger> _passengers = new ();

    public Mile Mileage { get; private set; }

    public Location Location { get; private set; }

    public IReadOnlyList<Passenger> Passengers => _passengers.AsReadOnly();

    public bool Taken => _passengers.Any();

    public Cab(Location location, int cabSize)
    {
        _cabSize = cabSize;
        Location = location;
    }

    public void Embark(Passenger passenger)
    {
        const int cabDrivers = 1;
        _passengers.Add(passenger);

        if (_passengers.Count + cabDrivers > _cabSize)
        {
            throw new NotSupportedException("Too many passengers.");
        }
    }

    public Receipt Drive(Mile distance, Location newLocation)
    {
        var receipt = Receipt.Create(distance, Location, newLocation); 
        
        Mileage += distance;
        Location = newLocation;
        
        return receipt;
    }

    public void Disembark() => _passengers.Clear();
}

Nothing is “wrong” in clean architecture. It doesn’t matter whether you have anaemic or rich domain models. All the entities need to do is to capture the high-level business rules.

2.) Use cases

Entities enforce high-level business rules. From a single responsibility perspective, they have only one reason to change. In fact.. They will probably not change at all. A mile will always be 5280 feet and we can only fit as many people in to a car as there are seats. That will never change.

But some things do change. When will a company bring a car to the junkyard? After 300,000 miles? After 5 years? A company is free to change that policy whenever they want. Those kind of business-rules are use-case specific. To not violate the Single Responsibility Principle, these “ifs” go into a separate file: the use-cases.

Use cases interact with the entities. They drive entities to achieve a business goal. Every use-case tells a story. And it might look like this:

public class DeprecationUseCase
  {
      private readonly ICabRepository _cabRepository;
      private readonly IJunkyardService _junkyardService;
      private readonly IMoneyService _moneyService;

      public DeprecationUseCase(ICabRepository cabRepository, IJunkyardService junkyardService, IMoneyService moneyService)
      {
          _cabRepository = cabRepository ?? throw new ArgumentNullException(nameof(cabRepository));
          _junkyardService = junkyardService ?? throw new ArgumentNullException(nameof(junkyardService));
          _moneyService = moneyService ?? throw new ArgumentNullException(nameof(moneyService));
      }

      public async Task SellDeprecatedCabs()
      {
          var cabs = await _cabRepository.GetAll();

          var limit = Mile.Create(300000);
          var money = Dollar.None;
          
          foreach (var deprecatedCab in cabs.Where(x => x.Mileage > limit))
          {
              money += await _junkyardService.Sell(deprecatedCab);
          }

          await _moneyService.Deposit(money);
      }
  }
Crossing the boundaries

The rule of clean architecture is: all dependencies point inwards. The entities are at the core of the application, the are use-case layer depends on the entities, and the infrastructure layer depends on the use-cases.

As you can see in the sample-code, the use-case layer interacts with infrastructure too. Like the database, for example. That makes the use-case layer seem dependent on the infrastructure layer. But with Clean Architecture, it should be the other way around. So how does that work?

At the core of the application, you’ll find entities, use-cases, and interfaces. These interfaces define what the outside layers will look like and how the use-cases will interact with it. But the implementation is not at the “core” of the application. It’s implemented in another layer. That’s how the boundary is crossed.

Core

There are two types of interaction with use-cases. The use-cases interact with the system, or a system interacts with the use-cases. In ports-and-adapters, these are called the primary- and the secondary ports.

A primary port is an interface to something the application will respond to. Like a keyboard, for example. When you hit enter, the application will do something.

A secondary port is an interface to a system the application interacts with. Like a database. The application is operating the database. It sends it instructions, and the database responds to it.

Screaming architecture

In most of the applications I’ve worked with, this is what the folder structure looks like:

No! Don’t do this! Group by functionality instead!

We have a tendency to group things by type. But this makes it unclear what the application does with these types. Not to mention it’s failing capacity to scale. Try putting 300 entities in a folder. It will be a mess.

Instead, focus on the functionality of the application. We are building a cab dispatching application. So, group everything by functionality, and have the application screaming “CABS!!! CAB DRIVERS!! I’M A CAB DISPATCHING APPLICATION!!!”. It should be clear what the application does by it’s file- and folder structure only, without reading a single line of code. Put interfaces, entities, and use-cases in the same folder. By grouping by functionality, the application will grow organically and remain easy to navigate:

Try to make the file- and folder structure reflect the functionality in the application. Group items by functionality, not by type. This makes it much easier to find things.

Read more on Screaming Architecture here, and here.

3.) Controllers

The use cases and the entities are the core of the application. By invoking one, or a sequence of use cases, an application can achieve a business goal. There are several “things” that can do this. In case of an event-driven application, a command handler will typically invoke the use-cases. In my case, I’m building a REST API, and the consumer of the API can invoke the use-cases directly. A simple controller will look like this:

public class MaintenanceController : ControllerBase
  {
      [HttpDelete]
      [Route("cars/deprecated")]
      public async Task<IActionResult> SellDeprecatedCars([FromServices] DeprecationUseCase useCase)
      {
          await useCase.SellDeprecatedCabs();
          return NoContent();
      }
  }

 

Or, in a more complex scenario:

public class RidesController : ControllerBase
{
    [HttpPost]
    [Route("rides/airport")]
    public async Task<IActionResult> CreateRide([FromServices] VacationUseCase useCase, Models.Location request)
    {
        var passenger = Passenger.Create(request.Long, request.Lat);
        var receipt = await useCase.DrivePassengerToAirport(passenger);
        
        return Ok(new Models.Location
        {
            Long = receipt.Destination.Longitude,
            Lat = receipt.Destination.Latitude
        });
    }
}

Note that I’m not using dependency injection on the constructor to inject the use-cases into the controller. I’m injecting directly into the method instead, because every controller method will probably use another use-case.

4.) Infrastructure

With the entities, use-cases, and controllers in place, there’s only one more layer left: The infrastructure layer. This layer contains things like database adapters, or API calls to third party API’s.

Implementing infrastructure on the outer layer has several advantages:

  • They are easily replaceable. Nothing depends on it.
  • The application becomes very testable. By mocking the infrastructure, all that’s left are the business rules.
public class CabRepository : ICabRepository
    {
        private readonly DispatchingDbContext _dbContext;

        public CabRepository(DispatchingDbContext dbContext) => _dbContext = dbContext;
        
        public async Task<Cab> FindAvailableCab() => await _dbContext.Cabs.Where(x => !x.Taken).FirstOrDefaultAsync();

        public async Task<IEnumerable<Cab>> GetAll() => await _dbContext.Cabs.ToArrayAsync();

        public async Task Update(Cab cab) => await _dbContext.SaveChangesAsync();
    }

Using this DBContext:

public class DispatchingDbContext : DbContext
    {
        public DispatchingDbContext(DbContextOptions options) : base(options) { }
        
        public DbSet<Cab> Cabs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Cab>(
                e =>
                {
                    e.HasKey("_id");
                    e.Property("_cabSize");
                    e.Property(x => x.Location).HasConversion(x => x.ToString(), x => Location.Parse(x));
                    e.Ignore(x => x.Passengers);
                });
        }
    }

 

4.2) Other infrastructure

Clean architecture projects often have a “Infrastructure” folder. This folder contains the adapters to the bits of infrastructure the application uses to accomplish things:

This application uses:

  • Google maps to calculate distance and to find the nearest airport
  • The imaginary JunkyardAndCo webservice to sell cars
  • SqlServer to store the state of the entities
  • Western Union to handle financial transactions
Putting it all together

There are lots of concepts in the Clean Architecture. There are several ways of having that manifest in an actual application. I’ve built a simple .NET Core application with — what I think — is clean enough. Check it out: https://github.com/appie2go/clean-architecture let me know what you think!

Shout out: Thank you Stacy, Yuri, Dimitri, and Daan!

Delen

Meer weten over dit onderwerp?

Neem contact op met Albert Starreveld
vx company