Building ASP.NET Core Web APIs with Clean Architecture

Last year I wrote a post introducing clean architecture and attempted to explain how its layered approach and separation of concerns can help overcome some common software design pitfalls enabling us to create testable, loosely-coupled code that is easier to maintain and extend.

In this post, we'll revisit Clean Architecture in the context of a somewhat more real-world example by using its principles to design and build an ASP.NET Core based Web API. Understanding these principles is critical for this guide and I won't be covering the basics from scratch so if you're new to Clean Architecture I recommend you check out my previous post or Uncle Bob's to get up to speed.

This guide also assumes knowledge of other topics like MVC, dependency injection and testing so if you run into something you're not familiar with please take a moment to familiarize yourself with any new concepts.

Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

A Story of Layers and Dependencies

At its absolute core, Clean Architecture is really about organizing our code into layers with a very explicit rule governing how those layers may interact.

The overriding rule that makes this architecture work is The Dependency Rule. This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.

Uncle Bob

With that in mind, to get started; I've fleshed out a project structure that should represent each of the logical layers in the diagram.

alt text

Let's break it down a little further:

Web.Api - Maps to the layers that hold the Web, UI and Presenter concerns. In the context of our API, this means it accepts input in the form of http requests over the network (e.g., GET/POST/etc.) and returns its output as content formatted as JSON/HTML/XML, etc. The Presenters contain .NET framework-specific references and types, so they also live here as per The Dependency Rule we don't want to pass any of them inward.

Web.Api.Core - Maps to the layers that hold the Use Case and Entity concerns and is also where our External Interfaces get defined. These innermost layers contain our domain objects and business rules. The code in this layer is mostly pure C# - no network connections, databases, etc. allowed. Interfaces represent those dependencies, and their implementations get injected into our use cases as we'll see shortly.

Web.Api.Infrastructure - Maps to the layers that hold the Database and Gateway concerns. In here, we define data entities, database access (typically in the shape of repositories), integrations with other network services, caches, etc. This project/layer contains the physical implementation of the interfaces defined in our domain layer.

Alright, we now have a skeletal solution spun up that looks to support the high-level layers requested by Clean Architecture - cool! 😎

Using Tests to Guide Development (TDD)

With our project foundation in place, we're ready to get down to brass tacks and write some interesting code. To start, we're going to build out our use cases "from the outside in" by defining them first and then using tests to iteratively tease out and implement only the bits of functionality required to pass them - red-green-refactor - this is the essence of TDD.

Our API needs to provide the capability for new users to create an account from the client application so our first use case will be [drumroll]... RegisterUserUseCase.

I quickly fleshed out a test containing the things I think must happen to satisfy the use case. As you can see below, there's lots of red here at the moment, and this won't even build, but it's a terrific guide that tells us precisely what we must implement thereby driving our development and ensuring we only work on the bits required to get this from red to green.

RegisterUserUseCase is the object under test here and currently has a single dependency on UserRepository which we know will have the responsibility of persisting the user's identity. We're not sure exactly how that will happen at this point and that's fine, we'll come back to that shortly.

To implement RegisterUserUseCase I first created a simple interface named IRegisterUserUseCase which in turn implements IUseCaseRequestHandler that will define the shape of all of our use case classes.

Note on naming; you may see the term Interactor being used in place of Use Case or even combined, i.e., Use Case Interactor in other Clean Architecture implementations. As far as I can tell they serve the same purpose and both represent the classes where the business logic lives. I prefer Use Case as I find it's a little more intuitive.

Next, I took at a crack and implemented the absolute bare minimum amount of code required by the use case at this point. My first cut looked like this:

TDD is an iterative process (red->-green->refactor...repeat) and it will take a few iterations to refine this gradually.

Input and Output Ports

You'll notice the signature for the use case's Handle() method contains two parameters: message and outputPort. The message parameter is an Input Port whose sole responsibility is to carry the request model into the use case from the upper layer that triggers it (UI, controller etc.). This class is simply a lightweight DTO owned by the Core/Domain layer. I created it during the previous step when I set up the use case handling infrastructure.

The second, outputPort as you can probably guess is responsible for getting data out of the use case into a form suitable for its caller. The critical difference is that it needs to be an interface with at least one method available for our Presenter to implement. The physical implementation of the presenter lives in the outer layer and may contain UI/View/Framework specifics and dependencies that we don't want bleeding into our use cases.

