Add a Backend Module

Step-by-step guide to creating a new backend module with Clean Architecture and CQRS.

This tutorial covers creating a complete backend module using Clean Architecture with CQRS. You'll define the entity, create commands and queries, implement the repository, register with DI, add the controller, and configure module registration.

Prerequisites

  • .NET 10 SDK installed
  • Oracle or SQL Server database configured
  • Understanding of CQRS, AstraFlow mediator, and Clean Architecture

Step-by-Step Guide

  1. 1. Create Project Structure

    Set up the module's class library projects following Clean Architecture layers (Domain, Application, Infrastructure, API).
  2. 2. Define Domain Entity

    Create the entity class inheriting from AuditableEntity with proper validation.
  3. 3. Create Commands

    Implement Create, Update, and Delete commands with FluentValidation and handlers.
  4. 4. Create Queries

    Implement GetById and GetPaged queries with response DTOs.
  5. 5. Implement Repository

    Create the EF Core repository with the generic repository base class.
  6. 6. Register with DI

    Configure dependency injection for the module's services, repositories, and SCRIPE request handlers.
  7. 7. Add Controller

    Create the API controller with RESTful endpoints, Swagger docs, and authorization attributes.
  8. 8. Module Registration

    Register the module in the gateway and add database migration for the new entity.

Project Structure

Backend Module — Project StructureText
src/Modules/Inventory/
├── Inventory.Application/
│   ├── Commands/
│   │   ├── CreateProduct/
│   │   │   ├── CreateProductCommand.cs
│   │   │   ├── CreateProductHandler.cs
│   │   │   └── CreateProductValidator.cs
│   │   └── UpdateProduct/
│   │       ├── UpdateProductCommand.cs
│   │       ├── UpdateProductHandler.cs
│   │       └── UpdateProductValidator.cs
│   ├── Queries/
│   │   ├── GetProducts/
│   │   │   ├── GetProductsQuery.cs
│   │   │   └── GetProductsHandler.cs
│   │   └── GetProductById/
│   │       ├── GetProductByIdQuery.cs
│   │       └── GetProductByIdHandler.cs
│   ├── DTOs/
│   │   ├── ProductDto.cs
│   │   └── CreateProductDto.cs
│   └── Mappings/
│       └── ProductMappingRule.cs
│
├── Inventory.Domain/
│   ├── Entities/
│   │   └── Product.cs
│   ├── Events/
│   │   ├── ProductCreatedEvent.cs
│   │   └── ProductUpdatedEvent.cs
│   └── Interfaces/
│       └── IProductRepository.cs
│
├── Inventory.Infrastructure/
│   ├── Data/
│   │   ├── InventoryDbContext.cs
│   │   └── Configurations/
│   │       └── ProductConfiguration.cs
│   ├── Repositories/
│   │   └── ProductRepository.cs
│   └── DependencyInjection.cs
│
└── Inventory.API/
    └── Controllers/
        └── ProductsController.cs

Domain Entity

Domain/Entities/Product.csC#
/// <summary>
/// Product entity — inherits from AuditableEntity for automatic
/// CreatedAt/UpdatedAt/DeletedAt tracking and tenant awareness.
/// </summary>
public class Product : AuditableEntity, ITenantAwareEntity
{
    public string Name { get; set; } = string.Empty;
    public string SKU { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public string Category { get; set; } = string.Empty;
    public bool IsActive { get; set; } = true;
    
    // ITenantAwareEntity
    public Guid TenantId { get; set; }
    public Tenant Tenant { get; set; } = null!;
    
    // Domain Events
    public void MarkCreated()
    {
        AddDomainEvent(new ProductCreatedEvent(Id, Name, SKU));
    }
}

Commands & Handlers

Commands/CreateProduct/CreateProductCommand.csC#
public record CreateProductCommand(
    string Name,
    string SKU,
    decimal Price,
    int Quantity,
    string Category
) : ICommand<ProductDto>;

Dependency Injection

Infrastructure/DependencyInjection.csC#
public static class DependencyInjection
{
    public static IServiceCollection AddInventoryModule(
        this IServiceCollection services, IConfiguration config)
    {
        // DbContext
        services.AddDbContext<InventoryDbContext>(options =>
            options.UseOracle(config.GetConnectionString("DefaultConnection")));

        // Repositories
        services.AddScoped<IProductRepository, ProductRepository>();

        // Request handlers and validators are scanned centrally by AddCoreApplication
        // from the module marker types collected in API ModuleRegistration.

        return services;
    }
}

API Controller

API/Controllers/ProductsController.csC#
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
[PermissionRequired("products.view")]
public class ProductsController : ControllerBase
{
    private readonly ISender _sender;

    [HttpGet]
    public async Task<IActionResult> GetProducts(
        [FromQuery] GetProductsQuery query)
        => HandleResult(await _sender.Send(query));

    [HttpPost]
    [PermissionRequired("products.create")]
    public async Task<IActionResult> CreateProduct(
        [FromBody] CreateProductCommand command)
        => HandleResult(await _sender.Send(command));

    [HttpPut("{id}")]
    [PermissionRequired("products.update")]
    public async Task<IActionResult> UpdateProduct(
        Guid id, [FromBody] UpdateProductCommand command)
        => HandleResult(await _sender.Send(command with { Id = id }));

    [HttpDelete("{id}")]
    [PermissionRequired("products.delete")]
    public async Task<IActionResult> DeleteProduct(Guid id)
        => HandleResult(await _sender.Send(new DeleteProductCommand(id)));
}

Module Registration

Host/API/Extensions/ModuleRegistration.cs — Add ModuleC#
// Add to ModuleRegistration.RegisterModules:
if (isMonolith || moduleName.Equals("Inventory", StringComparison.OrdinalIgnoreCase))
{
    mediatorAssemblies.Add(typeof(Inventory.Application.DependencyInjection));
    builder.Services.AddInventoryModule(builder.Configuration); // Register module DI
}