EnterpriseModularMonolith.Templates
2.0.0
dotnet new install EnterpriseModularMonolith.Templates@2.0.0
Enterprise Modular Monolith — dotnet new templates
dotnet new templates for an ASP.NET Core 10 Enterprise Modular Monolith:
- Clean Architecture modules (Domain / Application / Infrastructure) with a per-module Contracts project as its public API; layering and module isolation enforced by NetArchTest architecture tests
- CQRS mediator with FluentValidation in the pipeline (failures become RFC-7807 problem responses) and per-module authorization policies
- Wolverine + RabbitMQ & Kafka service bus (both transports wired; route per message)
- Marten event sourcing on PostgreSQL
- EF Core (schema per module, tenant-scoped entities, optimistic concurrency → 409)
- Marten event sourcing: an item's change history as an event stream (optional)
- Per-module settings and health checks (
Modules:<Module>,/health/ready) - OpenIddict PKCE/OAuth2 with cookie-based 2FA, impersonation and multi-tenancy
- An Angular SPA client (optional)
- A provider-adapter integration example (domain service → router → factory → adapter → resilient
IIntegrationHttpClient) - A full unit / integration / architecture test suite
Package: EnterpriseModularMonolith.Templates
Templates
| Template | Short names | What it does |
|---|---|---|
| Enterprise Modular Monolith | enterprise-modular, emm |
Generates the full solution. |
| Enterprise Modular Monolith — Add Module | enterprise-modular-module, emm-module |
Adds a new module to an existing solution and wires it in (.slnx, host and unit-test references, Angular route/nav/i18n). Run from the solution root. |
Install
From NuGet (once published):
dotnet new install EnterpriseModularMonolith.Templates
Or install the locally built package:
dotnet new install ./nupkg/EnterpriseModularMonolith.Templates.<version>.nupkg
Usage
Create a new solution:
dotnet new emm -n MyApp
# API-only (no Angular client):
dotnet new emm -n MyApp --IncludeUi false
Optional features
Each of these is on by default; set the flag to false to leave it out:
| Flag | Default | Off behaviour |
|---|---|---|
--IncludeUi |
true |
No Angular client (API-only). |
--IncludeMultiTenancy |
true |
Single-tenant: removes the Tenant entity, per-user TenantId, tenant endpoints/directory/validator, ICurrentTenant, and tenant-scoped admin queries. |
--IncludeRabbitMq |
true |
Omits the Wolverine RabbitMQ transport (package, wiring, RabbitMq config, compose service). |
--IncludeKafka |
true |
Omits the Wolverine Kafka transport (package, wiring, Kafka config, compose service). |
--IncludeProviderAdapter |
true |
Removes the 3rd-party provider-adapter example and the shared HTTP resilience infrastructure. |
# Lean API: no UI, single-tenant, Kafka only, no integration example
dotnet new emm -n MyApp \
--IncludeUi false --IncludeMultiTenancy false \
--IncludeRabbitMq false --IncludeProviderAdapter false
With both --IncludeRabbitMq false and --IncludeKafka false, Wolverine still runs and routes messages locally.
dotnet new does not prompt for options — it applies defaults unless you pass
flags. For an interactive experience (prompts for the project name and whether
to include the Angular UI), use the wrapper script instead:
pwsh -ExecutionPolicy Bypass -File create.ps1
In Visual Studio / Rider, the Include Angular UI option is shown as a checkbox in the New Project wizard automatically.
Add a module (run from the solution root; project name is auto-detected from the .slnx):
dotnet new emm-module -n Billing --allow-scripts yes
The template generates the module plus a one-off wire-Billing.ps1 script and runs it as a
post-action (requires PowerShell 7, pwsh). Without
--allow-scripts yes, dotnet new asks for confirmation first. If the script did not run,
run it yourself from the solution root:
pwsh -ExecutionPolicy Bypass -File wire-Billing.ps1
Each new module comes with a CRUD slice (list/get/create/update/delete) behind its own
<Module>.Read / <Module>.Write policies, validators, an integration event plus handler in its
Contracts project, unit and integration tests, and (optionally) an Angular page.
| Flag | Default | Effect |
|---|---|---|
--IncludeUi |
true |
Angular page, service, route, nav link and i18n entries. |
--IncludeMultiTenancy |
true |
Tenant-scoped entity and queries. Must match the solution. |
--IncludeEventSourcing |
false |
Marten event stream per item, exposed as GET /<module>/{id}/history. |
--IncludeProviderAdapter |
false |
The 3rd-party provider-adapter example. Requires a solution created with it. |
The script verifies these match the solution before wiring anything. The script checks the module fits the solution first (a
PascalCase name that is not taken or reserved, matching provider-adapter option); if not, it removes
the generated files and tells you what to change, so you can simply re-run dotnet new. It deletes
itself once wiring succeeds; if a later step fails it stays in place and can be re-run safely.
How modules plug in
Wiring is deliberately small, because the solution discovers modules by convention:
- Registration — every
*.Modules.*project referenced by the host is a module. At build time the host project turns those references into[ModuleAssembly]attributes; at startupModuleCatalogloads each module's singleIModuleimplementation (e.g.BillingModule), calls itsRegister, and adds its assembly to Wolverine handler discovery. Mediator handlers (ICommandHandler/IQueryHandler) and endpoints (IEndpointBase) in the module are registered automatically —Registeronly sets up the module's own infrastructure (DbContext, options, adapters). - Database — modules read
ConnectionStrings:<Module>and fall back toConnectionStrings:Default, so a new module needs no configuration. Set the module key to move it to its own database. - Contracts — each module owns a
<Project>.Modules.<Module>.Contractsproject: the events, request/response messages and interfaces other modules may use. Modules reference each other's Contracts, never each other's implementation, and an architecture test enforces exactly that. - Authorization — modules declare their own policies with
services.AddModulePolicy(...); integration tests re-register them against the test scheme automatically. - Settings — each module binds
Modules:<Module>to its own options class, validated at startup (ValidateOnStart), so a bad value stops the boot instead of the first request. - Health — a module registers its own check;
/healthis liveness and/health/readycovers every module's database, so an instance only takes traffic once its modules can serve. - Tests — the architecture tests discover modules through the host the same way, so a new module is covered by the layering, isolation and convention rules without edits. Integration tests reach it through the host; unit tests reference the module project.
So Program.cs, appsettings.json, docker-compose.yml and the architecture tests never change
when a module is added. To remove one, remove its project from the solution, the host and the unit
tests, then delete its folders (and its Angular page, route and nav link, if any).
CI
.github/workflows/ci.yml tests the templates, not just the source:
- source / client — build the template source (all features on) and run the .NET and Angular builds and tests.
- package — packs and inspects the
.nupkg: nobin/obj/.vs/node_modules, the dot-files NuGet likes to drop are present, and the package stays under its size limit. - templates — installs that package and generates solutions for five variants (all features, Windows,
API-only single-tenant, one broker, no brokers), adds two or three modules to each, then builds and tests
the result. A generated solution must build with zero warnings, leave no
wire-*.ps1behind and contain noPayFlowplaceholder. - template-guardrails — checks the wiring script refuses modules that do not fit the solution (option mismatch, invalid or reserved name, name already taken) and leaves the solution byte-for-byte unchanged.
Build / pack locally
dotnet pack EnterpriseModularMonolith.csproj -o nupkg
This packages solution-full/ (the enterprise-modular template) and enterprise-module/
(the enterprise-modular-module template) into the NuGet package. Bump <PackageVersion>
in EnterpriseModularMonolith.csproj for each release.
To try uncommitted template changes without packing, install from the repository root
(not from enterprise-module/, which reads its content from ../solution-full):
dotnet new install . --force
Versioning and releases
The package version lives in EnterpriseModularMonolith.csproj and every change is recorded in
CHANGELOG.md. The major version changes when a solution generated by an older
version can no longer take modules from the newer one — dotnet new emm-module checks this and
refuses, naming the template version to install instead.
To release:
# 1. bump <PackageVersion> in EnterpriseModularMonolith.csproj and add the CHANGELOG section
# 2. tag the commit; the tag must match the version
git tag v2.0.0 && git push origin v2.0.0
release.yml then runs the full CI suite, checks the tag matches
<PackageVersion> and that the CHANGELOG documents it, publishes to NuGet and creates a GitHub
release. It needs a NUGET_API_KEY secret on the nuget environment, which is also where you can
require a reviewer before anything is published.
Troubleshooting
dotnet new emm-module says the module does not fit the solution. The module's options must
match the solution it is added to (multi-tenancy, provider adapter) and the solution must come from
templates 2.0 or later. The message names the flag to change; the generated files are removed, so
just re-run with the corrected command.
The module was created but nothing was wired. The wiring runs as a post-action and needs
PowerShell 7 (pwsh) plus --allow-scripts yes. Run it yourself from
the solution root: pwsh -ExecutionPolicy Bypass -File wire-<Module>.ps1. Every step is idempotent,
so re-running is safe.
Generating the module's first migration. Run the wiring script yourself with
pwsh -File wire-<Module>.ps1 -WithMigration (before it deletes itself) to add an
InitialCreate migration instead of relying on auto-create.
A module's tables are missing after adding an entity. Auto-create only builds a schema that does not exist yet. Once a module is deployed, add a migration instead:
dotnet ef migrations add AddThing --project src/modules/<Module>/<Project>.Modules.<Module> --startup-project src/host/<Project>ApiHost.csproj
Removing the Sample module. It is a worked example, not a fixture — delete it once your own
modules exist: remove src/modules/Sample/* (module and Contracts), tests/*/Sample/, the Angular
page (client/src/app/pages/sample, core/services/sample.service.ts, its route, nav link and
sample i18n entries), and the three project references (solution, host, unit tests). Nothing else
points at it — the host discovers modules from its project references.
Repository layout
EnterpriseModularMonolith.csproj # packaging project (PackageId, version, content globs)
CHANGELOG.md # what changed per version (checked by the release workflow)
solution-full/ # the enterprise-modular template content
enterprise-module/ # the enterprise-modular-module template: template.json + wire-Sample.ps1 only
.github/workflows/ # ci.yml (source, package and template tests), release.yml
The module template has no copy of the module code. Its template.json sources the
Sample module, its tests (tests/*/Sample/) and its Angular page directly from
solution-full/, so the full solution's Sample module is the single source of truth:
change it there and both templates pick it up. Those files keep the PayFlow project
placeholder; wire-<Module>.ps1 renames it to the host solution's name when the module is added.
Keep everything module-specific under a Sample folder (e.g. tests/PayFlow.UnitTests/Sample/)
so it is included and renamed per module.
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
See CHANGELOG.md.