Upcoming book:

Modern .NET Development with Azure and DevOps

Practical Clean Architecture solution template

Introduction

A lot has been said and written about the Clean Architecture pattern. This post does not attempt to recreate all those many other posts/books on the subject, but rather, present a simpler, more practical approach that is extensible when the requirements are more complex.

Please do keep in mind that the idea of this post is not to fully comply with any dogmatic approach, but rather to have a simple, pragmatic approach that has been extremely useful to me in dozens of services/applications.

Structure

The structure of an ASP.NET Core solution that uses this approach looks like this (upon creating it):

  • Main (ASP.NET Core) project: SolutionName.API
  • Domain project: SolutionName.Domain
  • Infrastructure project: SolutionName.Infrastructure
  • Entities project: SolutionName.Entities
  • Models project: SolutionName.Models
  • Tests project: SolutionName.Tests

The flow of the dependencies is:

├───API
│   └───Domain
├───Domain
│   ├───Entities
│   ├───Infrastructure
│   └───Models
├───Infrastructure
│   ├───Entities
│   └───Models
├───Entities
├───Models
└───Tests
    ├───Domain
    └───API

API layer

The API in this solution is considered a 'client' of the application's domain. In other words, this means that the API project should only have logic related to ASP.NET Core (to parse requests and return responses, perform auth concerns, etc). When necessary, for example for IFormFiles, this project can also have input models that are translated to the Models layer before being passed over to the Domain layer.
This is also typically called Application and Web.

Domain layer

The Domain layer is where the business logic is written. Service interfaces are exposed (public) for the API to consume via input and output POCO (Plain Old CSharp Objects, aka classes with no logic) models defined in the Models project.
To make it simpler to report errors back to the API, I like having a generic class that encapsulates operation results:

public class OperationResult<T>
{
    public T? Data { get; }
    public OperationResultStatus Status { get; }
    public string? ErrorMessage { get; set; }

    public bool Success => Status == OperationResultStatus.Success;

    // ...
}

public enum OperationResultStatus
{
    Success,
    BadRequest,
    NotFound,
    Forbidden,
    Error
}

This is then used as results for services:

public interface IWeatherService
{
    Task<OperationResult<WeatherDto>> GetWeatherDataAsync(Guid currentAccountId, double latitude, double longitude);
}

The Domain layer consumes services from the Infrastructure project as well when necessary.

Infrastructure layer

This layer simply takes care of infrastructure concerns, that is, configurations for databases, clients for third-party services (such as external API clients, storage services (AWS' S3, Azure's Blob Storage, etc), and so on). It's quite typical to have models for the third-party APIs here as well.

NuGet packages used to configure database connections (like Microsoft.EntityFrameworkCore.SqlServer) should also only be here, unless the package also contains types needed for the entities' definitions.

Entities layer

As usual, your internal entities (database models and etc), are in this layer. I personally prefer POCOs here.

Models layer

Any models (DTOs / Data Transfer Objects) would be defined here. These could be models passed internally (from API to domain) and models received from API calls as well, when the model is simple enough (in other words, no ASP.NET Core dependency).

Tests layer

Personally, I prefer to have a single Tests project for both unit and integration tests, as it makes build pipelines much easier to write as well as the execution of the tests for example in Docker builds. How much or what you test depends on the testing culture at your team. I'd say that the most critical piece you want to test is the domain layer, in unit and/or integration tests.

Improving productivity

The above structure is obviously not ground-breaking, but I have found a number of productivity tips that allow me to code much faster.

Automatic conversion to ActionResult

The OperationResult<T> class shown above has to be mapped to ASP.NET Core's standard IActionResult (or ActionResult<T>), this can be done easily with an extension method in the API project:

internal static class OperationResultExtensions
{
    internal static async Task<ActionResult<T>> ToActionResult<T>(this Task<OperationResult<T>> operation)
    {
        var result = await operation;

        return result.Status switch
        {
            OperationResultStatus.Success => new OkObjectResult(result.Data),
            OperationResultStatus.BadRequest => BadRequest("Bad request", result.ErrorMessage),
            OperationResultStatus.Forbidden => new ForbidResult(),
            _ => new ObjectResult("Internal server error") { StatusCode = StatusCodes.Status500InternalServerError },
        };
    }
}

Which can then be easily used as:

[HttpGet("...")]
public async Task<ActionResult<WeatherDto>> GetWeatherDataAsync(...)
{
    // Simple validations could be done here before calling the service, or in the service.
    return await _weatherService.GetWeatherDataAsync(...).ToActionResult();
}

Simplifying the usage of OperationResult

The OperationResult<T> class can be extended as well with a few implicit operators to simplify the usage in the Domain layer. For example:

public static implicit operator OperationResult<T>(T value)
{
    return new OperationResult<T>(value);
}

public static implicit operator OperationResult<T>(OperationResultStatus status)
{
    return new OperationResult<T>(status, "");
}

In practice, that would look like this:

public async Task<OperationResult<WeatherDto>> GetWeatherDataAsync(Guid currentAccountId, double latitude, double longitude)
{
    if (!await _dbContext.Accounts.AnyAsync(x => x.Id == currentAccountId && x.Enabled))
    {
        return OperationResultStatus.Forbidden;
    }
}

You could also add a base, non-generic OperationResult class for the ability to return error messages along with status codes when you don't need to return any data. OperationResult<T> would then only add the T Data property.

Services injection

All Domain and Infrastructure services need to be injected to the DI container. A good way to achieve this (avoiding the Reflection-based way) is to have a class to inject these services in each project. For example, in Domain:

public static class DomainExtensions
{
    public static IServiceCollection AddDomainServices(this IServiceCollection services)
    {
        services.AddScoped<IWeatherService, WeatherService>();
        // ...

        return services;
    }
}

And in Infrastructure:

public static class InfrastructureExtensions
{
    public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
    {
        var dbOptions = configuration.GetRequiredSection("Database").Get<WeatherDbOptions>();
        dbOptions.Validate();

        services.AddDbContext<SimplifiedCAContext>(/*...*/);

        var weatherApiOptions = configuration.GetRequiredSection("WeatherApi").Get<WeatherApiOptions>();
        weatherApiOptions.Validate();

        services.AddHttpClient<IWeatherApiClient, WeatherApiClient>(/*...*/);

        return services;
    }
}

Conclusion

I hope you found the post useful, and remember you can look at the full sample in my GitHub repository. If I think of more ways to improve the sample code that can be shared, I'll try and add those here.

Your feedback is welcome, as always.