Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Modular Monolith with Clean Architecture Project Templates

This repository contains two project templates for building applications using Modular Monolith architecture with ASP.NET Core. They are intended to accelerate the development of scalable, maintainable web APIs.

What's new

The templates now target .NET 10, use .slnx solutions, and take current stable NuGet packages.

MediatR remains on 12.5. From 13 onward MediatR is dual-licensed (commercial / RPL) and needs a Lucky Penny key in production. 12.x is still Apache/MIT, so a generated project does not force that license on you.

Swagger UI is gone; Scalar is the API reference. The host builds OpenAPI with Microsoft.AspNetCore.OpenApi and FastEndpoints.OpenApi, and serves it at /scalar.

Architecture

A modular monolith is one application and one deployable. Bounded contexts live as in-process modules, not as separate services. They also share one database: each module owns a schema (orders, billing, catalog, …) and does not write to another module’s tables. Integration between modules stays in-process (messages, events, public contracts), not cross-schema joins.

%%{init: {
  "theme": "base",
  "flowchart": { "curve": "basis", "nodeSpacing": 40, "rankSpacing": 64, "padding": 20, "htmlLabels": false, "wrappingWidth": 280 },
  "themeVariables": {
    "fontFamily": "ui-sans-serif, system-ui, Segoe UI, Helvetica, Arial",
    "fontSize": "16px",
    "lineColor": "#64748b",
    "textColor": "#0f172a",
    "clusterBkg": "#ffffff",
    "clusterBorder": "#94a3b8"
  }
}}%%
flowchart TB
  clients(["Clients / UI "])

  subgraph app["MODULAR MONOLITH  ·  one process, one deploy "]
    direction TB
    hostApi[["Host API "]]
    subgraph modules["In-process modules "]
      direction LR
      mmOrders["Orders "]
      mmBilling["Billing "]
      mmCatalog["Catalog "]
    end
    hostApi --> mmOrders
    hostApi --> mmBilling
    hostApi --> mmCatalog
  end

  subgraph db["ONE DATABASE  ·  schema per module "]
    direction LR
    schemaOrders["orders "]
    schemaBilling["billing "]
    schemaCatalog["catalog "]
  end

  clients ==> hostApi
  mmOrders --> schemaOrders
  mmBilling --> schemaBilling
  mmCatalog --> schemaCatalog

  classDef client fill:#f8fafc,stroke:#475569,stroke-width:2px,color:#0f172a
  classDef host fill:#1d4ed8,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
  classDef module fill:#d1fae5,stroke:#059669,stroke-width:2px,color:#064e3b
  classDef schema fill:#ffedd5,stroke:#ea580c,stroke-width:2px,color:#9a3412
  class clients client
  class hostApi host
  class mmOrders,mmBilling,mmCatalog module
  class schemaOrders,schemaBilling,schemaCatalog schema

  style app fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
  style modules fill:#f0fdf4,stroke:#10b981,stroke-width:1.5px,color:#065f46
  style db fill:#fffbeb,stroke:#d97706,stroke-width:2px,color:#92400e
  linkStyle 0,1,2 stroke:#2563eb,stroke-width:2px
  linkStyle 3 stroke:#1d4ed8,stroke-width:3px
  linkStyle 4,5,6 stroke:#ea580c,stroke-width:2px
Loading

The host is a thin ASP.NET Core process. Modules are independently developed slices that register themselves through IModuleInstaller. Scrutor scans loaded assemblies, so a module is composed in as soon as the host references its *.Module project.

%%{init: {
  "theme": "base",
  "flowchart": { "curve": "basis", "nodeSpacing": 28, "rankSpacing": 44, "padding": 16, "htmlLabels": false, "wrappingWidth": 260 },
  "themeVariables": {
    "fontFamily": "ui-sans-serif, system-ui, Segoe UI, Helvetica, Arial",
    "fontSize": "15px",
    "lineColor": "#64748b"
  }
}}%%
flowchart TB
  subgraph hostComp["HOST"]
    api["ASP.NET Core + FastEndpoints "]
    scalarUi["Scalar / OpenAPI "]
    di["InstallModules + ConfigureModules "]
    ccc["CrossCuttingConcerns / IModuleInstaller "]
    api --> scalarUi
    api --> di
    di --> ccc
  end

  subgraph modOrders["MODULE  ·  Orders"]
    ep1["EndPoints "]
    app1["Application "]
    inf1["Infrastructure "]
    ctr1["Contracts "]
    dom1["Domain "]
    ep1 --> ctr1
    ep1 --> app1
    app1 --> ctr1
    inf1 --> ctr1
    inf1 --> dom1
  end

  subgraph modBilling["MODULE  ·  Billing"]
    ep2["EndPoints "]
    app2["Application "]
    inf2["Infrastructure "]
    ctr2["Contracts "]
    dom2["Domain "]
    ep2 --> ctr2
    ep2 --> app2
    app2 --> ctr2
    inf2 --> ctr2
    inf2 --> dom2
  end

  ccc -->|"discovers installers"| modOrders
  ccc -->|"discovers installers"| modBilling

  classDef hostN fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
  classDef ep fill:#fae8ff,stroke:#a21caf,stroke-width:1.5px,color:#701a75
  classDef app fill:#e0e7ff,stroke:#4f46e5,stroke-width:1.5px,color:#312e81
  classDef inf fill:#ccfbf1,stroke:#0f766e,stroke-width:1.5px,color:#134e4a
  classDef ctr fill:#f1f5f9,stroke:#64748b,stroke-width:1.5px,color:#334155
  classDef dom fill:#fce7f3,stroke:#db2777,stroke-width:1.5px,color:#9d174d
  class api,scalarUi,di,ccc hostN
  class ep1,ep2 ep
  class app1,app2 app
  class inf1,inf2 inf
  class ctr1,ctr2 ctr
  class dom1,dom2 dom

  style hostComp fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
  style modOrders fill:#f0fdf4,stroke:#059669,stroke-width:2px,color:#065f46
  style modBilling fill:#fff7ed,stroke:#ea580c,stroke-width:2px,color:#9a3412
