ASP.NET Certifications - Backend for Frontend pattern - Challenges and Pitfalls - Exams of ASP.NET - What is a Modular Monolith?

The URI space – Modular Monolith

The modules of this application follow the previously discussed URI space: /{module name}/{module space}. Each module has a Constants file at its root that looks like this:

namespace REPR.Baskets;
public sealed class Constants
{
    public const string ModuleName = nameof(Baskets);
}

We use the ModuleName constant in the {module name}ModuleExtensions files to set the URI prefix and tag the endpoints like this:

namespace REPR.Baskets;
public static class BasketModuleExtensions
{
    public static IEndpointRouteBuilder MapBasketModule(this IEndpointRouteBuilder endpoints)
    {
        _ = endpoints
            .MapGroup(Constants.ModuleName.ToLower())
            .WithTags(Constants.ModuleName)
            .AddFluentValidationFilter()
            // Map endpoints
            .MapFetchItems()
            .MapAddItem()
            .MapUpdateQuantity()
            .MapRemoveItem()
        ;
        return endpoints;
    }
}

With this in place, both modules self-register themselves in the correct URI space.

We can apply these types of conventions in many different ways. In this case, we opted for simplicity, which is the most error-prone, leaving the responsibility to the mercy of each module. With a more framework-oriented mindset, we could create a strongly typed module contract that gets loaded automatically, like an IModule interface. The aggregator could also create the root groups and enforce the URI space.

Next, we explore the data space.

The data space

Since we are following the microservices architecture tenets and each module should own its data, we must find a way to ensure our data contexts do not conflict.The project uses the EF Core in-memory provider to develop locally. For production, we plan on using SQL Server. One excellent way to ensure our DbContext classes do not conflict with each other is to create one database schema per context. Each module has one context, so one schema per module. We don’t have to overthink this; we can reuse the same idea as the URI and leverage the module name. So, each module will group its tables under the {module name} schema instead of dbo (the default SQL Server schema).

We can apply different security rules and permissions to each schema in SQL Server, so we could craft a very secure database model by expanding this. For instance, we could employ multiple users possessing minimal privileges, utilize different connection strings within the modules, etc.

In code, doing this is reflected by setting the default schema name in the OnModelCreating method of each DbContext. Here’s the ProductContext class:

namespace REPR.Products.Data;
public class ProductContext : DbContext
{
    public ProductContext(DbContextOptions<ProductContext> options)
        : base(options) { }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.HasDefaultSchema(Constants.ModuleName.ToLower());
    }
    public DbSet<Product> Products => Set<Product>();
}

The preceding code makes all ProductContext’s tables part of the products schema. We then apply the same for the basket module:

namespace REPR.Baskets.Data;
public class BasketContext : DbContext
{
    public BasketContext(DbContextOptions<BasketContext> options)
        : base(options) { }
    public DbSet<BasketItem> Items => Set<BasketItem>();
    public DbSet<Product> Products => Set<Product>();
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.HasDefaultSchema(Constants.ModuleName.ToLower());
        modelBuilder
            .Entity<BasketItem>()
            .HasKey(x => new { x.CustomerId, x.ProductId })
        ;
    }
}

The preceding code makes all BasketContext’s tables part of the baskets schema.Due to the schema, both contexts are safe from hindering the other. But wait! Both contexts have a Products table; what happens then? The catalog module uses the products.products table, while the basket module uses the baskets.products table. Different schema, different tables, case closed!

We can apply these notions to more than Modular Monolith as it is general SQL Server and EF Core knowledge.

If you are using another relational database engine that does not offer schema or a NoSQL database, you must also think about this. Each NoSQL database has different ways to think about the data, and it would be impossible to cover them all here. The important piece is to find a discriminator that segregates the data of your modules. At the limit, it can even be one different database per module; however, this increases the operational complexity of the application.Next, we explore the message broker.

Leave a Reply

Your email address will not be published. Required fields are marked *