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.
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.
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.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.