Skip to content

Architecture and Core Concepts

This guide explains why Pragmatic.Composition exists, how its pieces fit together, and how to choose the right abstraction for each situation. Read this before diving into the individual feature guides.


As a .NET application grows, dependency injection and application startup accumulate boilerplate that scatters across Program.cs, extension methods, and configuration helpers. The developer becomes responsible for keeping all of it consistent, ordered, and discoverable.

var builder = WebApplication.CreateBuilder(args);
// Service registration -- one line per service, repeated for every module
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
builder.Services.AddScoped<IShippingService, ShippingService>();
builder.Services.AddScoped<IPaymentService, PaymentService>();
builder.Services.AddScoped<INotificationService, NotificationService>();
builder.Services.AddScoped<IAuditService, AuditService>();
// ... 50 more lines, growing with every new class
// Decorators -- manual ordering, easy to get wrong
builder.Services.Decorate<IOrderService, LoggingOrderService>();
builder.Services.Decorate<IOrderService, CachingOrderService>();
// Infra modules -- scattered, no standard ordering
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 => { /* ... */ });
builder.Services.AddAuthorization();
builder.Services.AddCors();
builder.Services.AddResponseCompression();
builder.Services.AddHealthChecks();
var app = builder.Build();
// Middleware -- order matters, but nothing enforces it
app.UseResponseCompression();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();

A Program.cs with 20 services: 80+ lines. Most of it is mechanical wiring. Add a new module and you must remember to register its services, its DbContext, its middleware, and its health checks — all in the right order, in the right place. Forget one line and the application silently misbehaves.

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Startup>());
builder.Services.AddAutoMapper(typeof(Startup));
builder.Services.AddValidatorsFromAssemblyContaining<OrderValidator>();

Solves the verbosity problem but introduces runtime reflection, hidden lifetime defaults, and no compile-time validation. When a service fails to resolve, the stack trace points to ActivatorUtilities.CreateInstance deep inside the DI container — not to the missing registration.

  1. No module boundaries. All services register into one flat container. Nothing prevents the Billing module from depending on Booking internals.
  2. No standard startup ordering. Middleware must be ordered correctly, but the ordering is implicit and fragile. Move one line and authentication breaks.
  3. No compile-time validation. Missing registrations, lifetime mismatches, and circular dependencies surface only at runtime.
  4. No topology awareness. The application does not know which modules it contains, which databases they use, or how they relate to each other.

Pragmatic.Composition inverts the model. You declare the application’s structure — modules, boundaries, databases, services — and the source generator produces all registration code at compile time.

The same application from above:

// Program.cs -- the ENTIRE file
await PragmaticApp.RunAsync(args, app =>
{
if (app.Environment.IsDevelopment())
app.UseDatabaseEnsureCreated();
app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");
}).ConfigureAwait(false);
// Module topology (one class per module)
[Module]
[Include<OrdersModule, AppDatabase>]
[Include<BillingModule, FinancialDatabase>]
[Include<ShippingModule, AppDatabase>]
[NeedsStep<RoutingStep>]
public sealed class MyAppModule;
// Service registration (on the class itself)
[Service]
public class OrderService(IOrderRepository repository) : IOrderService { }

The source generator reads all [Module], [Service], [Decorator], [StartupStep], and [Include] declarations at compile time and produces:

  • PragmaticApp.RunAsync — the complete host startup implementation
  • PragmaticHost.RegisterAllPragmaticServices — infrastructure module auto-registration
  • PragmaticHost.CallConfigureServices — ordered invocation of all IStartupStep implementations
  • PragmaticHost.ConfigurePipeline — ordered middleware pipeline setup
  • PragmaticHost.MapAllEndpoints — endpoint route mapping
  • Service registration code per assembly — AddMyAppServices() extension methods
  • Database registration — DbContext setup per boundary
  • Topology validation — circular dependency detection, missing boundary checks, lifetime mismatch warnings

All at compile time. Zero reflection at runtime. Every generated file is visible under obj/ and fully debuggable.


Pragmatic.Composition organizes startup into three tiers, each with a distinct responsibility and a fixed execution order.

+---------------------------+ +---------------------------+ +---------------------------+
| Tier 1: Topology | | Tier 2: Module Strategy | | Tier 3: Business Wiring |
| | | | | |
| [Module] | --> | IPragmaticBuilder | --> | IStartupStep |
| [Include<T, TDb>] | | Use*() extensions | | ConfigureServices() |
| [BelongsTo<T>] | | Program.cs callback | | ConfigurePipeline() |
| [IncludeModule<T>] | | | | |
| | | Developer configures | | Developer configures |
| SG auto-detects | | infrastructure choices | | business logic |
+---------------------------+ +---------------------------+ +---------------------------+
Compile-time Before steps After defaults + builder
1. WebApplicationBuilder created
2. SG-generated infrastructure defaults (Tier 1 detection -> Tier 2 auto-registration)
3. IPragmaticBuilder callback (Tier 2 overrides via DI last-registration-wins)
4. IStartupStep.ConfigureServices (Tier 3, sorted by Order ascending)
5. WebApplication built
6. IStartupStep.ConfigurePipeline (Tier 3, sorted by Order ascending)
7. Endpoint mapping
8. Application runs

This ensures a deterministic, predictable startup sequence. Infrastructure defaults are established first, the developer overrides what they need, and business logic wires last.


Modules are the building blocks of a Pragmatic application. They define boundaries, declare dependencies, and organize code into cohesive units.

A module is an empty sealed class decorated with [Module]:

[Module(Name = "MyApp.Catalog", Version = "1.0.0", Description = "Product catalog management")]
public sealed class CatalogModule;

The Name, Version, and Description properties are optional metadata used in topology reports and diagnostics. The class itself has no methods or properties — it is a marker type.

There are two categories of modules:

CategoryWherePurpose
Domain moduleClass library projectContains entities, actions, services for a bounded context
Host moduleExecutable projectDeclares deployment topology: which modules are included and how

The source generator automatically distinguishes them based on the output type of the project. An executable project (.csproj with <OutputType>Exe</OutputType>) triggers Host mode; a class library triggers Library mode.

When a domain module depends on types from another module (e.g., Booking needs Catalog for property lookups), declare it with [IncludeModule<T>]:

[Module(Name = "MyApp.Booking", Description = "Reservation management")]
[IncludeModule<CatalogModule>]
public sealed class BookingModule;

This is a compile-time dependency declaration. It does not register services — it tells the SG (and other developers) that Booking expects Catalog to be present.

The host module declares the deployment topology — which domain modules are included, which databases they use, and which infrastructure steps are needed:

[Module]
[Include<AccountsModule, AppDatabase>]
[Include<BillingModule, FinancialDatabase>]
[Include<BookingModule, AppDatabase>]
[Include<CatalogModule, AppDatabase>]
[NeedsStep<InternationalizationStep>]
[NeedsStep<RoutingStep>]
public sealed class ShowcaseHostModule;

The [Include] attribute has three overloads:

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

Physical databases are declared by deriving from PragmaticDatabase:

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

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

The SG generates a DbContext per boundary-database pair. Multiple modules sharing the same database class get the same DbContext, ensuring entities are co-located in one migration set.

Entities and actions declare which boundary they belong to using [BelongsTo<T>]:

[Entity]
[BelongsTo<BookingBoundary>]
public partial class Reservation { /* ... */ }

This enables the SG to route entity configurations, repositories, and actions to the correct DbContext and registration group.

Generated ArtifactContentWhen
[PragmaticMetadata] assembly attributesModule info, service registrations, startup steps as JSON metadataLibrary mode (every class library)
_Infra.DI.ServiceRegistration.g.csAdd{Prefix}Services() extension method with all [Service] and [Decorator] registrationsLibrary mode, when services exist
_Infra.DI.PipelineStepRegistration.g.csStep registration codeLibrary mode, when startup steps exist
Host.Entry.g.csPragmaticApp.RunAsync() implementationHost mode
Host.Services.g.csPragmaticHost static class with all aggregated registrationsHost mode
Host.Topology.g.csTopology report string (Debug builds only)Host mode

IPragmaticBuilder is the Tier 2 configuration surface. It runs in Program.cs as a callback to PragmaticApp.RunAsync and is your single point for infrastructure decisions.

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

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

The concrete implementation is PragmaticBuilder in Pragmatic.Composition.Host, which adds:

public PragmaticOptions Options { get; }

PragmaticOptions controls cross-cutting hosting concerns: telemetry, maintenance mode, database initialization, and JSON serialization defaults.

Each Pragmatic infrastructure module contributes extension methods on IPragmaticBuilder. IntelliSense shows only the methods for packages you have referenced:

await PragmaticApp.RunAsync(args, app =>
{
// Database initialization (from Pragmatic.Persistence.EFCore)
app.UseDatabaseEnsureCreated();
// Multi-tenancy strategy (from Pragmatic.MultiTenancy)
app.UseMultiTenancy(mt => mt.UseHeader());
// Authentication handler (from Pragmatic.Identity)
app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");
// JWT authentication (from Pragmatic.Identity.Jwt)
app.UseJwtAuthentication(jwt =>
{
jwt.SigningKey = app.Configuration["Jwt:Key"]!;
jwt.Issuer = "my-app";
});
// Authorization configuration (from Pragmatic.Authorization)
app.UseAuthorization(authz =>
{
authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
authz.MapRole<AdminRole>();
});
// Logging sinks (from Pragmatic.Logging)
app.UseLogging(log =>
{
log.AddConsole(PragmaticConsoleConfiguration.ForDevelopment());
log.AddFile("logs/app-{Date}.log");
});
// File storage strategy (from Pragmatic.Storage)
app.UseStorage(sp => new LocalDiskFileStorage(basePath, sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));
});

How Overrides Work: Last-Registration-Wins

Section titled “How Overrides Work: Last-Registration-Wins”

The SG auto-registers infrastructure defaults based on detected packages (see Auto-Registration below). The IPragmaticBuilder callback runs after these defaults. Because .NET DI uses last-registration-wins semantics, your Use*() calls replace the defaults:

Step 1: SG auto-registers SystemClock as IClock (Singleton)
Step 2: You call app.UseClock<FakeClock>() in the callback
Result: FakeClock wins (last registration)

This pattern means you never need to “remove” a default — you simply override it.

The builder’s Options property controls host-level behavior:

PropertyTypeDefaultDescription
EnsureDatabaseCreatedboolfalseCall EnsureCreatedAsync on all DbContexts at startup
AutoMigrationsboolfalseApply pending EF Core migrations at startup
TelemetryTelemetryOptions(enabled)OpenTelemetry configuration
MaintenanceModeMaintenanceModeOptions(enabled)Maintenance mode on startup failure

IStartupStep is the Tier 3 configuration surface. It handles business-level wiring: registering application services, configuring middleware, and setting up infrastructure that depends on business decisions.

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

Both methods have default implementations (no-op), so you implement only what you need. Mark implementations with [StartupStep] for auto-discovery:

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

The generated host executes startup steps in two passes, both sorted by Order ascending:

  1. Phase 1 — ConfigureServices: Runs before builder.Build(). Registers DI services.
  2. Phase 2 — ConfigurePipeline: Runs after builder.Build(). Configures HTTP middleware.

Steps that only implement ConfigureServices (no ConfigurePipeline) participate in Phase 1 but produce no middleware.

Follow these conventions to avoid ordering conflicts:

RangePurposeExamples
0-24Early infrastructureCustom exception handlers
25Response compressionResponseCompressionStep (built-in)
26-49Pre-routing middlewareStatic files, HTTPS redirect
50RoutingRoutingStep (built-in)
51-74Post-routing, pre-authTenant resolution
75CORSCorsStep (built-in)
76-99Pre-auth infrastructureRate limiting, request logging
100-499Module stepsAuthentication (100), Authorization (110), I18n (200)
500+Application stepsBusiness services, OpenAPI, custom middleware

Pragmatic.Composition.Host provides three infrastructure steps that are automatically included when you use [NeedsStep<T>]:

StepOrderPipeline Action
ResponseCompressionStep25app.UseResponseCompression()
RoutingStep50app.UseRouting()
CorsStep75app.UseCors()

Modules declare which steps they need via [NeedsStep<T>] on the host module:

[Module]
[Include<BookingModule, AppDatabase>]
[NeedsStep<RoutingStep>]
[NeedsStep<InternationalizationStep>]
public sealed class ShowcaseHostModule;