Loading

Inside a module, HTTP never talks to the domain directly. FastEndpoints takes the request, MediatR dispatches a contract, and Infrastructure handles it.

%%{init: {
  "theme": "base",
  "sequence": { "actorMargin": 28, "boxMargin": 8, "messageMargin": 36, "useMaxWidth": true },
  "themeVariables": {
    "fontFamily": "ui-sans-serif, system-ui, Segoe UI, Helvetica, Arial",
    "fontSize": "15px",
    "actorBkg": "#dbeafe",
    "actorBorder": "#2563eb",
    "actorTextColor": "#1e3a8a",
    "actorLineColor": "#93c5fd",
    "signalColor": "#334155",
    "signalTextColor": "#0f172a",
    "labelBoxBkgColor": "#ecfdf5",
    "labelBoxBorderColor": "#059669",
    "labelTextColor": "#064e3b",
    "loopTextColor": "#0f172a",
    "noteBkgColor": "#ffedd5",
    "noteBorderColor": "#ea580c",
    "noteTextColor": "#9a3412",
    "activationBkgColor": "#bfdbfe",
    "activationBorderColor": "#1d4ed8",
    "sequenceNumberColor": "#ffffff"
  }
}}%%
sequenceDiagram
  autonumber
  actor Client
  participant Host as Host
  participant Ep as EndPoints
  participant Val as FluentValidation
  participant Med as MediatR
  participant Inf as Infrastructure
  participant Dom as Domain

  Client->>+Host: HTTP POST /hello/world
  Host->>+Ep: FastEndpoints
  Ep->>Val: validate request DTO
  Ep->>+Med: ISender.Send query
  Med->>+Inf: IRequestHandler
  Inf->>Dom: entities when needed
  Inf-->>-Ep: Result of T
  Ep-->>-Client: 200 JSON or 400
  deactivate Host
  deactivate Med
Loading

Technologies

  • ASP.NET Core / .NET 10: Web apps and services on .NET.
  • MediatR 12.5: In-process messaging (Apache/MIT).
  • FastEndpoints: Minimal API-style endpoints for ASP.NET Core.
  • FluentValidation: Strongly-typed validation rules.
  • Ardalis Result: Result pattern.
  • Scrutor: Assembly scanning to auto-register modules.
  • Scalar: API reference UI on Microsoft.AspNetCore.OpenApi (/scalar).

Templates

  1. Main project (modularmonolith): Host ASP.NET Core Web API plus CrossCuttingConcerns (IModuleInstaller) and a .slnx solution.
  2. Module (modularmonolithmodule): Clean Architecture module (Application, Contracts, Domain, EndPoints, Infrastructure, Module) that plugs into the host.

Installation

From the repository root:

dotnet new install ./Monolith/MonolithTemplate
dotnet new install ./Module/Content

The templates then appear in dotnet new list and in the Visual Studio project wizard.

Creating a host

CLI:

dotnet new modularmonolith -n Host
cd Host

That creates:

Host/
  Host.slnx
  Directory.Packages.props
  Host/Host.csproj
  Host.CrossCuttingConcerns/

Use the same value for the project name and the Host namespace prefix so namespaces stay consistent.

Visual Studio: choose Modular Monolith Project, set the name (e.g. Host) and the same namespace prefix.

Adding a module

Run this inside the host folder (next to Host.slnx), not in a subfolder. The module projects are siblings of the host projects.

dotnet new modularmonolithmodule -n Host.Orders --hostName Host

--hostName must match the host project name. The template then:

  • Points Host.Orders.Module at ..\Host.CrossCuttingConcerns\...
  • Adds the six module projects to the solution

The host already references ..\*.Module\*.Module.csproj, so new modules are picked up without editing the host .csproj.

Visual Studio: add Modular Monolith Module Project with location set to the host solution folder (the same folder as Host.slnx, not a new subfolder). Set Host project name to Host.

After that, run the host. Scrutor loads IModuleInstaller implementations automatically. In Development open /scalar for the API reference and try POST /hello/world.

Contributing

Contributions are welcome. Open a pull request or an issue with feedback.

License

MIT — see LICENSE.

Releases

Packages

Used by

Contributors

Languages