Structuring ASP.NET Core project: Web API and Services
8/27/2021 by InstanceMaster
In this article we are going to apply Consumer->Service->Provider architecture to the Web API ASP.NET Core application. Please see the previous article describing fundamentals of this architecture.
The typical .Net Core Web API project Visual Studio project contains “Controllers” folder, Startup.cs and Program.cs. If authentication is chosen, it creates database model and “Migration” folder in the same project. This is a perfect project structure for the start.
As development progresses, the controllers “grow” to the enormous size. They call EF code directly, execute queries, send emails etc. It may help a little to move reusable code to “the BaseController” and every controller can be inherited from it. However, it would complicate providing of sufficient unit testing coverage (if any). This is when Consumer->Service->Provider comes to rescue.
ASP.NET Core Web API project
As mentioned in the introductory article, the sample app has three branches: web-api, blazor-app and angular-app. The ASP.NET Core project (server side) is almost the same for all three branches. The difference is that angular-app and blazor-app have a client configuration and handling code and web-api is a pure “server side” app.
Here is the high-level diagram of the server’s architecture:
For the latest and greatest information about project structure, see the README file.
Controllers
Controllers represent Consumers in this case. Controller’s function is handling web requests. There is no more to say here. Controllers’ responsibility is accepting HTTP request, read inputs (query or request body), call the service, return result from service back to caller and handle exceptions converting them to HTTP code. It “consumes” a service. Any logic not directly related to HTTP should be handled by the dedicated service. Typically, controllers don’t need to be unit tested.
Service
As explained in the [introductory article]((/blog/aspnet-core-introduction-to-the-clean-architecture), Service is a middleware component and contains all high-level logic or, so called the business logic. It “sits” between controller and data access components and other providers. It is designed to abstract controller from the details of implementation and keep controller’s code very simple. In turn, Service is protected by the abstraction from details of database or other framework features implementations. The whole idea that you can take service, place it in another application code (e.g. the desktop application), implement all the interfaces needed for it to operate and it will just work without any change.
Service should follow single responsibility pattern. It should not get into the trap of satisfy needs of the specific controller. It should have a single purpose. For instance, "SudentService" should include functions needed to List, Create, Update, Delete student but has nothing to do with teaches.
Typical service lives in “Services” folder of the project. It refers to “Tools” namespace for some basic tools for the service configuration. Potentially tools and all the services or some of them can moved into a separate assembly.
Providers
Currently sample app has several providers:
- Identity and Authentication is the set of interfaces implementing user and password validation and management. Interfaces declarations and implementation resides in "Identity" folder of the project. Potentially it can be moved not a separate assembly if authentication process gets more complicated.
- Email provider: set of interfaces for composing and sending emails wrapped into a separate assembly "EmailProcessor". There is an implementation based on MailKit.
- Database provider: please see the next article for the details
Example
Let’s look at Consumer->Service->Provider in action. As an example, let’s take a forgot password implementation in the sample app. Calling this API should perform 4 specific steps:
- Verify if the given email address exists in the system
- Generate reset password token
- Create an URL for the password reset
- Send email to the customer
Following the definitions above, controller has only one responsibility - to call service:
[Route("forgot-password")]
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ForgotPassword(ForgotPasswordParameters parameters)
{
try
{
return Ok(await Service.ForgotPasswordAsync(parameters, this.Request.Scheme, this.Request.Host.Host, this.Request.Host.Port));
}
catch (BadRequestException ex)
{
return BadRequest(ex.Message);
}
}
As you can see, everything controller does it passes parameters needed for service to work and handles a result or an error. In turn, service implements this 4 steps logic:
public AuthorizationService(IApplicationUserManager userManager,
IEmailSender emailSender)
{
...
}
...
public async Task<bool> ForgotPasswordAsync(ForgotPasswordParameters forgotParameters, string scheme, string host, int? port)
{
// 1. Verify if the given email address exists in the system
var user = await UserManager.FindByEmailAsync(forgotParameters.Email);
if (user == null)
{
// Pretend that email has been sent
return true;
}
// 2. Generate reset password token
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
// 3. Create the url for the password reset
var uriBuilder = new UriBuilder();
uriBuilder.Scheme = scheme;
uriBuilder.Host = host;
if(port.HasValue)
{
uriBuilder.Port = port.Value;
}
uriBuilder.Path = "authentication/reset-password";
uriBuilder.Query = $"email={user.Email}&token={HttpUtility.UrlEncode(token)}";
// 4. Send email to the customer
await EmailSender.SendAsync(
TemplateFactory.CreateResetPasswordMessage(
new EmailAddress {
Name = user.UserName,
Address= user.Email
}, uriBuilder.Uri.AbsoluteUri)
);
return true;
}
Please notice that ForgotPasswordAsync
uses UserManger
and EmailSender
references via interfaces. It doesn’t really have to rely on the specific implementation of these two interfaces.
Testing
Using example above we can construct unit tests that would cover all possible scenarios without complicated setups. For instance, ForgotPasswordAsync should not call generate token and send email if user doesn’t exist:
[TestMethod()]
public async Task ForgotPasswordEmailDoesntExistTest()
{
Mock<IApplicationUserManager> userManagerMock = new Mock<IApplicationUserManager>();
Mock<IEmailSender> emailSenderMock = erverTestUtils.CreateSignManager(true);
//Setup mocks
userManagerMock.Setup(x => x.FindByEmailAsync(It.IsAny<string>())).ReturnsAsync((ApplicationUser)null);
userManagerMock.Setup(x => x.GeneratePasswordResetTokenAsync(It.IsAny<ApplicationUser>()))
.ReturnsAsync("ttttokenxxx");
emailSender.Setup(x => x.SendAsync(It.IsAny<IEmailMessage>()));
// Run test
var result = await authorizationService.ForgotPasswordAsync(new ForgotPasswordParameters
{
Email = "test@test.com"
}, "https", "test.com", 8080);
// Verify results
Assert.IsTrue(result);
userManagerMock.Verify(x => x.FindByEmailAsync(It.IsAny<string>()), Times.Once());
// Following two should not be called
userManagerMock.Verify(x => x.GeneratePasswordResetTokenAsync(It.IsAny<ApplicationUser>()), Times.Never());
emailSenderMock.Verify(x => x.SendAsync(It.IsAny<IEmailMessage>()), Times.Never());
}
Please check the latest code.
Configuration
Sample app includes some tools needed to support the architecture. Adding services ASP.NET application is easy. There is a AppServiceAttribute
definition in Sample app that should be used to declare a class as a service. For instance:
[AppService]
public class AuthorizationService
{
...
}
Please check AuthorizationService
for the latest implementation.
The in Startup.cs:ConfigureServices
:
Call
AddIdentity
andAddMailKit
to initialize implementation ofIApplicationUserManager
andIEmailSender
Add
AddAppServices()
call of extension method to add all service classes to the service collection.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity();
services.AddMailKit(Configuration);
...
services.AddAppServices();
...
}
In order to obtain reference to a service, in a controller, add property and a parameter in the constructor:
[ApiController]
public class AuthorizationController
{
AuthorizationService Service { get; }
public AuthorizationController(AuthorizationService service)
{
Service = service;
}
Question: Why controller consumes service reference directly instead of via interface as it is done with UserManager and EmailSender?
The answer is to simplify the structure of the project. However, abstracting a service would make sense in the cases when controller has additional responsibilities and requires unit testing coverage. In our case controller’s logic should be very straight forward and doesn't have to be unit tested. Another example when abstraction is needed when service must be consumed by other services.
Shared model
Shared model is a set of entities that shared between client and server. These usually reside in Shared assembly. In case of Blazor project this assembly is used by both: Server and Client. Shared model objects are not a database objects and should not be used as ones.
Service should be able to transform database type into shared type. In the next article we will look into “Decorators” which help service perform such kinds of transformations.
There is a name convention to distinguish between different types of objects. For instance, objects like Employee that can be returned as a list, inserted, updated, or delete should end with “Item” and object like login parameters or registration should end with “Parameters”.
Next
In the next article we will talk about implementing Data Providers for SQLServer and PostgreSQL.
I would appreciate your feedback.