The SG aggregates all [NeedsStep<T>] declarations, deduplicates by type, and orders by Order. You do not instantiate steps manually.

[RequiresConfig("path")] declares that a configuration key must exist at startup:

[StartupStep]
[RequiresConfig("ConnectionStrings:App")]
[RequiresConfig("Jwt:Key", Description = "JWT signing key for token validation")]
public class SecureStartupStep : IStartupStep { /* ... */ }

The SG generates a ValidateConfiguration() call that runs before any step. If a required key is missing, the application fails fast with a clear error listing all missing keys.

IPragmaticBuilder vs IStartupStep — When to Use Which

Section titled “IPragmaticBuilder vs IStartupStep — When to Use Which”
AspectIPragmaticBuilderIStartupStep
WhenBefore all stepsAfter SG defaults + builder
ScopeInfrastructure choicesBusiness wiring
WhatWhich auth handler, which storage, which loggingApp services, query filters, middleware
WhoDeveloper configuring the hostDeveloper configuring business logic
CountOne (callback in RunAsync)Multiple, ordered by Order
HTTP PipelineNoYes (ConfigurePipeline)
Exampleapp.UseAuthentication<T>(...)services.AddScoped<IQueryFilter, ...>()

The rule of thumb: if it is an infrastructure decision (auth strategy, logging sink, database mode), use IPragmaticBuilder. If it is business logic (services, filters, middleware), use IStartupStep.


PragmaticApp is a static class with stub methods in Pragmatic.Composition.Host. The source generator replaces the implementation with fully wired startup code based on discovered modules.

MethodDescriptionUse Case
RunAsync(args, configure?)Web application host with full HTTP pipelineAPIs, web apps
RunWorkerAsync(args, configure?)Background worker host without HTTPBackground jobs, message consumers
RunConsoleAsync(args, configure?)Console application hostCLI tools, one-shot scripts

The source generator produces a PragmaticApp.RunAsync method that follows this sequence:

1. Create WebApplicationBuilder
2. Validate required configuration (fail-fast)
3. RegisterAllPragmaticServices (SG defaults for detected infra modules)
4. Create PragmaticBuilder and invoke configure callback
5. Configure telemetry (OpenTelemetry auto-setup)
6. Configure JSON defaults (camelCase, enum-as-string, null handling)
7. Register databases (DbContext per boundary-database pair)
8. Call ConfigureServices on all startup steps (sorted by Order)
9. Build the WebApplication
10. Database initialization (EnsureCreated or Migrate if enabled)
11. Configure pipeline (all startup steps, sorted by Order)
12. Map all endpoints
13. Run the application
14. On failure: enter maintenance mode (if enabled)

When the application fails to start and MaintenanceModeOptions.EnableOnStartupFailure is true (default), the application enters maintenance mode 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

This ensures monitoring systems can detect the failure and operators can diagnose the issue without SSH access to the container.


Services are the runtime types that compose your application. Pragmatic.Composition provides multiple registration mechanisms, all of which produce compile-time code (except assembly scanning, which is runtime-based).

The simplest and recommended approach:

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

The SG generates services.AddScoped<IOrderService, OrderService>() (and equivalent for other lifetimes) at compile time. Cross-assembly discovery works through [PragmaticMetadata] assembly attributes — each library declares what it registers, the host aggregates automatically.

Decorators wrap existing services with cross-cutting behavior:

[Decorator(Order = 1)]
public class LoggingPricingService(
IReservationPricingService inner,
ILogger<LoggingPricingService> logger) : IReservationPricingService
{
public decimal CalculatePrice(Reservation reservation)
{
logger.LogInformation("Calculating price...");
return inner.CalculatePrice(reservation);
}
}

Order semantics: Lower order = closer to the real service. Higher order = outermost wrapper. Call flow: Outer (Order=2) -> Inner (Order=1) -> RealService.

For services that need custom construction logic:

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

For optional dependencies or post-construction initialization:

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

For convention-based registration without attributes (runtime reflection):

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

Use [Service] for all known services. Reserve scanning for bulk convention-based scenarios where attribute decoration is impractical.


Auto-Registration of Infrastructure Modules

Section titled “Auto-Registration of Infrastructure Modules”

When a Pragmatic infrastructure package is referenced in the host’s .csproj, the source generator auto-detects it via FeatureDetector and registers sensible defaults. This is Tier 1 -> Tier 2 automation.

ModuleDetection MarkerDefault Registration
TemporalPragmatic.Temporal.IClockSystemClock as IClock (Singleton)
ResiliencePragmatic.Resilience assemblyAddPragmaticResilience() + bind ResilienceOptions
CachingPragmatic.Caching assemblyAddHybridCache() + AddPragmaticCaching()
I18nPragmatic.Internationalization assemblyAddPragmaticI18n()
IdentityPragmatic.Identity.AspNetCore assemblyAddPragmaticAuthorization()
MultiTenancyPragmatic.MultiTenancy assemblyAddPragmaticMultiTenancy() (SingleTenant default)
FeatureFlagsPragmatic.FeatureFlags assemblyAddPragmaticFeatureFlags()
DiscoveryPragmatic.Discovery assemblyAddPragmaticDiscovery()
AuthorizationPragmatic.Authorization assemblyAddPragmaticAuthorization()

FeatureDetector runs at compile time inside the SG. It checks for well-known type names using compilation.GetTypeByMetadataName(). When a type is found, the corresponding DetectedFeatures flag is set to true. The PragmaticHostTemplate uses these flags to generate RegisterAllPragmaticServices().

Referenced NuGet: Pragmatic.MultiTenancy
-> FeatureDetector finds Pragmatic.MultiTenancy types
-> DetectedFeatures.HasMultiTenancy = true
-> PragmaticHostTemplate generates: services.AddPragmaticMultiTenancy()
-> You can override in IPragmaticBuilder: app.UseMultiTenancy(mt => mt.UseHeader())

This model means adding a capability is as simple as adding a NuGet reference. Remove the reference and the registration disappears. No manual wiring needed.


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

  1. Library mode: The SG generates [assembly: PragmaticMetadata(category, json)] attributes for every [Service], [Decorator], [StartupStep], [Module], and [EventHandler] in the assembly.
  2. Host mode: The SG reads [PragmaticMetadata] from all referenced assemblies (via MetadataReader), deserializes the JSON, and generates a unified PragmaticHost class that aggregates all registrations.

MetadataCategory enum values control how metadata is grouped:

CategoryContent
DIService and decorator registrations
StartupPipeline step declarations
ModuleModule topology (name, version, dependencies)
ActionsDomain action declarations
EndpointsEndpoint route registrations
ValidationValidator registrations
MappingMapping configuration
PersistenceEntity and repository metadata
EventHandlersDomain event handler registrations
HealthChecksHealth check registrations
IdentifiersCustom ID type converters
TranslationsTranslation key registrations
CachingCache configuration metadata
ConfigurationConfiguration section bindings
HostTopologyHost-level includes and databases

This mechanism is invisible to the developer. You add [Service] to a class in a library, and the host automatically picks it up.


Packages are pre-built collections of entities, actions, and services that fuse into a host module. They are the distribution mechanism for reusable bounded contexts.

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";
}
[Module(Name = "MyApp.Accounts")]
[UsePackage<LocalIdentityPackage>]
public sealed class AccountsModule;

When a module uses a package, the SG:

  1. Includes the package’s services, actions, and entities in the module’s registrations
  2. Routes package endpoints under the package’s RoutePrefix
  3. Validates that required dependencies are available
PropertyTypeRequiredDescription
PackageNamestringYesUnique package identifier
RoutePrefixstring?NoURL prefix for package endpoints
Descriptionstring?NoHuman-readable description

For distributed deployments, [RemoteBoundary<TModule>] replaces a local module with HTTP-based action invokers. The calling code does not change — it still depends on IDomainActionInvoker<TAction, TReturn> — but the implementation sends HTTP requests instead of executing in-process.

// Monolith: all modules local
[Module]
[Include<BookingModule, AppDatabase>]
[Include<BillingModule, FinancialDatabase>]
public sealed class MonolithHostModule;
// Distributed: Billing runs on a separate service
[Module]
[Include<BookingModule, AppDatabase>]
[RemoteBoundary<BillingModule>]
public sealed class DistributedHostModule;

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. The calling code is identical in both topologies.

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

For full details, see Remote Boundaries.


The source generator operates in two modes based on the project type:

Generated for every class library that references Pragmatic.Composition:

Generated FileContentCondition
_Infra.DI.ServiceRegistration.g.csAdd{Prefix}Services() extension methodHas [Service] or [Decorator] types
_Infra.DI.PipelineStepRegistration.g.csStep registration codeHas [StartupStep] types
_Infra.DI.ServiceMetadata.g.cs[PragmaticMetadata] for DI registrationsHas services
_Infra.DI.StartupMetadata.g.cs[PragmaticMetadata] for startup stepsHas startup steps
_Infra.DI.ModuleMetadata.g.cs[PragmaticMetadata] for module topologyHas [Module] class
_Infra.DI.EventHandlerMetadata.g.cs[PragmaticMetadata] for event handlersHas [EventHandler] types

Generated for the host project (the .csproj with <OutputType>Exe</OutputType>):

Generated FileContentCondition
Host.Entry.g.csPragmaticApp.RunAsync(), RunWorkerAsync(), RunConsoleAsync()Always
Host.Services.g.csPragmaticHost class with all registration methodsAlways
Host.Topology.g.csTopology report string (Debug only)Debug builds
Host.EventHandlers.g.csEvent handler registrationsHas event handlers
Host.HttpInvoker.{Module}.g.csHTTP action invokers per remote boundaryHas [RemoteBoundary]
Host.InvokeEndpoint.g.cs/_pragmatic/invoke dispatcherHas local actions

PragmaticHost Methods (Host.Services.g.cs)

Section titled “PragmaticHost Methods (Host.Services.g.cs)”

The PragmaticHost static class contains categorized registration methods:

MethodPurpose
RegisterAllPragmaticServicesInfrastructure defaults (Tier 1 auto-detection)
RegisterAllRepositoriesAll repository + read-repository registrations per boundary
RegisterAllDomainActionsAll domain action invoker registrations
RegisterAllPipelineStepsAll IStartupStep instantiation and registration
RegisterAllDatabasesDbContext registrations per database-boundary pair
RegisterAllServicesService + decorator registrations from all assemblies
CallConfigureServicesOrdered invocation of step ConfigureServices methods
ConfigurePipelineOrdered invocation of step ConfigurePipeline methods
MapAllEndpointsEndpoint route mapping via MapPragmaticEndpoints()
ValidateConfigurationFail-fast validation of [RequiresConfig] keys

Pragmatic.Composition is the orchestration layer. Every other Pragmatic module plugs into it to register services, expose endpoints, and participate in the startup pipeline.

ModuleIntegration PointWhat Gets Registered
Actions[PragmaticMetadata(Actions)]Action invokers via RegisterAllDomainActions()
Endpoints[PragmaticMetadata(Endpoints)]Routes via MapAllEndpoints()
Persistence[PragmaticMetadata(Persistence)]Repositories, query filters, DbContexts
Validation[PragmaticMetadata(Validation)]Validators via generated extension methods
Mapping[PragmaticMetadata(Mapping)]Mapping configurations (static, no DI needed)
Events[PragmaticMetadata(EventHandlers)]Domain event handlers
Caching[PragmaticMetadata(Caching)]Cache profiles and invalidation rules
IdentityAuto-detectedAuthentication and authorization pipeline
MultiTenancyAuto-detectedTenant resolution middleware and services
ResilienceAuto-detectedResilience policies and circuit breakers

You never manually wire these integrations. Reference a Pragmatic package in your .csproj, add the attributes to your types, and the SG generates all registration code. This is what “composition by presence” means:

  1. Add Pragmatic.Validation to your .csproj
  2. Add [Validator] to your validator class
  3. The SG generates AddGeneratedValidators() in the library
  4. The host SG discovers the [PragmaticMetadata] attribute and includes the call in RegisterAllPragmaticServices()

No manual services.AddXxx() needed.


The generated PragmaticApp.RunAsync automatically configures OpenTelemetry with sensible defaults:

  • 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.

Configure via PragmaticOptions.Telemetry or appsettings.json:

{
"Telemetry": {
"Enabled": true,
"UseOtlpExporter": true,
"SamplingRatio": 0.1
}
}