Several years ago, I got the “Pro ASP.NET Web API” book. This article is the offshoot of ideas from this book, a little CQRS, and my own experience developing client-server systems.
In this article, I’ll be covering:
Why ASP.NET Core?
ASP.NET Core provides many improvements over the ASP.NET MVC/Web API. Firstly, it is now one framework and not two. I really like it because it is convenient and there is less confusion. Secondly, we have logging and DI containers without any additional libraries, which saves me time and allows me to concentrate on writing better code instead of choosing and analyzing the best libraries.
What Are Query Processors?
A query processor is an approach when all business logic relating to one entity of the system is encapsulated in one service and any access or actions with this entity are performed through this service. This service is usually called {EntityPluralName}QueryProcessor. If necessary, a query processor includes CRUD (create, read, update, delete) methods for this entity. Depending on the requirements, not all methods may be implemented. To give a specific example, let’s take a look at ChangePassword. If the method of a query processor requires input data, then only the required data should be provided. Usually, for each method, a separate query class is created, and in simple cases, it is possible (but not desirable) to reuse the query class.
Our Aim
In this article, I’ll show you how to make an API for a small cost management system, including basic settings for authentication and access control, but I will not go into the authentication subsystem. I will cover the whole business logic of the system with modular tests and create at least one integration test for each API method on an example of one entity.
Requirements for the developed system: The user can add, edit, delete his expenses and can see only their expenses.
The entire code of this system is available at on Github.
So, let’s start designing our small but very useful system.
API Layers
The diagram shows that the system will have four layers:
In addition to the described layers, we have several important concepts. The first is the separation of data models. The client data model is mainly used in the REST API layer. It converts queries to domain models and vice versa from a domain model to a client data model, but query models can also be used in query processors. The conversion is done using AutoMapper.
Project Structure
I used VS 2017 Professional to create the project. I usually share the source code and tests on different folders. It’s comfortable, it looks good, the tests in CI run conveniently, and it seems that Microsoft recommends doing it this way:
Project Description:
Project | Description |
---|---|
Expenses | Project for controllers, mapping between domain model and API model, API configuration |
Expenses.Api.Common | At this point, there are collected exception classes that are interpreted in a certain way by filters to return correct HTTP codes with errors to the user |
Expenses.Api.Models | Project for API models |
Expenses.Data.Access | Project for interfaces and implementation of the Unit of Work pattern |
Expenses.Data.Model | Project for domain model |
Expenses.Queries | Project for query processors and query-specific classes |
Expenses.Security | Project for the interface and implementation of the current user’s security context |
References between projects:
Expenses created from the template:
Other projects in the src folder by template:
All projects in the tests folder by template:
This article will not describe the part associated with the UI, though it is implemented.
The first step was to develop a data model that is located in the assembly
Expenses.Data.Model
:
The Expense
class contains the following attributes:
This class supports “soft deletion” by means of the IsDeleted
attribute and contains all the data for one expense of a particular user that will be useful to us in the future.
The User
, Role
, and UserRole
classes refer to the access subsystem; this system does not pretend to be the system of the year and the description of this subsystem is not the purpose of this article; therefore, the data model and some details of the implementation will be omitted. The system of access organization can be replaced by a more perfect one without changing the business logic.
Next, the Unit of Work template was implemented in the Expenses.Data.Access
assembly, the structure of this project is shown:
The following libraries are required for assembly:
It is necessary to implement an EF
context that will automatically find the mappings in a specific folder:
Mapping is done through the MappingsHelper
class:
The mapping to the classes is in the Maps
folder, and mapping for Expenses
:
Interface IUnitOfWork
:
Its implementation is a wrapper for EF DbContext
:
The interface ITransaction
implemented in this application will not be used:
Its implementation simply wraps the EF
transaction:
Also at this stage, for the unit tests, the ISecurityContext
interface is needed, which defines the current user of the API (the project is Expenses.Security
):
Next, you need to define the interface and implementation of the query processor, which will contain all the business logic for working with costs—in our case, IExpensesQueryProcessor
and ExpensesQueryProcessor
:
The next step is to configure the Expenses.Queries.Tests
assembly. I installed the following libraries:
Then in the Expenses.Queries.Tests
assembly, we define the fixture for unit tests and describe our unit tests:
After the unit tests are described, the implementation of a query processor is described:
Once the business logic is ready, I start writing the API integration tests to determine the API contract.
The first step is to prepare a project Expenses.Api.IntegrationTests
public class ApiServer : IDisposable
{
public const string Username = “admin”;
public const string Password = “admin”;
For the convenience of working with HTTP
requests in integration tests, I wrote a helper:
At this stage, I need to define a REST API contract for each entity, I’ll write it for REST API expenses:
URL | Method | Body type | Result type | Description |
---|---|---|---|---|
Expense | GET | – | DataResult<ExpenseModel> | Get all expenses with possible usage of filters and sorters in a query parameter “commands” |
Expenses/{id} | GET | – | ExpenseModel | Get an expense by id |
Expenses | POST | CreateExpenseModel | ExpenseModel | Create new expense record |
Expenses/{id} | PUT | UpdateExpenseModel | ExpenseModel | Update an existing expense |
When you request a list of costs, you can apply various filtering and sorting commands using the AutoQueryable library. An example query with filtering and sorting:
/expenses?commands=take=25%26amount%3E=12%26orderbydesc=date
A decode commands parameter value is take=25&amount>=12&orderbydesc=date
. So we can find paging, filtering, and sorting parts in the query. All the query options are very similar to OData syntax, but unfortunately, OData is not yet ready for .NET Core, so I’m using another helpful library.
The bottom shows all the models used in this API:
Models CreateExpenseModel
and UpdateExpenseModel
use data annotation attributes to perform simple checks at the REST API level through attributes.
Next, for each HTTP
method, a separate folder is created in the project and files in it are created by fixture for each HTTP
method that is supported by the resource:
Implementation of the integration test for getting a list of expenses:
Implementation of the integration test for getting the expense data by id:
Implementation of the integration test for creating an expense:
Implementation of the integration test for changing an expense:
Implementation of the integration test for the removal of an expense:
At this point, we have fully defined the REST API contract and now I can start implementing it on the basis of ASP.NET Core.
API Implementation
Prepare the project Expenses. For this, I need to install the following libraries:
After that, you need to start creating the initial migration for the database by opening the Package Manager Console, switching to the Expenses.Data.Access
project (because the EF
context lies there) and running the Add-Migration InitialCreate
command:
In the next step, prepare the configuration file appsettings.json in advance, which after the preparation will still need to be copied into the project Expenses.Api.IntegrationTests
because from there, we will run the test instance API.
The logging section is created automatically. I added the Data
section to store the connection string to the database and my ApplicationInsights
key.
Application Configuration
You must configure different services available in our application:
Turning on of ApplicationInsights
: services.AddApplicationInsightsTelemetry(Configuration);
Register your services through a call: ContainerSetup.Setup(services, Configuration);
ContainerSetup
is a class created so we don’t have to store all service registrations in the Startup
class. The class is located in the IoC folder of the Expenses project:
Almost all the code in this class speaks for itself, but I would like to go into the ConfigureAutoMapper
method a little more.
This method uses the helper class to find all mappings between models and entities and vice versa and gets the IMapper
interface to create the IAutoMapper
wrapper that will be used in controllers. There is nothing special about this wrapper—it just provides a convenient interface to the AutoMapper
methods.
To configure AutoMapper, the helper class is used, whose task is to search for mappings for specific namespace classes. All mappings are located in the folder Expenses/Maps:
All mappings must implement a specific interface:
An example of mapping from entity to model:
Also, in the Startup.ConfigureServices
method, authentication through JWT Bearer tokens is configured:
And the services registered the implementation of ISecurityContext
, which will actually be used to determine the current user:
Also, we changed the default MVC registration a little in order to use a custom error filter to convert exceptions to the right error codes:
services.AddMvc(options => { options.Filters.Add(new ApiExceptionFilter()); });
Implementing the ApiExceptionFilter
filter:
It’s important not to forget about Swagger
, in order to get an excellent API description for other https://www.toptal.com/api:
The Startup.Configure
method adds a call to the InitDatabase
method, which automatically migrates the database until the last migration:
Swagger
is turned on only if the application runs in the development environment and does not require authentication to access it:
Next, we connect authentication (details can be found in the repository):
ConfigureAuthentication(app);
At this point, you can run integration tests and make sure that everything is compiled but nothing works and go to the controller ExpensesController
.
Note: All controllers are located in the Expenses/Server folder and are conditionally divided into two folders: Controllers and RestApi. In the folder, controllers are controllers that work as controllers in the old good MVC—i.e., return the markup, and in RestApi, REST controllers.
You must create the Expenses/Server/RestApi/ExpensesController class and inherit it from the Controller class:
Next, configure the routing of the ~ / api / Expenses
type by marking the class with the attribute [Route ("api / [controller]")]
.
To access the business logic and mapper, you need to inject the following services:
At this stage, you can start implementing methods. The first method is to obtain a list of expenses:
The implementation of the method is very simple, we get a query to the database which is mapped in the IQueryable <ExpenseModel>
from ExpensesQueryProcessor
, which in turn returns as a result.
The custom attribute here is QueryableResult
, which uses the AutoQueryable
library to handle paging, filtering, and sorting on the server side. The attribute is located in the folder Expenses/Filters
. As a result, this filter returns data of type DataResult <ExpenseModel>
to the API client.
Also, let’s look at the implementation of the Post method, creating a flow:
Here, you should pay attention to the attribute ValidateModel
, which performs simple validation of the input data in accordance with the data annotation attributes and this is done through the built-in MVC checks.
Full code of ExpensesController
:
I’ll start with problems: The main problem is the complexity of the initial configuration of the solution and understanding the layers of the application, but with the increasing complexity of the application, the complexity of the system is almost unchanged, which is a big plus when accompanying such a system.
And it’s very important that we have an API for which there is a set of integration tests and a complete set of unit tests for business logic. Business logic is completely separated from the server technology used and can be fully tested. This solution is well suited for systems with a complex API and complex business logic.
If you’re looking to build an Angular app that consumes your API, check out by fellow Toptaler Pablo Albella.
Understanding the Basics
What is a Data Transfer Object?
A Data Transfer Object (DTO) is a representation of one or more objects in a database. A single database entity can be represented with or without any number of DTOs
What is a web API?
A web API provides an interface to a system’s business logic access to the database and underlying logic are encapsulated in the API.
What is a REST API?
The actual interface through which clients can work with a Web API. It works over HTTP(s) protocol only.
What is unit testing?
Unit testing is a set of small, specific, very fast tests covering a small unit of code, e.g. classes. Unlike integration testing, unit testing ensures that all aspects of the unit are tested in isolation from other components of the overall application.
What is Web API integration testing?
Integration testing is a set of tests against a specific API endpoint. Unlike unit testing, integration testing checks that all units of code that power the API work as expected. These tests may be slower than unit-tests.
What is ASP.NET Core?
ASP.NET Core is a rewrite and the next generation of ASP.NET 4.x. It is cross-platform and compatible with Windows, Linux, and Docker containers.
What is a JWT Bearer token?
A JWT (JSON Web Token) Bearer token is a stateless and signed JSON object that is widely used in modern Web & Mobile applications to provide access to an API. These tokens contain their own claims and are accepted as long as the signature is valid.
What is Swagger?
Swagger is a library used document a REST API. The documentation itself can also be used to generate a client for the API for different platforms, automatically.
About the author
This content was originally published here.