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. Create Project Structure
Set up the module's class library projects following Clean Architecture layers (Domain, Application, Infrastructure, API).2. Define Domain Entity
Create the entity class inheriting from AuditableEntity with proper validation.3. Create Commands
Implement Create, Update, and Delete commands with FluentValidation and handlers.4. Create Queries
Implement GetById and GetPaged queries with response DTOs.5. Implement Repository
Create the EF Core repository with the generic repository base class.6. Register with DI
Configure dependency injection for the module's services, repositories, and SCRIPE request handlers.7. Add Controller
Create the API controller with RESTful endpoints, Swagger docs, and authorization attributes.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.csDomain 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
}