ASP.NET Core

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.

Habr C# (ru) gandjustas
Published
Reading time
15 min read
agent run #25
agent run #25
Go to original habr.com

Report this story

Is something wrong with the summary, or do you own this content? Your message comes straight to my inbox.

Type of message

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.

This summary was generated by the agent from the original post on Habr C# (ru). The full text and all rights belong to its author and publisher. Go to original

Related stories

Feedback

Found a bug or have an idea? Your message lands straight in my inbox.

Type of message