In this approach, we use DIP to invert the control flow so rather than getting a return value with the response data from the use case we allow it to decide when and where it sends the data through the output port to the presenter. We could use a callback, but for our purposes, as you'll see shortly, our presenter creates an http response message that is returned by our controller. In the context of a REST API, this response is effectively the "UI" or at very least the output data to be displayed by whatever application might be consuming it. The question of whether or not the use case should call the presenter is a good one, and the answer is likely whichever is most viable for your solution.

The IOutputPort contract is pretty simple; it exposes a single method accepting a generic use case response model it will transform into the final format required by the outer layer. We'll see an example implementation shortly.

public interface IOutputPort<in TUseCaseResponse>
{
   void Handle(TUseCaseResponse response);
}

Next, I created a contract representing IUserRepository with a single method for creating new users.

public interface IUserRepository
{
  Task<CreateUserResponse> Create(User user, string password);       
}

From Red to Green

With a few more iterations the interfaces are complete, and the use case has no more red squiqqles. I moved back to the unit test and touched it up by creating mocks for both collaborators using the suitably named Moq mocking library.

Things are looking much better - my project compiles, I can run the test and it passes! We have a thin slice of functionality working to register new users.

The Data Layer

Most programs require data to make them somewhat interesting or valuable, and our API is no different. Moving up a layer we'll find the data layer. This layer contains our data access and persistence concerns and any frameworks, drivers or dependencies they require. In this layer, we're moving away from the blissful ignorance we had in the domain layer as we now begin to make decisions and lay down concrete implementations of the various data providers our application requires. Persistence and data access are handled using Sql Server and Entity Framework Core. All data access related dependencies and code live in Web.Api.Infrastructure.

I provisioned the infrastructure project with nuget packages for Entity Framework Core and the ASP.NET Core Identity Provider along with code for the UserRepository and a data entity to represent our User.

The implementation of UserRepository at this point is pretty simple. It depends on UserManager which we get out of the box from the identity provider and IMapper which comes from AutoMapper and enables us to elegantly map domain, data and dto objects across the layers of our application while saving us from littering our project with repetitive boilerplate code. UserManager provides APIs for managing users in the membership database.

In our Create() method we start off by mapping our domain user (currently a tad anemic) to the data entity AppUser and then call the CreateAsync() method on _userManager to do the deed of creating the user in the database and returning the result. All of the work to validate the input parameters and create the user is handled by UserManager and everything is abstracted behind the IUserRepository interface. In this sense, it is acting mostly as a facade to keep these implementation details encapsulated in the Infrastructure layer.

internal sealed class UserRepository : IUserRepository
{
    private readonly UserManager<AppUser> _userManager;
    private readonly IMapper _mapper;

    public UserRepository(UserManager<AppUser> userManager, IMapper mapper)
    {
      _userManager = userManager;
      _mapper = mapper;
    }

    public async Task<CreateUserResponse> Create(User user, string password)
    {
      var appUser = _mapper.Map<AppUser>(user);
      var identityResult = await _userManager.CreateAsync(appUser, password);
      return new CreateUserResponse(appUser.Id,identityResult.Succeeded, identityResult.Succeeded ? null : identityResult.Errors.Select(e => new Error(e.Code, e.Description)));
    }
}

Using Migrations to Spin Up a Database

We're ready to use Entity Framework's migrations to create the physical sql database by running two simple dotnet CLI commands from within the infrastructure project folder.

Web.Api.Infrastructure>dotnet ef migrations add initial
Web.Api.Infrastructure>dotnet ef database update

These commands are run on the infrastructure class library project because we've encapsulated all our entity framework-related code there. It's common to have these dependencies/responsibilities live in the root Web API project however because we're seeking a higher level of decoupling in our Clean Architecture we've pulled them out into the infrastructure layer.

After running these commands, a new database appears in my sql server localdb instance. Note, you'll need to ensure you've installed Sql Server Express LocalDB under Individual Components in the Visual Studio installer.

Dependency Injection

At this point, I wired up Autofac and registered the required components and services in the Web API project's Startup and within individual module files in each class library project. Modules provide a very nice way to orgranize your dependencies on a per-project basis when working with Autofac. It also plays a pivotal role in our Clean Architecture by enforcing the Dependency Inversion Principle across the application for us.

