Pragmatic.Composition
Source-generated application composition and dependency injection for .NET 10. Declare modules, services, and startup steps; the generator writes all the wiring, so the composed application starts without reflection and without scanning assemblies.
Convention-based scanning lives in a separate package, Pragmatic.Composition.Scanning, for the cases
the generator cannot see. It loads assemblies and enumerates types at run time and is neither trim- nor
AOT-safe; referencing it is the opt-in, and nothing in the generated path calls it.
The Problem
Section titled “The Problem”Every .NET app accumulates the same startup ceremony: register services one by one, order middleware,
wire DbContexts, keep it consistent across modules. With 50+ services, Program.cs becomes a wall of
services.AddScoped<>() that no one wants to maintain, and forgetting one line silently breaks the app.
// Without Pragmatic: 80+ lines of mechanical wiringbuilder.Services.AddScoped<IOrderService, OrderService>();builder.Services.AddScoped<IProductService, ProductService>();// ... 50 more, growing with every new classbuilder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(cs));app.UseAuthentication(); app.UseAuthorization(); // forget one and it breaksThe Solution
Section titled “The Solution”Declare what your application is; the generator handles how it starts up.
// Program.cs: one call wires everythingawait PragmaticApp.RunAsync(args, app =>{ app.UsePragmaticMigrations(); if (app.Environment.IsDevelopment()) app.UseDevelopmentIdentity(); // X-User-* headers are the caller});// Module topology: one class per bounded context[Module][Include<OrdersModule, AppDatabase>][Include<BillingModule, FinancialDatabase>]public sealed class MyAppModule;
// Services register themselves[Service]public class OrderService(IOrderRepository repository) : IOrderService { }The generator emits the complete host startup (infrastructure auto-registration, ordered startup
steps, database init, endpoint mapping, telemetry, maintenance mode) at compile time. Cross-assembly
discovery works through [PragmaticMetadata] assembly attributes: libraries declare what they
register, the host aggregates automatically.
Packages
Section titled “Packages”| Package | Role |
|---|---|
Pragmatic.Composition |
Meta-package (references Abstractions + Host) |
Pragmatic.Composition.Host |
ASP.NET Core runtime: PragmaticApp, IStartupStep, telemetry, remote boundaries, maintenance mode |
Pragmatic.Composition.Scanning |
Opt-in convention scanning at run time, not trim- or AOT-safe (see above) |
Attributes ([Service], [Module], [StartupStep], …) live in Pragmatic.Abstractions, so domain
modules use them without referencing ASP.NET Core.
The 3-tier configuration model
Section titled “The 3-tier configuration model”| Tier | Where | What it decides |
|---|---|---|
| Topology (compile-time) | [Module], [Boundary], [BelongsTo<T>], [UsePackage<T>] |
Structure & module dependencies; the generator detects it |
| Module strategy | Program.cs via IPragmaticBuilder.Use*() |
Infrastructure choices: auth handler, storage, transport |
| Business wiring | IStartupStep |
Services, filters, OpenAPI, feature-specific DI + HTTP pipeline |
What a module declares is wired without a line in Program.cs, with in-memory or passthrough defaults
where a backend is optional; you override only what you need. Two things are never defaulted, because
guessing them would be unsafe: who calls the API (every endpoint requires an authenticated caller,
the build stops on PRAG1695 without an identity package or [AnonymousHost], and outside Development a
host with no authentication method refuses to start) and a culture, from UseI18N or the modules’
translations. See Startup Pipeline.
Installation
Section titled “Installation”dotnet add package Pragmatic.Composition.Hostdotnet add package Pragmatic.SourceGenerator # the unified analyzer(Building inside this monorepo? See Monorepo Structure.)
What the generator gives you
Section titled “What the generator gives you”[Service]/[Decorator]: self-registering services and ordered decorators, noAddScopedlists.[Module]/[Include<…>]: module topology and per-module databases.[StartupStep]/IStartupStep: ordered business wiring + HTTP pipeline configuration.IPragmaticBuilder.Use*(): module strategy (auth, storage, messaging, …).- Remote boundaries:
[RemoteBoundary<T>]generates typed HTTP invokers for cross-service calls. - Maintenance mode, telemetry, and database initialization, wired automatically.
Status
Section titled “Status”Functional within 1.0.0-alpha: the composition model, service/decorator registration, startup steps, the builder, remote boundaries, and maintenance mode. See the roadmap.
| Concepts | Composition model, metadata aggregation, the 3-tier configuration model |
| Getting Started | Your first module, service, and PragmaticApp.RunAsync |
| Service Registration | [Service], lifetimes, [Decorator], keyed services |
| Startup Pipeline | IStartupStep, IPragmaticBuilder, ordering, HTTP pipeline |
| Remote Boundaries | [RemoteBoundary<T>], typed HTTP invokers, the invoke endpoint |
| Common Mistakes | The most frequent composition pitfalls |
| Troubleshooting | Problem/solution guide with diagnostics |
Requirements
Section titled “Requirements”- .NET 10.0+
- ASP.NET Core 10.0+
Pragmatic.SourceGeneratoranalyzer
License
Section titled “License”Part of the Pragmatic.Design ecosystem. See Licensing. Pragmatic.Composition is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).
