Structuring ASP.NET Core: Introduction to the clean architecture

8/21/2021 by Yuriy Frankiv

Structuring ASP.NET Core project: Introduction to the three layers architecture

This article introduces the sample app: a basic ASP.NET Core project with three layers architecture: Consumers, Services and Providers. This is the first article of the series of articles about structuring ASP.NET Core projects. The following articles in this series will dive into details of the implementation: project structure, service boundaries and responsibilities, unit testing strategy, designing database providers, using EF, etc.

Introduction

Every time I started a new project, I used standard Visual Studio project template. Visual Studio team keeps it up to date, so it is a good way of discovering new features. However, the project template keeps things simple for the obvious reasons. It lacks the structure and organization needed for the mature, feature rich application.

As projects grew, I ended up refactoring my code multiple times, performing loose coupling, introducing layers, interfaces, and unit tests. Inspired by Clean Architecture book by Robert Martin, Angular architecture and SOLID design principles, I shaped app’s architecture into three layers: Consumers, Services and Providers. This three-layer pattern can be applicable to various type of applications.

In order to save some time with the future project, I decided to create my own sample app. It is an open-source project in case it helps anyone else. This sample app is based on Visual studio template but with the specific policies around organizing the source code as a three layers architecture. I still create new projects with every new version of Visual Studio to see what changes and apply similar changes my project to keep it up to date.

So far, I worked on projects with Angular and Blazor frameworks. Thus, the app has three branches: one is pure web api project and other two are dedicated to these two UI frameworks: blazor-app and angular-app. The ASP.NET Core project (server side) is almost the same for all three branches.

Consumer -> Service -> Provider

We are going dive deep into project’s structure in the following articles. But before, lets clarify fundamental things. Both client and server sides following the three-tier architecture: Consumer -> Service -> Provider. The main idea that consumer orders something using a service, service partners with one or more providers to handle the order and to service the consumer. Happy consumer doesn’t have to know any details about its order processing, service should take full care of the consumer. Service heavily relies on one or multiple providers to perform its duties. The diagram below illustrates the typical structure and dependencies between these modules:

  1. Consumer is a layer that directly “works” with the client. For the server app it is going to be web API controller and for app frontend it is a web page that directly interacts with the customer. Consumer should be “thin”, it fully depends on the business logic implemented in a service and contains only code needed for handling the calls to the service:

    1. Calling service

    2. Handling result

  2. Service is a core of the whole project and everything “spins” around it. Services contain all high-level logic or, so called the business logic and logic needed to help consumer with the presentation. Thus, service is a business logic + presenter. Once it is called by consumer, its’ responsibility is to call one or multiple providers, get the data and process it, translate it to the consumer. The most unit test focus is on these services. It depends on data access layer interface but not its implementation. Service responsibilities are:

    1. Calls providers to retrieve or modify data

    2. Transforms results from data model structures to the model structures understandable by consumer (shared model)

  3. Provider is responsible for the low-level data handling operations (e.g SQL or LINQ queries, working with files, network requests or sending emails). Provider provides an abstraction (the set of interfaces); it is called by service via interface and should never be accessed directly. The dependencies go from top to bottom: Consumer is dependent on Service and Service is dependent on Provider. Thus, providers should be the most stable components and any changes to them should be done with extra consideration. On the other hand, consumer can be changed as frequently as needed.

Unit testing

The focus of testing is on services. The following diagram shows that approach when service logic can be isolated by provider mocks for providers.

Provider’s testing can be more complicated, and some approaches will be described in the future articles.

Next

In the following articles I will dive deep into how to apply the three-tier architecture for specific use cases: