Skip to content

Pragmatic.Composition

Source-generated application composition and dependency injection for .NET 10. Declare modules, services, and startup steps — the generator writes all the wiring.

Every .NET application accumulates the same startup ceremony: register services one by one, configure middleware in the right order, wire database contexts, and keep it all consistent across modules. With 50+ services, Program.cs becomes a wall of services.AddScoped<>() calls that no one wants to maintain.

// Without Pragmatic: 80+ lines of mechanical wiring
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
// ... 50 more services, growing with every new class
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(config.GetConnectionString("App")));
builder.Services.AddDbContext<BillingDbContext>(o => o.UseSqlServer(config.GetConnectionString("Billing")));
builder.Services.AddAuthentication("Bearer").AddJwtBearer(o => { /* ... */ });
var app = builder.Build();
app.UseResponseCompression();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// Forget one line, the app silently breaks

With Pragmatic.Composition, you declare WHAT your application is. The source generator handles HOW it starts up.

// With Pragmatic: declare the shape, the SG generates the rest
await PragmaticApp.RunAsync(args, app =>
{
if (app.Environment.IsDevelopment())
app.UseDatabaseEnsureCreated();
app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");
}).ConfigureAwait(false);
// 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 SG produces the complete host startup: infrastructure auto-registration, ordered startup steps, database initialization, endpoint mapping, telemetry, and maintenance mode — all at compile time, zero reflection.

Pragmatic.Composition replaces manual services.AddScoped<>() registration with source-generated DI. Mark classes with [Service], order decorators with [Decorator], organize modules with [Module], and define application lifecycle with [StartupStep] and IPragmaticBuilder. The generator emits all registrations as compile-time code — no reflection, no assembly scanning at runtime.

The module ships as two packages:

PackageRole
Pragmatic.CompositionMeta-package (references Abstractions + Host)
Pragmatic.Composition.HostASP.NET Core hosting runtime: PragmaticApp, IStartupStep, assembly scanning, telemetry, remote boundaries, maintenance mode

Attributes ([Service], [Module], [StartupStep], etc.) live in Pragmatic.Abstractions so domain modules can use them without referencing ASP.NET Core.

At startup, one call (PragmaticApp.RunAsync()) wires everything. Cross-assembly discovery works through [PragmaticMetadata] assembly attributes — libraries declare what they register, the host aggregates automatically.


Terminal window
dotnet add package Pragmatic.Composition

Add the source generator as a project reference:

<ProjectReference Include="..\Pragmatic.SourceGenerator\src\Pragmatic.SourceGenerator\Pragmatic.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />

Each domain boundary declares a module class. Libraries use [IncludeModule<T>] for cross-boundary dependencies:

Showcase.Booking/BookingModule.cs
[Module(Name = "Showcase.Booking", Version = "1.0.0", Description = "Reservation and guest management")]
[IncludeModule<CatalogModule>]
public sealed class BookingModule;

The host declares the deployment topology — which modules are included and which database each uses:

Showcase.Host/ShowcaseHostModule.cs
[Module]
[Include<AccountsModule, ShowcaseAppDatabase>]
[Include<BillingModule, ShowcaseFinancialDatabase>]
[Include<BookingModule, ShowcaseAppDatabase>]
[Include<CatalogModule, ShowcaseAppDatabase>]
[NeedsStep<InternationalizationStep>]
[NeedsStep<RoutingStep>]
public sealed class ShowcaseHostModule;

PragmaticApp.RunAsync is the entry point. The source generator replaces the stub implementation with a fully wired host based on discovered modules:

await PragmaticApp.RunAsync(args, app =>
{
// Database -- create tables automatically in development
if (app.Environment.IsDevelopment())
app.UseDatabaseEnsureCreated();
// Multi-tenancy -- resolve tenant from HTTP header
app.UseMultiTenancy(mt => mt.UseHeader());
// Authentication -- config-driven: JWT for production, NoOp for development
var jwtKey = app.Configuration["Jwt:Key"];
if (!string.IsNullOrEmpty(jwtKey))
app.UseJwtAuthentication(jwt => { jwt.SigningKey = jwtKey; /* ... */ });
else
app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");
// Authorization -- compose application roles from module definitions
app.UseAuthorization(authz =>
{
authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
authz.MapRole<ShowcaseAdmin>();
authz.MapRole<BookingManagerRole>(r => r
.IncludeDefinition<BookingOperator>()
.IncludeDefinition<CatalogReader>());
});
// Logging -- Console + daily rolling file
app.UseLogging(log =>
{
log.AddConsole(PragmaticConsoleConfiguration.ForDevelopment());
log.AddFile("logs/app-{Date}.log", config => { /* ... */ });
});
// File storage
app.UseStorage(sp =>
{
var basePath = Path.Combine(app.Environment.ContentRootPath, "wwwroot");
return new LocalDiskFileStorage(basePath, sp.GetRequiredService<ILogger<LocalDiskFileStorage>>());
});
}).ConfigureAwait(false);

Application-specific services and middleware are configured via IStartupStep:

[StartupStep]
[RequiresConfig("ConnectionStrings:App")]
public class ShowcaseStartupStep : IStartupStep
{
public int Order => 60;
public void ConfigureServices(
IServiceCollection services,
IConfiguration configuration,
IHostEnvironment environment)
{
services.AddShowcaseConfiguration(configuration);
services.AddShowcaseQueryFilters();
services.AddOpenApi(options => options.AddResultTypeSupport());
}
public void ConfigurePipeline(IApplicationBuilder app)
{
app.UseMiddleware<TenantResolutionMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
}
}

Pragmatic.Composition organizes startup into three tiers, each with a distinct responsibility:

TierWhereWhatWho
1. Topology[Module], [Include], [BelongsTo]Compile-time structure, module dependenciesSource generator (automatic)
2. Module StrategyProgram.cs via IPragmaticBuilderInfrastructure choices: auth, storage, logging, multi-tenancyDeveloper (explicit)
3. Business WiringIStartupStepApp services, query filters, middleware, OpenAPI, feature flagsDeveloper (explicit)
Topology (compile-time) --> Module Strategy (Program.cs) --> Business Wiring (IStartupStep)
SG auto-detect IPragmaticBuilder IStartupStep

Execution order: The SG registers infrastructure defaults first, the IPragmaticBuilder callback overrides them (DI last-registration-wins), then IStartupStep.ConfigureServices runs in Order sequence.

Defined in Pragmatic.Abstractions (namespace Pragmatic.Composition):

public interface IPragmaticBuilder
{
IServiceCollection Services { get; }
IConfiguration Configuration { get; }
IHostEnvironment Environment { get; }
}

Each infrastructure module contributes Use*() extension methods on IPragmaticBuilder. IntelliSense shows only the modules referenced by the host project.

MethodPackageWhat It Configures
UseDatabaseEnsureCreated()Composition.HostSchema creation on startup (development)
UseDatabaseMigrate()Composition.HostRun pending EF Core migrations on startup
UseAuthentication<THandler>(scheme)Identity.AspNetCoreAuthentication handler + scheme
UseJwtAuthentication(jwt => { ... })Identity.Local.JwtJWT Bearer with signing key, issuer, expiry
UseAuthorization(authz => { ... })AuthorizationRoles, permissions, groups, policies, ABAC
UseMultiTenancy(mt => { ... })MultiTenancy.AspNetCoreTenant resolution strategy
UseLogging(log => { ... })LoggingConsole + file loggers
UseStorage(sp => new LocalDiskFileStorage(...))StorageFile storage provider

The concrete implementation is PragmaticBuilder in Pragmatic.Composition.Host, which adds a PragmaticOptions property for cross-cutting hosting options (telemetry, maintenance mode, database initialization).

When a Pragmatic infrastructure package is referenced in the .csproj, the SG auto-registers its default. Override via IPragmaticBuilder:

ModuleDefault RegistrationOverride Method
TemporalSystemClock as IClock (Singleton)
ResilienceAddPragmaticResilience() + bind ResilienceOptions
CachingAddHybridCache() + AddPragmaticCaching()
I18nAddPragmaticI18n()
IdentityAddPragmaticAuthorization()UseAuthentication<T>()
MultiTenancyAddPragmaticMultiTenancy() (SingleTenant default)UseMultiTenancy()
FeatureFlagsAddPragmaticFeatureFlags()
DiscoveryAddPragmaticDiscovery()
AuthorizationAddPragmaticAuthorization()UseAuthorization()

Override behavior: SG registers defaults FIRST, then the IPragmaticBuilder callback runs. DI uses last-registration-wins, so Use*() calls override the SG defaults. Finally, IStartupStep.ConfigureServices() runs for business-level wiring.

Execution order: SG defaults → IPragmaticBuilder callback → IStartupStep.ConfigureServices()app.Build()IStartupStep.ConfigurePipeline()MapAllEndpoints()


Mark classes with [Service] for automatic DI registration at compile time:

// Default: scoped, registered as first implemented interface
[Service]
public class OrderService : IOrderService { /* ... */ }
// Explicit interface (generic attribute)
[Service<IPaymentProvider>(Key = "stripe")]
public class StripePaymentProvider : IPaymentProvider { /* ... */ }
// Register as self (concrete type)
[Service(AsSelf = true, Lifetime = ServiceLifetime.Singleton)]
public class CacheWarmupService { /* ... */ }

ServiceAttribute properties:

PropertyTypeDefaultDescription
LifetimeServiceLifetimeScopedSingleton, Scoped, or Transient
AsType?nullRegister as this type (prefer ServiceAttribute<T>)
AsSelfboolfalseRegister as concrete type
Keystring?nullKeyed service registration (.NET 8+)

Decorators wrap existing services. They are applied in ascending Order (lower = closer to real service, higher = outermost):

[Decorator(Order = 1)]
public class LoggingReservationPricingService(
IReservationPricingService inner,
ILogger<LoggingReservationPricingService> logger) : IReservationPricingService
{
// Wraps inner with logging
}
Call flow: Outer (Order=2) --> Inner (Order=1) --> RealService

The Decorate<TService, TDecorator>() extension method on IServiceCollection is also available for manual decoration in IStartupStep.ConfigureServices.

For services that need custom construction logic:

[ServiceFactory]
public class ConnectionFactories
{
[Factory(Lifetime = ServiceLifetime.Singleton)]
public IDbConnection CreateConnection(IConfiguration config)
{
var connStr = config.GetConnectionString("Default");
return new SqlConnection(connStr);
}
}

[ServiceFactory] marks the container class (registered as singleton). [Factory] marks individual factory methods whose return type becomes the registered service type.

For optional dependencies or post-construction initialization:

[Service]
public class NotificationService : INotificationService
{
[Inject]
public IEmailSender? EmailSender { get; set; }
[Inject(Required = true, Key = "priority")]
public IMessageQueue Queue { get; set; } = null!;
}

InjectAttribute properties:

PropertyTypeDefaultDescription
RequiredboolfalseThrow if service not available
Keystring?nullKeyed service resolution

[Module] declares a boundary/module. Applied to an empty sealed class:

[Module(Name = "Showcase.Billing", Version = "1.0.0", Description = "Invoice and payment processing")]
[IncludeModule<BookingModule>]
public sealed class BillingModule;

[IncludeModule<T>] declares a library-level dependency on another module.

[Include<TModule>] and [Include<TModule, TDatabase>] are host-level attributes that wire modules into the deployment topology:

[Module]
[Include<BookingModule, ShowcaseAppDatabase>]
[Include<BillingModule, ShowcaseFinancialDatabase>]
public sealed class ShowcaseHostModule;

The three Include overloads:

AttributePurpose
[Include<TModule>]Include module without database
[Include<TModule, TDatabase>]Include module wired to a database (DbContext name auto-derived)
[Include<TModule, TDatabase, TDbContext>]Include module with explicit DbContext class name

Declare physical databases by deriving from PragmaticDatabase:

[PragmaticDatabase(Provider = DatabaseProvider.SqlServer, ConfigKey = "ConnectionStrings:App")]
public sealed class ShowcaseAppDatabase : PragmaticDatabase { }

Supported providers: SqlServer, PostgreSql, SQLite, MySql, InMemory.

IStartupStep defines the two-phase startup lifecycle:

public interface IStartupStep
{
int Order => 0;
void ConfigureServices(IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) { }
void ConfigurePipeline(IApplicationBuilder app) { }
}

Mark implementations with [StartupStep] for auto-discovery. Order determines execution sequence:

RangePurposeExamples
0-99InfrastructureResponseCompressionStep (25), RoutingStep (50), CorsStep (75)
100-499Module stepsAuthentication, authorization, i18n
500+Application stepsBusiness services, OpenAPI, custom middleware

Built-in steps provided by Pragmatic.Composition.Host:

StepOrderWhat It Does
ResponseCompressionStep25app.UseResponseCompression()
RoutingStep50app.UseRouting()
CorsStep75app.UseCors()

Use [NeedsStep<T>] on a module to declare that it requires a specific step in the pipeline. The SG deduplicates and orders all needed steps automatically.

[RequiresConfig("path")] on a startup step declares required configuration keys. The host validates them at startup (fail-fast before the application starts).

Packages are pre-built collections of entities, actions, and services that fuse into a host module:

// Package definition
public class LocalIdentityPackage : IPackageDefinition
{
public static string PackageName => "Identity.Local";
public static string? RoutePrefix => "identity/local";
public static string? Description => "Local username/password identity provider";
}
// Import in module
[UsePackage<LocalIdentityPackage>]
public sealed class AccountsModule;

IPackageDefinition requires: PackageName, RoutePrefix, Description. Optional: RequiredTypeNames for compile-time dependency validation.

For convention-based registration without attributes, use the fluent scanning API:

services.Scan(scan => scan
.FromAssemblyOf<OrderService>()
.AddClasses(filter => filter.AssignableTo<ICommandHandler>())
.AsImplementedInterfaces()
.WithScopedLifetime());

Assembly sources:

MethodDescription
FromAssemblyOf<T>()Assembly containing type T
FromAssemblies(params Assembly[])Explicit assembly list
FromAssembliesMatching(string pattern)Glob pattern on DLL file names
FromCallingAssembly()Calling assembly
FromEntryAssembly()Entry assembly
FromDependencyContext(predicate?)From DependencyContext.Default
FromDependencyContext(string prefix)Libraries whose name starts with prefix

Type filters (AddClasses(filter => ...)):**

MethodDescription
AssignableTo<T>() / AssignableTo(Type)Implements interface or extends class (supports open generics)
WithAttribute<T>()Has specific attribute
InNamespace(string) / InNamespaceOf<T>()In specific namespace
Where(Func<Type, bool>) / NotWhere(...)Custom predicate

Registration modes (AsImplementedInterfaces(), etc.):**

MethodDescription
AsImplementedInterfaces()All implemented interfaces
AsSelf()Concrete type only
AsSelfWithInterfaces()Concrete type + all interfaces
As<T>() / As(Type) / As(Func<Type, Type>)Specific type
AsMatchingInterface()Interface named I{ClassName}

Registration strategies control duplicate handling:

StrategyBehavior
Append (default)Always adds, even if duplicate
SkipSkip if service type already registered
ReplaceRemove existing, add new
ThrowThrow InvalidOperationException on duplicate

For distributed deployments, [RemoteBoundary<TModule>] replaces a local module with HTTP-based invokers:

// Distributed host: Billing lives on a separate service
[Module]
[Include<BookingModule, ShowcaseAppDatabase>]
[Include<CatalogModule, ShowcaseAppDatabase>]
[RemoteBoundary<BillingModule>]
public sealed class ShowcaseDistributedModule;

The SG generates HttpActionInvoker<TAction, TReturn> for each action in the remote module. Actions are serialized as JSON, POSTed to /_pragmatic/invoke, and deserialized back.

Configuration (appsettings.json):

{
"Pragmatic": {
"RemoteBoundaries": {
"Billing": {
"BaseUrl": "https://billing-service:5001"
}
}
}
}

Runtime types in Pragmatic.Composition.Host/Remote/:

TypeRole
HttpActionInvoker<TAction, TReturn>Sends action via HTTP, returns Result<TReturn, IError>
HttpVoidActionInvoker<TAction>Same for void actions, returns VoidResult<IError>
PragmaticInvokeDispatcherServer-side: resolves invoker from DI, dispatches action
PragmaticInvokeRequestWire format: { ActionType, Payload }
PragmaticInvokeResponseWire format: { IsSuccess, Value?, Error? }
RemoteErrorError type for failed remote invocations

Each source generator emits [PragmaticMetadata] assembly-level attributes with category-specific JSON data. The host generator reads these from referenced assemblies and generates aggregated registration code.

MetadataCategory enum values: DI, Mapping, Actions, Startup, Validation, Endpoints, HealthChecks, Identifiers, Module, Translations, Persistence, EventHandlers, HostTopology, Caching, Configuration.

PragmaticTelemetry.AddPragmaticTelemetry() configures OpenTelemetry with sensible defaults, called automatically by the generated PragmaticApp.RunAsync:

  • Tracing: ASP.NET Core, HttpClient, EF Core instrumentation. All Pragmatic ActivitySources registered.
  • Metrics: ASP.NET Core metrics. All Pragmatic Meters registered.
  • Logging: OTel logging bridge with formatted messages and scopes.
  • Sampling: AlwaysOn in Development, ratio-based (default 10%) in production.
  • Exporters: Console in Development, OTLP when TelemetryOptions.UseOtlpExporter = true.

When the application fails to start, MaintenanceMode provides diagnostic endpoints instead of crashing:

EndpointResponse
/health503 with status, error type, message
/maintenance503 with full error details, stack trace (dev only), suggestions
Any other path503 with pointer to /health and /maintenance

Configure via PragmaticOptions.MaintenanceMode:

public sealed class MaintenanceModeOptions
{
public bool EnableOnStartupFailure { get; set; } = true;
public string HealthPath { get; set; } = "/health";
public string MaintenancePath { get; set; } = "/maintenance";
public bool? IncludeStackTrace { get; set; } // auto in Development
}

Extension methods on IPragmaticBuilder for development convenience:

MethodDescription
UseDatabaseEnsureCreated()Calls EF Core EnsureCreatedAsync on startup (dev only)
UseDatabaseMigrate()Applies pending EF Core migrations on startup (production)

PragmaticApp is a static class with stub methods. The source generator replaces the implementation based on discovered [PragmaticMetadata] attributes:

MethodDescription
RunAsync(string[] args, Action<IPragmaticBuilder>? configure)Web application host
RunWorkerAsync(string[] args, Action<IPragmaticBuilder>? configure)Background worker host
RunConsoleAsync(string[] args, Action<IPragmaticBuilder>? configure)Console application host

Detailed guides in docs/:

GuideWhat You’ll Learn
Architecture and Concepts3-Tier model, module system, IPragmaticBuilder, what gets generated
Getting StartedMinimal host setup, adding modules step by step
Startup PipelineIStartupStep ordering, ConfigureServices vs ConfigurePipeline
Service Registration[Service], [Decorator], [ServiceFactory], keyed services
Remote Boundaries[RemoteBoundary], HttpActionInvoker, distributed deployment
Common MistakesPatterns to avoid and how to fix them
TroubleshootingProblem/solution guide, diagnostics reference, FAQ
With ModuleIntegration
Pragmatic.ActionsAddMyAppActions() registers all action invokers
Pragmatic.ValidationAddGeneratedValidators() registers all [Validator] types
Pragmatic.PersistenceAddBillingRepositories() + AddBillingDbContext() in pipeline step
Pragmatic.EndpointsMapPragmaticEndpoints() in ConfigurePipeline()
IDSeverityCategoryDescription
PRAG1050ErrorPackageDuplicate [UsePackage<T>] on module
PRAG1600ErrorTopologyModule requires unavailable middleware
PRAG1601ErrorTopologyModule dependency not declared
PRAG1602ErrorTopologyCircular dependency detected
PRAG1605WarningTopology[Service] without Pragmatic.Composition reference
PRAG1606ErrorTopology[Decorator] requires Pragmatic.Composition reference
PRAG1610ErrorSchemaIncompatible metadata schema version
PRAG1611WarningSchemaNewer metadata schema version than supported
PRAG1612InfoSchemaLegacy metadata schema version (backward compat)
PRAG1620ErrorInjection[Inject] references unregistered service
PRAG1621ErrorInjection[Inject] creates circular reference
PRAG1630ErrorStartup[StartupStep] must implement IStartupStep
PRAG1631ErrorStartup[StartupStep] must be on a class
PRAG1632ErrorStartup[NeedsStep<T>] references unavailable step type
PRAG1640ErrorService[Service] requires a class type
PRAG1641WarningServiceDependency not registered
PRAG1642WarningServiceSingleton depends on scoped service (captive dependency)
PRAG1643WarningServiceNo interface found for service
PRAG1644InfoServiceService registered
PRAG1645ErrorServiceAbstract class cannot be a service
PRAG1646WarningServiceKeyed services require .NET 8+
PRAG1651WarningDatabaseBoundary has no database configured
PRAG1652ErrorDatabaseDbContext name collision across different databases
PRAG1660ErrorDecoratorDecorator must implement at least one interface
PRAG1661ErrorDecoratorDecorator missing inner service constructor parameter
PRAG1670ErrorEvents[EventHandler] on class not implementing IDomainEventHandler<T>
PRAG1680WarningPackage[ExposeEndpoint<T>] references non-package action
PRAG1685ErrorRemote[RemoteBoundary<T>] and [Include<T>] on same module
PRAG1686WarningRemote[RemoteBoundary<T>] module has no actions
PRAG1687InfoRemote[RemoteBoundary<T>] base URL not configured
PRAG1690InfoDiscoveryDiscovered Pragmatic modules count
PRAG1691InfoDiscoveryModule composition info
PRAG1693InfoDiscoveryNo [Service] or [Decorator] registrations found
PRAG1694InfoDiscoveryNo [StartupStep] registrations found

See samples/Pragmatic.Composition.Samples/ for 3 conceptual scenarios: three-tier model (Topology → Strategy → Business), IStartupStep (ordering, ConfigureServices, ConfigurePipeline), and SG output (Host.Entry, Host.Services, auto-registration).

  • .NET 10.0+
  • Pragmatic.SourceGenerator analyzer

Part of the Pragmatic.Design ecosystem.