The Presentation Layer

This layer is where the rubber effectively meets the road by composing the different components across our architecture into a single, cohesive unit - i.e., our application. Here, we find concerns related to GUIs, web pages, devices, etc. It also contains our Presenters which, as mentioned, are responsible for formatting the response data from our use cases into a convenient format for delivery to whatever human interface our user happens to trigger it from, e.g., a web page, mobile app, microwave, etc.

In the context of a REST API, this layer is relatively simple as there is no GUI, view state or complex user interactions to handle. The request/response semantics of REST means we're mostly just delivering data back to the user. Our presenters, in this case, will construct http messages containing the data and status of the request and return these as the response from our Web API controller operations.

RegisterUserPresenter implements IOutportPort with all of its work happening in Handle(). You'll remember this is the method the use case triggers. From there, we're just building a regular MVC ContentResult and in this case, directly storing the serialized results from the use case and setting the appropriate HttpStatusCode based on the result of the use case.

public sealed class RegisterUserPresenter : IOutputPort<RegisterUserResponse>
{
  public JsonContentResult ContentResult { get; }

  public RegisterUserPresenter()
  {
     ContentResult = new JsonContentResult();
  }

  public void Handle(RegisterUserResponse response)
  {
     ContentResult.StatusCode = (int)(response.Success ? HttpStatusCode.OK : HttpStatusCode.BadRequest);
     ContentResult.Content = JsonSerializer.SerializeObject(response);
  }
}

Setting Up the Controller

The piece at the very furthest edge of our Clean Architecture is the API controller. The controller will receive input in the form of http requests, trigger our use case and respond with some meaningful message based on the outcome.

I added a new AccountsController with a single POST action to receive new user registration requests.

// POST api/accounts
[HttpPost]
public async Task<ActionResult> Post([FromBody] Models.Request.RegisterUserRequest request)
{
     if (!ModelState.IsValid)
     {
        return BadRequest(ModelState);
     }
 
    await _registerUserUseCase.Handle(new RegisterUserRequest(request.FirstName,request.LastName,request.Email, request.UserName,request.Password), _registerUserPresenter);
    return _registerUserPresenter.ContentResult;
}

Not much happening here, we validate the input model (the user's information) using FluentValidation implicitly and if that looks good, trigger the use case and return the ContentResult the presenter has generated based on the results of the operation.

Unit Testing the Controller

I created a new Web.Api.UnitTests project and applied some TDD to iteratively build out the required bits for the controller action with tests from AccountsControllerUnitTests. It's useful to test how our controller behaves based on the validity of the inputs and validate its response based on the result of the operation it performs.

Manual End-to-End Testing with Swagger

Now the moment of truth. I'd like to test the API for real by submitting requests to it externally in the same fashion as its consuming applications will. To help me out, in the Web API project I installed and configured swagger tooling which includes a very handy UI for testing and exploring our API. With that in place, I fired up the project and tested the /api/accounts endpoint by submitting a user registration request via the Swagger UI.

I got back a successful response in the Swagger console - so far so good.

Finally, I queried the database to confirm the associated record was successfully inserted into the AspNetUsers table. 👌

Implementing a Login Use Case

With the user registration API feeling pretty solid I went ahead and implemented a login use case to authenticate user credentials and issue JWT tokens. The process was nearly identical to the one we followed for registration, so I'm going to save some repetitive keystrokes by not documenting it here but if you're interested please check out the code or give it a spin in swagger using the /api/auth/login endpoint.

Wrapping Up

If you made it this far - you rock! 🤘

I hope you can take some value from this guide with an understanding of how Clean Architecture can help produce well-designed, expressive code that is more loosely-coupled and testable.

As we saw, its approach is very flexible in that it can be used with or without other design activities like TDD and DDD and plays nicely with existing application structures like MVC, MVVM, etc.

Putting Clean Architecture or any design pattern into practice successfully takes skill and judgment. Like anything, there's a learning curve associated, and teams must understand and accept its strict rules and conventions to apply it successfully.

Finally, this is just my interpretation of Clean Architecture using a simple example so your mileage may vary applying this to your projects in the real world. As always, if you have any feedback, questions or suggestions for improvement, please let me know below in the comments.

Source code here

Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

Get notified on new posts
X

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.