.NET Architecture & Design Patterns

.NET Architecture & Design Patterns

Explore .net architecture & design patterns code snippets and tutorials

.NET Architecture & Design Patterns

Modular monolith in .NET: bounded-context modules, separate DbContexts, integration events

A single deployable, split into modules per bounded context (Orders, Catalog, Pricing, Shipping).

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ======================================================================
// Q: What is a modular monolith?
//
// A single deployable, split into modules per bounded context (Orders, Catalog,
// Pricing, Shipping). Each module has its own data and a public contract. No
// module reads another module's tables.
// - Internal classes are marked internal. The boundaries are enforced with
//   architecture tests.
// - If a module later needs independent scaling, it can be extracted into a
//   microservice with few changes: the boundaries already exist.
// ======================================================================

// Orders does not JOIN Catalog's tables: it asks through a public contract
public interface ICatalogModule
{
    Task<ProductInfo?> GetProductAsync(string sku, CancellationToken ct);
}

// Each module with its own DbContext and schema
public class OrdersDbContext(DbContextOptions<OrdersDbContext> o) : DbContext(o)
{
    protected override void OnModelCreating(ModelBuilder b) => b.HasDefaultSchema("orders");
}

// Asynchronous communication between modules via integration events (in-process or outbox)
public record OrderPlacedIntegrationEvent(int OrderId, string CustomerNumber);
.NET Architecture & Design Patterns

Vertical Slice Architecture in ASP.NET Core: organizing code by feature with minimal APIs

Organization by feature: each use case keeps its endpoint, request, validation, handler and data access together.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ======================================================================
// Q: What is Vertical Slice Architecture?
//
// Organization by feature: each use case keeps its endpoint, request,
// validation, handler and data access together. A change touches one place, not
// five layers.
// - Each slice chooses its own complexity: a simple query goes straight to the
//   DbContext, a complex command goes through the domain model.
// - Risk: duplicated rules. Shared business rules belong in the domain, not in
//   every slice.
// - It combines well with Clean Architecture: the Domain / Infrastructure
//   boundaries stay, and inside Application the code is organized by feature.
// ======================================================================

// Feature folder layout and a GetOrderById slice
Features/
  Orders/
    CreateOrder.cs        // request + validator + handler + endpoint
    CancelOrder.cs
    GetOrderById.cs
  Catalog/
    SearchProducts.cs

public static class GetOrderById
{
    public record Response(int Id, string Number, decimal Total);

    public static void Map(IEndpointRouteBuilder app) =>
        app.MapGet("/orders/{id:int}", Handle).RequireAuthorization();

    private static async Task<Results<Ok<Response>, NotFound>> Handle(
        int id, AppDbContext db, CancellationToken ct)
    {
        var dto = await db.Orders.AsNoTracking()
            .Where(o => o.Id == id)
            .Select(o => new Response(o.Id, o.OrderNumber, o.TotalNet))
            .FirstOrDefaultAsync(ct);
        return dto is null ? TypedResults.NotFound() : TypedResults.Ok(dto);
    }
}
.NET Architecture & Design Patterns

Hexagonal architecture (Ports and Adapters) in .NET: driving and driven adapters in C#

The core (domain + use cases) doesn't know who calls it or what it calls.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ======================================================================
// Q: What is Hexagonal architecture (Ports and Adapters)?
//
// The core (domain + use cases) doesn't know who calls it or what it calls. It
// communicates only through ports.
// - Driving (primary) adapters: they call the core. REST endpoints, message
//   consumers, scheduled jobs, tests.
// - Driven (secondary) adapters: the core calls them. EF repository, ERP client,
//   email sender.
// Onion, Clean and Hexagonal share the same idea: an independent core and
// dependency inversion. They differ in emphasis: layers (Onion), use cases
// (Clean), inputs / outputs (Hexagonal).
// ======================================================================

// Driven port: the core states what it needs
public interface IErpGateway { Task<string> SendOrderAsync(Order o, CancellationToken ct); }

public class SapErpAdapter(HttpClient http) : IErpGateway { /* SAP over HTTP */ }
public class FakeErpAdapter : IErpGateway { /* for tests and local dev */ }

