Modular monolith in ASP.NET out of the box with HostingStartup
The hosting startup assemblies mechanism that has shipped with ASP.NET Core since 2.0 turns out to be enough to build a modular monolith without a NuGet package or a custom plugin system. Modules are ordinary class libraries carrying `[assembly: HostingStartup]`, and the `HOSTINGSTARTUPASSEMBLIES` environment variable decides which ones load. One Docker image can then run as a monolith or as a set of services depending on that value.
Key points
- A module is a plain class library with `[assembly: HostingStartup]`; no `IModule`-style interface or plugin loader is needed.
- The `HOSTINGSTARTUPASSEMBLIES` environment variable chooses which modules load, so one image serves monolith and distributed topologies.
- To add endpoints and middleware a module registers itself as an `IStartupFilter` and uses `UseEndpoints`; the host must call `app.UseRouting()` explicitly.
- Calling `IWebHostBuilder.Configure` inside `IHostingStartup.Configure` replaces the host pipeline entirely, so stick to `ConfigureServices` and `ConfigureAppConfiguration`.
- MVC and Razor Pages discover controllers and pages from modules that were not even loaded; disable that with `GenerateMvcApplicationPartsAssemblyAttributes=false` and register parts explicitly.
- A typo in an assembly name only logs a crit entry and startup continues, so silent module skips need log monitoring.
The article shows how ASP.NET Core's little-known hosting startup assemblies feature can carry a modular monolith. When `WebApplication.CreateBuilder` runs, it reads the `HostingStartupAssemblies` configuration key (the `HOSTINGSTARTUPASSEMBLIES` environment variable), loads each listed assembly with `Assembly.Load`, finds the `[assembly: HostingStartup(typeof(T))]` attribute and calls `IHostingStartup.Configure(IWebHostBuilder)` on it. The application's own assembly always runs first; the rest follow in the order they appear in the variable.
A module is a Web SDK project with `OutputType=Library`. It registers services and configuration through `ConfigureServices` and `ConfigureAppConfiguration`, and to add middleware or endpoints it registers itself as an `IStartupFilter` and uses `UseEndpoints`. Nothing in the host's `Program.cs` mentions modules; project references exist only so the assemblies land next to the host at build and publish time. Separate modules provide in-process, HTTP, gRPC and RabbitMQ (EasyNetQ) implementations of the same `ICalculator` contract, and switching between them leaves business code untouched.
Real-world use
- A SaaS team that wants one codebase to run as a single-process monolith and as per-service containers
- A backend that uses the in-process `ICalculator` implementation in dev and tests but HTTP or gRPC in production
- A team migrating a modular monolith toward distributed deployment without touching business logic at each step
- Developers who want to run and debug every module in a single host with F5 during integration testing
Why it matters
Because modularity is fully decoupled from the host, deployment topology becomes a configuration decision, and changing or abandoning the architecture does not force a rewrite of module code.