README
This commit is contained in:
584
README.md
Normal file
584
README.md
Normal file
@@ -0,0 +1,584 @@
|
||||
# Mccn Modular Monolith
|
||||
|
||||
A .NET 10 modular monolith template using CQRS, Domain-Driven Design, Keycloak authentication, and a full observability stack.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Infrastructure Services](#infrastructure-services)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Running the Tests](#running-the-tests)
|
||||
- [EF Core Migrations](#ef-core-migrations)
|
||||
- [Adding a New Module](#adding-a-new-module)
|
||||
- [Adding a Feature to an Existing Module](#adding-a-feature-to-an-existing-module)
|
||||
- [Key Patterns and Conventions](#key-patterns-and-conventions)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The solution is a **modular monolith**: a single deployable unit composed of fully isolated modules. Each module owns its domain, application logic, database schema, and HTTP endpoints. Modules cannot directly reference each other — this boundary is enforced by architecture tests.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Mccn.Api │
|
||||
│ (entry point / host) │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
│ wires up
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌────────────┐ ┌── ─ ─ ─ ─ ┐
|
||||
│ Users │ │ Hello │ Your Module
|
||||
│ Module │ │ Module │ └── ─ ─ ─ ─ ┘
|
||||
└────────────┘ └────────────┘
|
||||
```
|
||||
|
||||
### Layers per Module
|
||||
|
||||
Each module is split into four projects following a strict dependency direction:
|
||||
|
||||
```
|
||||
Presentation ──► Application ──► Domain
|
||||
│
|
||||
Infrastructure ─────────┘
|
||||
```
|
||||
|
||||
| Layer | Responsibility |
|
||||
|------------------|-----------------------------------------------------------|
|
||||
| **Domain** | Entities, value objects, domain errors, repository interfaces |
|
||||
| **Application** | Use cases (commands & queries), validators, service abstractions |
|
||||
| **Infrastructure** | EF Core DbContext, repositories, external HTTP clients (e.g. Keycloak) |
|
||||
| **Presentation** | Minimal API endpoints |
|
||||
|
||||
### Cross-Cutting Concerns
|
||||
|
||||
| Concern | Technology |
|
||||
|------------------|-----------------------------------------------------------|
|
||||
| Authentication | Keycloak (JWT Bearer / OIDC) |
|
||||
| Logging | Serilog → Seq |
|
||||
| Distributed Tracing | OpenTelemetry → Jaeger |
|
||||
| Caching | Redis (falls back to in-memory if Redis is unavailable) |
|
||||
| Validation | FluentValidation via MediatR pipeline behavior |
|
||||
| Error handling | Global exception handler + `Result<T>` pattern |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||
- An IDE that supports `.slnx` solution files (Rider, Visual Studio 2022+)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Start the infrastructure
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This starts five containers. Wait ~30 seconds for Keycloak to finish importing the realm before starting the application.
|
||||
|
||||
### 2. Run the application
|
||||
|
||||
```bash
|
||||
dotnet run --project src/API/Mccn.Api/Mccn.Api.csproj
|
||||
```
|
||||
|
||||
The API is available at `http://localhost:5000`. Swagger UI is at `http://localhost:5000/swagger`.
|
||||
|
||||
On first run in Development mode the database schema is automatically created via EF Core.
|
||||
|
||||
### 3. Test the endpoints
|
||||
|
||||
Open `src/API/Mccn.Api/Mccn.Api.http` in your IDE (JetBrains HTTP Client or VS Code REST Client). The file contains ready-to-run requests for every endpoint, including a login request that acquires a Keycloak token and injects it into subsequent authenticated requests.
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure Services
|
||||
|
||||
| Service | Purpose | Local URL / Port |
|
||||
|------------|---------------------|-------------------------------------------|
|
||||
| PostgreSQL | Database | `localhost:5432` |
|
||||
| Keycloak | Identity provider | `http://localhost:18080` (admin: `admin` / `admin`) |
|
||||
| Seq | Log aggregation | `http://localhost:9081` |
|
||||
| Redis | Distributed cache | `localhost:6379` |
|
||||
| Jaeger | Distributed tracing | `http://localhost:16686` |
|
||||
|
||||
> **Note:** If Redis, Seq, or Jaeger are not running, the application still starts. Redis gracefully falls back to an in-memory cache. Seq and Jaeger are fire-and-forget sinks. Keycloak is required at startup only when a protected endpoint is first called (the JWT metadata is fetched lazily). The **database is the only hard dependency at startup**.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mccn-modular-monolith/
|
||||
├── docker-compose.yml
|
||||
├── Directory.Build.props # Global: net10.0, nullable, implicit usings
|
||||
├── .files/
|
||||
│ └── mccn-realm-export.json # Keycloak realm imported on container start
|
||||
│
|
||||
├── src/
|
||||
│ ├── API/
|
||||
│ │ └── Mccn.Api/ # Host: wires all modules, middleware, migrations
|
||||
│ │
|
||||
│ ├── Common/
|
||||
│ │ ├── Mccn.Common.Domain/ # Entity<T>, Result<T>, Error
|
||||
│ │ ├── Mccn.Common.Application/ # ICommand, IQuery, ValidationBehavior
|
||||
│ │ ├── Mccn.Common.Infrastructure/ # Auth, caching, observability, exception handling
|
||||
│ │ └── Mccn.Common.Presentation/ # IEndpoint, EndpointExtensions
|
||||
│ │
|
||||
│ └── Modules/
|
||||
│ ├── Hello/ # Example module (no database)
|
||||
│ │ ├── Mccn.Modules.Hello.Application/
|
||||
│ │ ├── Mccn.Modules.Hello.Infrastructure/
|
||||
│ │ ├── Mccn.Modules.Hello.Presentation/
|
||||
│ │ ├── Mccn.Modules.Hello.ArchitectureTests/
|
||||
│ │ └── Mccn.Modules.Hello.IntegrationTests/
|
||||
│ │
|
||||
│ └── Users/ # Authentication module
|
||||
│ ├── Mccn.Modules.Users.Domain/
|
||||
│ ├── Mccn.Modules.Users.Application/
|
||||
│ ├── Mccn.Modules.Users.Infrastructure/
|
||||
│ ├── Mccn.Modules.Users.Presentation/
|
||||
│ ├── Mccn.Modules.Users.ArchitectureTests/
|
||||
│ ├── Mccn.Modules.Users.IntegrationTests/
|
||||
│ └── Mccn.Modules.Users.UnitTests/
|
||||
│
|
||||
└── test/
|
||||
└── Mccn.ArchitectureTests/ # Global: verifies modules don't reference each other
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running the Tests
|
||||
|
||||
### All tests
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
### A specific test project
|
||||
|
||||
```bash
|
||||
dotnet test src/Modules/Users/Mccn.Modules.Users.ArchitectureTests/
|
||||
dotnet test src/Modules/Users/Mccn.Modules.Users.UnitTests/
|
||||
dotnet test src/Modules/Users/Mccn.Modules.Users.IntegrationTests/
|
||||
```
|
||||
|
||||
### Notes on integration tests
|
||||
|
||||
Integration tests spin up real Docker containers using **Testcontainers**. Docker Desktop must be running. The first run pulls the container images and may take a few minutes. Subsequent runs use cached layers and are much faster.
|
||||
|
||||
Each integration test module has its own `IntegrationTestWebAppFactory` that starts the containers it actually needs:
|
||||
|
||||
| Test project | Containers started |
|
||||
|-----------------------|-----------------------------------|
|
||||
| Users IntegrationTests | PostgreSQL, Redis, Keycloak |
|
||||
| Hello IntegrationTests | PostgreSQL, Redis |
|
||||
|
||||
---
|
||||
|
||||
## EF Core Migrations
|
||||
|
||||
Migrations live inside the module's Infrastructure project. Each module has its own `DbContext` and its own schema.
|
||||
|
||||
### Add a migration (Users module)
|
||||
|
||||
Run this from the solution root:
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add <MigrationName> \
|
||||
--project src/Modules/Users/Mccn.Modules.Users.Infrastructure/ \
|
||||
--startup-project src/API/Mccn.Api/ \
|
||||
--context UsersDbContext
|
||||
```
|
||||
|
||||
### Apply migrations manually
|
||||
|
||||
```bash
|
||||
dotnet ef database update \
|
||||
--project src/Modules/Users/Mccn.Modules.Users.Infrastructure/ \
|
||||
--startup-project src/API/Mccn.Api/ \
|
||||
--context UsersDbContext
|
||||
```
|
||||
|
||||
In Development mode, `ApplyMigrationsAsync()` in `Program.cs` runs pending migrations automatically on startup. In other environments you are expected to run migrations explicitly (e.g. as part of a deployment pipeline).
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Module
|
||||
|
||||
Follow these steps to add a module called **Orders** as an example.
|
||||
|
||||
### 1. Create the four layer projects
|
||||
|
||||
```bash
|
||||
# From the solution root
|
||||
dotnet new classlib -n Mccn.Modules.Orders.Domain -o src/Modules/Orders/Mccn.Modules.Orders.Domain
|
||||
dotnet new classlib -n Mccn.Modules.Orders.Application -o src/Modules/Orders/Mccn.Modules.Orders.Application
|
||||
dotnet new classlib -n Mccn.Modules.Orders.Infrastructure -o src/Modules/Orders/Mccn.Modules.Orders.Infrastructure
|
||||
dotnet new classlib -n Mccn.Modules.Orders.Presentation -o src/Modules/Orders/Mccn.Modules.Orders.Presentation
|
||||
```
|
||||
|
||||
### 2. Wire the project references
|
||||
|
||||
```bash
|
||||
# Domain depends on Common.Domain
|
||||
dotnet add src/Modules/Orders/Mccn.Modules.Orders.Domain/ reference \
|
||||
src/Common/Mccn.Common.Domain/
|
||||
|
||||
# Application depends on Common.Application + Domain
|
||||
dotnet add src/Modules/Orders/Mccn.Modules.Orders.Application/ reference \
|
||||
src/Common/Mccn.Common.Application/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Domain/
|
||||
|
||||
# Infrastructure depends on Common.Infrastructure + Application + Domain
|
||||
dotnet add src/Modules/Orders/Mccn.Modules.Orders.Infrastructure/ reference \
|
||||
src/Common/Mccn.Common.Infrastructure/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Application/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Domain/
|
||||
|
||||
# Presentation depends on Common.Presentation + Application
|
||||
dotnet add src/Modules/Orders/Mccn.Modules.Orders.Presentation/ reference \
|
||||
src/Common/Mccn.Common.Presentation/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Application/
|
||||
```
|
||||
|
||||
### 3. Add projects to the solution
|
||||
|
||||
```bash
|
||||
dotnet sln add \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Domain/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Application/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Infrastructure/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Presentation/
|
||||
```
|
||||
|
||||
### 4. Reference the module from the API host
|
||||
|
||||
```bash
|
||||
dotnet add src/API/Mccn.Api/ reference \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Infrastructure/ \
|
||||
src/Modules/Orders/Mccn.Modules.Orders.Presentation/
|
||||
```
|
||||
|
||||
### 5. Create the DependencyInjection extension methods
|
||||
|
||||
**Infrastructure** (`DependencyInjection.cs`):
|
||||
|
||||
```csharp
|
||||
namespace Mccn.Modules.Orders.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddOrdersModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Register DbContext, repositories, etc.
|
||||
return services;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Presentation** (`DependencyInjection.cs`):
|
||||
|
||||
```csharp
|
||||
namespace Mccn.Modules.Orders.Presentation;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddOrdersPresentationServices(
|
||||
this IServiceCollection services)
|
||||
{
|
||||
return services;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Register the module in Program.cs
|
||||
|
||||
```csharp
|
||||
// Register modules.
|
||||
builder.Services.AddUsersModule(builder.Configuration);
|
||||
builder.Services.AddHelloModule(builder.Configuration);
|
||||
builder.Services.AddOrdersModule(builder.Configuration); // add this
|
||||
|
||||
// Register endpoints.
|
||||
builder.Services.AddUsersPresentationServices();
|
||||
builder.Services.AddHelloPresentationServices();
|
||||
builder.Services.AddOrdersPresentationServices(); // add this
|
||||
```
|
||||
|
||||
Also register the module's Application assembly with MediatR in `Program.cs`:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddApplication(
|
||||
typeof(RegisterUserCommand).Assembly,
|
||||
typeof(SayHelloQuery).Assembly,
|
||||
typeof(YourOrderCommand).Assembly); // add this
|
||||
```
|
||||
|
||||
### 7. Add a DbContext (if the module has a database)
|
||||
|
||||
Create `OrdersDbContext.cs` inside Infrastructure:
|
||||
|
||||
```csharp
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Mccn.Modules.Orders.Infrastructure.Database;
|
||||
|
||||
public sealed class OrdersDbContext(DbContextOptions<OrdersDbContext> options) : DbContext(options)
|
||||
{
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema(Schemas.Orders);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrdersDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Schemas.cs
|
||||
namespace Mccn.Modules.Orders.Infrastructure.Database;
|
||||
|
||||
internal static class Schemas
|
||||
{
|
||||
internal const string Orders = "orders";
|
||||
}
|
||||
```
|
||||
|
||||
Register it in `DependencyInjection.cs`:
|
||||
|
||||
```csharp
|
||||
services.AddDbContext<OrdersDbContext>(options =>
|
||||
options
|
||||
.UseNpgsql(configuration.GetConnectionString("Database"))
|
||||
.UseSnakeCaseNamingConvention());
|
||||
```
|
||||
|
||||
Then add `OrdersDbContext` to `MigrationExtensions.cs` in `Mccn.Api`:
|
||||
|
||||
```csharp
|
||||
internal static async Task ApplyMigrationsAsync(this WebApplication app)
|
||||
{
|
||||
using IServiceScope scope = app.Services.CreateScope();
|
||||
await MigrateAsync<UsersDbContext>(scope.ServiceProvider);
|
||||
await MigrateAsync<OrdersDbContext>(scope.ServiceProvider); // add this
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Create the architecture test project
|
||||
|
||||
```bash
|
||||
dotnet new xunit -n Mccn.Modules.Orders.ArchitectureTests \
|
||||
-o src/Modules/Orders/Mccn.Modules.Orders.ArchitectureTests
|
||||
dotnet sln add src/Modules/Orders/Mccn.Modules.Orders.ArchitectureTests/
|
||||
```
|
||||
|
||||
Copy the `BaseTest.cs`, `TestResultExtensions.cs`, `LayerTests.cs`, and `ApplicationTests.cs` files from an existing architecture test project (e.g. Hello) and update the namespaces and assembly references to point to the Orders module assemblies.
|
||||
|
||||
Also add the new module to the global architecture tests in `test/Mccn.ArchitectureTests/Layers/ModuleTests.cs` to enforce that Orders does not depend on Users or Hello.
|
||||
|
||||
---
|
||||
|
||||
## Adding a Feature to an Existing Module
|
||||
|
||||
As an example, adding a **Get Order by ID** feature to the Orders module.
|
||||
|
||||
### 1. Define the domain error (Domain layer)
|
||||
|
||||
```csharp
|
||||
// OrderErrors.cs
|
||||
public static class OrderErrors
|
||||
{
|
||||
public static Error NotFound(Guid id) =>
|
||||
Error.NotFound("Orders.NotFound", $"Order with ID '{id}' was not found.");
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create the query (Application layer)
|
||||
|
||||
```csharp
|
||||
// GetOrder/GetOrderQuery.cs
|
||||
public sealed record GetOrderQuery(Guid OrderId) : IQuery<OrderResponse>;
|
||||
```
|
||||
|
||||
```csharp
|
||||
// GetOrder/OrderResponse.cs
|
||||
public sealed record OrderResponse(Guid Id, string Status, decimal Total);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// GetOrder/GetOrderQueryHandler.cs
|
||||
internal sealed class GetOrderQueryHandler(IOrderRepository orderRepository)
|
||||
: IQueryHandler<GetOrderQuery, OrderResponse>
|
||||
{
|
||||
public async Task<Result<OrderResponse>> Handle(
|
||||
GetOrderQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Order? order = await orderRepository.GetByIdAsync(query.OrderId, cancellationToken);
|
||||
|
||||
if (order is null)
|
||||
return Result.Failure<OrderResponse>(OrderErrors.NotFound(query.OrderId));
|
||||
|
||||
return new OrderResponse(order.Id, order.Status, order.Total);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create the endpoint (Presentation layer)
|
||||
|
||||
```csharp
|
||||
// Orders/GetOrder.cs
|
||||
internal sealed class GetOrder : IEndpoint
|
||||
{
|
||||
public void MapEndpoint(IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet("orders/{orderId:guid}", async (Guid orderId, ISender sender) =>
|
||||
{
|
||||
var query = new GetOrderQuery(orderId);
|
||||
Result<OrderResponse> result = await sender.Send(query);
|
||||
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.NotFound();
|
||||
})
|
||||
.WithTags("Orders")
|
||||
.RequireAuthorization();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The endpoint is discovered and registered automatically — no manual wiring needed.
|
||||
|
||||
---
|
||||
|
||||
## Key Patterns and Conventions
|
||||
|
||||
### Result<T>
|
||||
|
||||
Handlers never throw exceptions for expected failures. They return `Result<T>`:
|
||||
|
||||
```csharp
|
||||
// Returning success
|
||||
return new OrderResponse(order.Id, order.Status);
|
||||
|
||||
// Returning failure
|
||||
return Result.Failure<OrderResponse>(OrderErrors.NotFound(id));
|
||||
```
|
||||
|
||||
At the endpoint level, map the result to an HTTP response:
|
||||
|
||||
```csharp
|
||||
return result.IsSuccess
|
||||
? Results.Ok(result.Value)
|
||||
: Results.NotFound();
|
||||
```
|
||||
|
||||
### Commands and Queries
|
||||
|
||||
Use `ICommand<TResponse>` for operations that change state, `IQuery<TResponse>` for reads:
|
||||
|
||||
```csharp
|
||||
public sealed record CreateOrderCommand(Guid UserId, decimal Total) : ICommand<Guid>;
|
||||
public sealed record GetOrderQuery(Guid OrderId) : IQuery<OrderResponse>;
|
||||
```
|
||||
|
||||
Implement `ICommandHandler<TCommand, TResponse>` or `IQueryHandler<TQuery, TResponse>` accordingly.
|
||||
|
||||
### Validation
|
||||
|
||||
Add a `FluentValidation` validator next to any command that requires input validation:
|
||||
|
||||
```csharp
|
||||
// CreateOrderCommandValidator.cs
|
||||
public sealed class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
|
||||
{
|
||||
public CreateOrderCommandValidator()
|
||||
{
|
||||
RuleFor(c => c.UserId).NotEmpty();
|
||||
RuleFor(c => c.Total).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `ValidationBehavior` MediatR pipeline behavior runs all validators automatically before the handler. Invalid requests return a `400 Bad Request` with field-level error details — no handler code needed.
|
||||
|
||||
### Domain Entities
|
||||
|
||||
Every entity must:
|
||||
|
||||
1. Inherit from `Entity`
|
||||
2. Have a **private** parameterless constructor (required by EF Core)
|
||||
3. Expose a **static `Create()` factory method** for all other construction
|
||||
|
||||
```csharp
|
||||
public sealed class Order : Entity
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
public decimal Total { get; private set; }
|
||||
|
||||
private Order() { } // EF Core
|
||||
|
||||
public static Order Create(Guid userId, decimal total)
|
||||
{
|
||||
return new Order
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is enforced by the architecture tests in `DomainTests.cs`.
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
The architecture tests enforce these naming rules:
|
||||
|
||||
| Type | Convention | Example |
|
||||
|------------------|-----------------------------------------|--------------------------------------|
|
||||
| Commands | End with `Command` | `CreateOrderCommand` |
|
||||
| Queries | End with `Query` | `GetOrderQuery` |
|
||||
| Command handlers | End with `CommandHandler` | `CreateOrderCommandHandler` |
|
||||
| Query handlers | End with `QueryHandler` | `GetOrderQueryHandler` |
|
||||
| Validators | End with `Validator` | `CreateOrderCommandValidator` |
|
||||
|
||||
### Endpoints
|
||||
|
||||
Implement `IEndpoint` and the class is discovered and registered automatically at startup:
|
||||
|
||||
```csharp
|
||||
internal sealed class MyEndpoint : IEndpoint
|
||||
{
|
||||
public void MapEndpoint(IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapPost("resource", Handler).WithTags("Resource");
|
||||
}
|
||||
|
||||
private static async Task<IResult> Handler(...) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
No registration in `DependencyInjection.cs` is needed for individual endpoints — only the `AssemblyReference` marker class in the Presentation project needs to exist so `EndpointExtensions` can scan the assembly.
|
||||
|
||||
### Module Isolation
|
||||
|
||||
Modules must never reference each other directly. Cross-module communication should go through:
|
||||
|
||||
- **Integration events** (publish via a message broker or in-process event bus — add as needed)
|
||||
- **Shared contracts** in a dedicated `Mccn.Modules.<Name>.Contracts` project that other modules may reference
|
||||
|
||||
The global architecture tests in `test/Mccn.ArchitectureTests/` will fail the build if a module-to-module reference is introduced.
|
||||
Reference in New Issue
Block a user