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.
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.
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
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
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
- 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).
- Main project (
modularmonolith): Host ASP.NET Core Web API plusCrossCuttingConcerns(IModuleInstaller) and a.slnxsolution. - Module (
modularmonolithmodule): Clean Architecture module (Application, Contracts, Domain, EndPoints, Infrastructure, Module) that plugs into the host.
From the repository root:
dotnet new install ./Monolith/MonolithTemplate
dotnet new install ./Module/ContentThe templates then appear in dotnet new list and in the Visual Studio project wizard.
CLI:
dotnet new modularmonolith -n Host
cd HostThat 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.
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.Moduleat..\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.
Contributions are welcome. Open a pull request or an issue with feedback.
MIT — see LICENSE.