Structuring ASP.NET Core project: Web API and Services

8/27/2021 by Yuriy Frankiv

Structuring ASP.NET Core project: Web API and Services

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:

  1. Verify if the given email address exists in the system
  2. Generate reset password token
  3. Create an URL for the password reset
  4. 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:

  1. Call AddIdentity and AddMailKit to initialize implementation of IApplicationUserManager and IEmailSender

  2. 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.