// Two driving adapters, the same use case
app.MapPost("/orders/{id}/sync", (int id, SyncOrderHandler h, CancellationToken ct) =>
    h.Handle(id, ct));

public class OrderPlacedConsumer(SyncOrderHandler h) : IConsumer<OrderPlaced>
{
    public Task Consume(ConsumeContext<OrderPlaced> ctx) =>
        h.Handle(ctx.Message.OrderId, ctx.CancellationToken);
}
.NET Architecture & Design Patterns

Onion / Clean Architecture in .NET: project references and dependency inversion (C#)

Difference from N-tier: in N-tier the BLL points to the DAL.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// ======================================================================
// Q: Explain Onion / Clean Architecture. How do the projects reference each
// other?
//
// - Difference from N-tier: in N-tier the BLL points to the DAL. Here
//   Infrastructure points to Application (dependency inversion). The database is
//   a detail.
// - Onion vs Clean: practically the same. Clean explicitly names the use cases
//   (handlers) and the "interface adapters".
// - Enforce the rules with architecture tests, so that nobody (not even an AI
//   agent) puts EF Core into the Domain.
// ======================================================================

// Domain: plain C#, the rules live inside the entity
public class Order
{
    public OrderStatus Status { get; private set; }
    public void Cancel()
    {
        if (Status == OrderStatus.Shipped) throw new DomainException("Already shipped");
        Status = OrderStatus.Cancelled;
    }
}

// Application: defines the interface (port) and the use case
public interface IOrderRepository { Task<Order?> GetAsync(int id, CancellationToken ct); }

public class CancelOrderHandler(IOrderRepository orders, IUnitOfWork uow)
{
    public async Task Handle(int id, CancellationToken ct)
    {
        var order = await orders.GetAsync(id, ct) ?? throw new NotFoundException();
        order.Cancel();
        await uow.SaveChangesAsync(ct);
    }
}

// Infrastructure: the EF Core implementation
public class EfOrderRepository(AppDbContext db) : IOrderRepository
{
    public Task<Order?> GetAsync(int id, CancellationToken ct) =>
        db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == id, ct);
}

// Api: composition root
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();

// Architecture test: the Domain must not depend on EF Core or Infrastructure
[Fact]  // NetArchTest.Rules
public void Domain_does_not_depend_on_infrastructure()
{
    var result = Types.InAssembly(typeof(Order).Assembly)
        .ShouldNot().HaveDependencyOnAny("Microsoft.EntityFrameworkCore", "Shop.Infrastructure")
        .GetResult();
    Assert.True(result.IsSuccessful);
}
.NET Architecture & Design Patterns

Onion / Clean Architecture in .NET: project references and dependency inversion (diagram)

Difference from N-tier: in N-tier the BLL points to the DAL.

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# ======================================================================
# Q: Explain Onion / Clean Architecture. How do the projects reference each
# other?
#
# - Difference from N-tier: in N-tier the BLL points to the DAL. Here
#   Infrastructure points to Application (dependency inversion). The database is
#   a detail.
# - Onion vs Clean: practically the same. Clean explicitly names the use cases
#   (handlers) and the "interface adapters".
# - Enforce the rules with architecture tests, so that nobody (not even an AI
#   agent) puts EF Core into the Domain.
# ======================================================================

  +-------------------------------------------------+
  |  Api / UI                    Infrastructure     |   outer: ASP.NET, EF Core, HTTP
  |   +-----------------------------------------+   |
  |   |  Application (use cases, interfaces)    |   |
  |   |   +---------------------------------+   |   |
  |   |   |  Domain (entities, value objs)  |   |   |   core: no dependencies
  |   |   +---------------------------------+   |   |
  |   +-----------------------------------------+   |
  +-------------------------------------------------+
  All dependencies point inward.

Shop.Domain          → nothing (no EF Core, no ASP.NET)
Shop.Application     → Shop.Domain
Shop.Infrastructure  → Shop.Application      (implements its interfaces)
Shop.Api             → Shop.Application + Shop.Infrastructure (only for DI)