In this article, we will discuss Modularizing Web Applications using Modular Architecture in ASP.NET Core. We will go through Monolith Architecture’s various cons and pros and work on how to build monolith applications in a better way. Let’s call it Modular Monolith Architecture. Towards the latter section of the article, we will also build a sample solution that follows Modular Architecture. I have also attached the repository link to the ongoing real-life implementation of Modular Architecture in an Open Source Point of Sales System later in this article. Let’s get started.
You can find the complete source code of this implementation here.
Background
Having quite a lot of experience with POS & Inventory Management systems, we set out to build a full-fledged open-source system using our favorite tech stack and tools. Modular development was a prime requirement for us when we got started. Adapting to a Microservice architecture was the first choice we had. But given the complexities of the mentioned architecture, we decided to stay away from it at least for the starting. We named our product fluentpos.
There actually was no real need to implement microservices. fluentpos was meant to help businesses in their day-to-day activities. For this, a well-designed monolith application would also do the trick. We were clear to have the API and UI separated, to give opportunities to multiple client apps in the future.
The WebAPI application had to be highly modular to improve the development experience. This needed breaking down the application to logical modules like Identity, Catalog, Sales, Inventory. Each of these modules has its own controllers/interfaces / dbContext. As for the DB providers, Postgres / MSSQL will be used. One module cannot directly talk to the other module nor modify its table. CrossCutting concerns would use interfaces/events. And yes, domain events are also included in the project using mediator Handler. Each of the modules follows a clean architecture design / Onion / Hex.
Essentially we would end up with a Solution that contains MultipleModules where-in each of the modules implements a variation of Clean / Onion Architecture.
This was the reason that I got started writing articles about this architecture. Let’s go further.
Monolith Architecture
Monolith Architecture is probably used for 80% of the applications in existence. There is a high chance that you are already using this in your ongoing projects. This is a very straightforward approach to architecting applications where there is exactly one point of entry into the application. This means that a single deployment is often enough to get things into production. Imagine the application as a single block of code, internally split into multiple concerns (Business, Data Access, Infrastructure).
Below is an illustration of a simple monolith architecture.
You can call it 3-Layer Architecture or N-Layer Architecture, it’s all the same. Ultimately this is a Monolith. It’s a single application code block without dependencies on other applications (exes).
Reasons to Avoid Microservices - For Now.
Microservices are definitely the best architecture you can implement for large-scale applications. But the trend we see nowadays is that even mid-scale applications tend to use it. Is it really required? Most of the time, the answer is NO.
Read about Microservice Architecture in ASP.NET Core with API Gateway here.
Although it’s a scalable architecture, it comes with a lot of cons. There is actually no ideal solution that exists without a con. It’s all about the requirement and how comfortable you feel with the implementation. Message buses, Consumers, Publishers, Multiple deployments, are a few of the complexities that arise with Microservices.
The Need to Build Better Monoliths
Transitioning to Microservices is a real painful process given that you educate the entire team of all the required basics at the practical level. Sticking to a well-built monolith is a viable solution for most of the products and code bases. When your product’s user base explodes, that is when you would actually need to transition to a Microservices Approach. But until then, design your application in such a way that it mimics Microservices yet keeps the simplicity and goodness of Monoliths. Makes sense, yeah?
What is Modular Monolithic Architecture?
Modular Monolith Architecture is a software design in which a monolith is made better and modular with importance to re-use components/modules. It’s often quite easy to transition from a Monolith Architecture to Modular Monolith.
Here is an illustration:
The main idea here is to build a better Monolith Solution.
- API / Host - A very thin Rest API / Host Application that is responsible for registering the controllers/services of other modules into the service container.
- Modules - A logical block of the business unit. For example, Sales. Everything that is related to Sales can be found here. We will walk through the definition of a module in the next section.
- Shared Infrastructure - Application-Specific Interfaces and implementations are found here for other modules to consume. This includes Middlewares, Data Access providers, and so on.
- Finally a Database. Note that you have the flexibility to use multiple databases, i.e one database per module.But it ultimately depends on how you would want to design your application.
You can see that there is not much deviation from a standard Monolith implementation. The basic recipe is to Split your Application into multiple smaller applications/modules and make them follow clean architecture principles.
Definition of a Module
- A module is a logical unit of the business requirement. In a Point of Sales application, sales, customers, and Inventory are a few examples of Modules.
- Each module will have a DBContext and can access only the assigned table/entities.
- One module should never depend on any other module. It can depend on Abstraction Interfaces that are present in Shared Application Projects.
- Each module has to follow a domain-driven architecture
- Every module will be further split into API, Core, and Infrastructure projects to enforce Clean Onion Architecture.
- Cross Module communication can happen only via Interfaces/events/in-memory bus. Cross Module DB Writes should be kept minimal or avoided completely.
To get a better understanding, let’s see an actual module from the fluentpos project and examine its responsibility.
- Modules.Catalog - Contains the API Controllers needed for the module.
- Modules.Catalog.Core - Contains Entities, Abstractions, CQRS handlers, and everything needed for the module to function independently.
- Modules.Catalog.Infrastructure - Consists of DbContexts and Migrations. This project depends on the Core for abstractions.
You will get more idea on this when to start to build an application later in this article.
Benefits of Modular Architecture in ASP.NET Core
- Clear Separation of Concerns
- Easily Scalable
- Lower complexity compared to Microservices
- Low operational / deployment costs.
- Reusability
- Organized Dependencies
Cons of Modular Architecture when compared to Microservices
- Not Multi-technology compatible.
- Horizontal Scaling can be a concern. But this can be managed via load balancers.
- Since Interprocess Communication is used, messages may be lost during Application Termination. Microservices combat this issue by using external messaging brokers like Kafka, RabbitMQ. (You can still use message brokers in Monoliths, but let’s keep things simple)
Examining fluentpos Project Structure
- API
- Bootstrapper / Host
- Modules
- Catalog
- Controllers
- Core
- Entities
- Interfaces
- Exceptions
- CQRS Handlers - Commands & Queries
- Infrastructure
- Context
- Migrations
- Other Modules
- Catalog
- Shared
- Core
- Interfaces
- DTOs
- Infrastructure
- Middlewares
- Persistence Registrations
- Core
Building Modular Architecture In ASP.NET Core
What we will build
We will be building a simple application that demonstrates the implementation of Modular Architecture in ASP.NET Core. We will not be building a full-blown application in this article, as it may require lots of explanation. I plan to build a framework / Nuget package later to help you generate Modular Solutions for your upcoming project (Any thoughts? Leave your comments below!). But for now, let’s build a very basic implementation. Here is what you can expect.
- Controller registration from other Class libraries
- CQRS using MediatR
- MSSQL
- Migrations
- Catalog Module
- Customer Module
- Shared DTOs
Architectural Assumptions
To keep things simple, we will assume that Entity Framework Core will be our default DB Abstraction provider and will go strong for another 10+ years. In this way, we can avoid the Repository pattern that usually tends to make our codebase larger.
Getting Started
Let’s start by creating a new Blank Solution in Visual Studio. PS, I will be using Visual Studio 2019 Community for this demonstration.
Project Structure
Within the newly created solution, let’s create a new folder Host and add in an ASP.NET Core 5.0 WebAPI Application. Remove all the boilerplate code that comes along with the WebAPI.
With that done, let me add a few other C# library projects. You can follow the similar folder structure as demonstrated in the screenshot below.
As mentioned,
- API will hold all the service / controller registration logics and nothing else.
- Module.Catalog & Module.People will contain the API controllers only,which will be picked up by the API Project.
- Module.Catalog.Core & Module.People.Core will contain the Entity models, interfaces specific to the module, Mediatr Handlers and so on.
- Module.Catalog.Infrastructure & Module.People.Infrastructure will mainly hold the module specific DBContext,Migrations, SeedData and Service implementation if any.
- Shared.Core will have MediatR Behaviors, Common Service Implementations / Interfaces and basically everything that has to be shared across the application.
- Shared.Models is where you will have to add in the Request /Response classes.Note that this project can be used for any C# Client applications as well.
- Finally, Shared.Infrastructure is where you would want your middlewares, utilities and specify which Database Provider to use for the entire application.
With the structure ready, let’s add in the required extensions and controllers.
Controller Registrations
The first challenge is if you place the controllers in Module.Catalog & Module.People projects, how will the API project recognize it and add the routing required? Thus we need a way to make the API project use controllers that are in separate projects but uses the standard naming conventions of API Controllers.
Before all that, you would want to add the following to the Shared.Infrastructure project file to ensure that we have access to the AspNetCore Framework references and classes.
Next, create a Controllers folder under Shared.Infrastructure and add a new class InternalControllerFeatureProvider.
Now, this class will be responsible for adding controllers that are in different projects. We will have to register this class into the service container of our host ASP.NET Core application.
Create a new folder under Shared.Infrastructure, Extensions and add a new class ServiceCollectionExtensions.cs.
Now, let’s navigate to the API project / Startup / ConfigureServices method and add the following.
Make sure that the API Project has reference to the Shared Infrastructure, Module.Catalog, Module.People projects too. (Important)
Let’s add a controller to our Modules.Catalog. Create a new folder Controllers under the Modules.Catalog project and add in a new Controller, BrandsController. We are just adding this controller to ensure that the API project is able to detect the controller in the Module project.
IMPORTANT - You might be seeing a lot of unresolved dependencies now. It’s important to add the following project references for each module going forward.
- Module.Catalog.Core should have a referece to Shared.Core
- Module.Catalog.Infrastructure should have a referece to Shared.Infrastructure & Module.Catalog.Core
- Module.Catalog should have a referece to Module.Catalog.Core and Module.Catalog.Infrastructure
- Shared.Infrastructure should have a reference to Shared.Core
- Shared.Core should depend on Shared.Models
Make sure to add similar dependencies for the People Module as well.
PS, these are few crucial dependencies stated by Clean Architecture principles (Onion). So make sure that you get them all right.
With that done, let’s run the project and check if the BrandController comes up in Swagger.
So, that’s done. Now let’s connect the application to a database, in a modular way.
Persistence
As mentioned earlier, we will be using Entity Framework Core as the DB Abstraction in this project.
Let’s get started by adding the Brand Model Entity. Open up the Modules.Catalog.Core and add a new folder, Entities. Here create a new class and name it Brand.
Since we decided to add separate DBContexts for each module, it makes sense to add in a common DBContext first, then inherit it as the base class, yeah?
First, Navigate to Shared.Infrastructure Project and add a new Folder, Persistence. Here let’s add a new class named ModuleDbContext. Remember, this will be the base of all the DBContext classes that you will be creating moving forward in each and every module.
It’s important to note that we are using Schemas to make a logical separation between the database tables as well. For example, tables associated with Catalog module will be named Catalog.Brand , Catalog.Products and so on. You get the idea, yeah?
Next, let’s add the DBContext specific to this Module. Remember, no other modules can have access to the Brand Table other than the Catalog Module. This is made sure by creating separate DbContexts for each Module.
Navigate to Module.Catalog.Core and create a new folder Abstractions. Here is where you will have to place the interfaces to achieve Dependency Inversion. This is the whole essence of Onion Architecture, yeah? In this folder, let’s add a new interface and name it ICatalogDbContext.
Next, Navigate to Modules.Catalog.Infrastructure and add a new folder, Persistence. Here, add in a new class, CatalogDbContext which inherits from the catalog DB context interface and the module Dbcontext base class.
Note that anything related to the data access of the Catalog module will have to be put here.
Note that we specify the schema name as Catalog here. Also, make sure to inherit from our common ModuleDbContext and the interface specific to our current module.
Let’s install the required packages now.
For Shared.Infrastructure project, install the following packages.
For the API Project, install the following.
Now comes the interesting part of adding Database Provider extensions. We know that we will be using MSSQL as the DB provider in this implementation. But let’s build a somewhat flexible system, where it can be switched to PostgreSQL or other providers easily. Ideally, this solution should be present in a common project for other modules to use easily. That’s right, navigate to Shared.Infrastructure project and open up Extensions/ServiceCollectionExtensions.cs file. Here, add in the following Extension methods.
- Line 3 - Get the connection string defined in the appsettings.json of the API project. Note that we will be adding the connection string in the next section.
- Line 4 - Calls the extension method specific for MSSQL. You could write a new extension for other DB Providers as well. You get the idea, yeah?
- Line 9 - Adds the passed DbContext to the service container using the MSSQL package of EFCore. Make sure that you have already installed it.
- Line 12 - Updates the database using the latest available Migrations.
Open up the appsettings.json of the API Project and add in the following.
Next, we need to make sure that each of the modules uses these extensions. Navigate to Module.Catalog.Infrastructure and add a new Folder, Extensions. Here add a new static class, ServiceCollectionExtensions
Next, in the Module.Catalog.Core project, add an Extensions folder and add ServiceCollectionExtensions.cs. We will need this while implementing the MediatR handlers.
Next, we need an extension for each module, that can be read by the API Project for registering the required services. Navigate to Module.Catalog and add a new class, ModuleExtensions. Here we will be adding the other extensions like AddCatalogCore and AddCatalogInfrastructure.
Finally, go to the API Project / Startup / ConfigureServices method and add in the following. Make sure that you have added the reference to Module.Catalog as well.
That’s everything you need to do to set up Database Access in a Modular fashion. As the last step, let’s add the required migrations and check if the tables are created as expected.
On Visual Studio, Right-click the Module.Catalog.Infrastructure and click on ‘Open in Terminal’. Run the following command.
This would create the Migrations in the following folder.
As per our code, as soon as the application runs, the required tables will be created. Let’s test.
There you go, the tables are created exactly as we wanted.
Adding MediatR Handlers and Controllers
As the final part of this implementation, we will be adding the required MediatR handlers and API controllers. To keep the article minimal, we will just be adding the ‘GetAll’ and ‘Register’ endpoints for the Brands Entity.
Let’s start by adding 2 new folders under the Module.Catalog.Core project and name it Queries & Commands respectively.
Under the Queries folder, add in a new class and name it GetAllBrandsQuery.
This is the simplified version of the MediatR handler. For more details, you can refer to CQRS with MediatR in ASP.NET Core – Ultimate Guide, where we go in-depth about MediatR Handlers and CQRS.
Similarly, add a new class, RegisterBrandCommand under the Register folder.
Now that we have taken care of the Handlers, let’s wire them up with our BrandsController. Open up the BrandsController and add in the following.
That’s it for the implementation. Let’s run the application and verify the swagger endpoints.
The POST method allows you to add in new brands.
And using the Get method, we can fetch all the brands records from the DB.
That’s it for the article. Do you want me to write another article to build upon this same solution and add extra infrastructure like Middlewares, Logging, and so on? Do let me know in the comments section. Modularizing application is definitely the way to go for a cleaner and scalable project.
fluentPOS - Modular Architecture Real Life Implementation
Moving forward, fluentpos will be our next full-fledged open-source implementation of Modular Architecture along with Angular Material Frontend and probably a Mobile App with MAUI too! Here is the repository - https://github.com/fluentpos/fluentpos. This project should be completed in about 2-3 months’ time from now. Do leave behind your stars for this repository as well.
Summary
In this article, we have learned about Modular Monolith Architecture in ASP.NET Core and also learned how to build it up right from scratch! We also learned about how it compares to Microservices and Monolith Architecture. You can also find the complete source code on my Github here. Have any suggestions or questions? Feel free to leave them in the comments section below. Thanks and Happy Coding! 😀