Configuration
Pragmatic Design uses a 3-tier configuration model. Each tier answers a different question and lives in a different place.
Topology (compile-time) → Module Strategy (Program.cs) → Business Wiring (IStartupStep) SG auto-detect IPragmaticBuilder IStartupStep| Tier | Where | What | Who writes it |
|---|---|---|---|
| Topology | [Module], [Boundary], [BelongsTo<T>], [UsePackage<T>] | Compile-time structure and module dependencies | Source Generator (automatic) |
| Module Strategy | Program.cs via IPragmaticBuilder.Use*() | Infrastructure choices: auth handler, storage backend, cache provider | Developer (explicit) |
| Business Wiring | IStartupStep | Services, query filters, OpenAPI, feature-specific DI | Developer (explicit) |
Tier 1 — Topology (Source Generator)
Section titled “Tier 1 — Topology (Source Generator)”You don’t configure topology — you declare it with attributes, and the unified Source Generator detects it:
[Module] public sealed class BookingModule;
[Boundary] public sealed class ReservationsBoundary;
[BelongsTo<ReservationsBoundary>]public partial class CreateReservation : DomainAction<ReservationResult> { ... }
[UsePackage<CommentsPackage>][assembly: Comments<Reservation>]At compile time the generator emits:
- module discovery (
PragmaticHost) — who is registered, in what order - package loading (
UsePackagemetadata fusion) - boundary/sub-boundary grouping for endpoints and actions
- feature flags (
DetectedFeatures) based on referenced assemblies
Nothing to tweak in Program.cs. If the Source Generator got it wrong, the fix is in the attribute declaration or the referenced NuGet.
Tier 2 — Module Strategy (IPragmaticBuilder)
Section titled “Tier 2 — Module Strategy (IPragmaticBuilder)”Modules that have a strategy to pick (auth scheme, storage backend, transport) expose a Use*() extension on the IPragmaticBuilder. You call them as statements inside the PragmaticApp.RunAsync callback (the parameter is the builder; it also exposes Configuration and Environment):
using Pragmatic.Composition.Hosting;
await PragmaticApp.RunAsync(args, app =>{ // Authentication — config-driven (JWT in production) app.UseJwtAuthentication(jwt => { jwt.SigningKey = app.Configuration["Jwt:Key"]!; jwt.Issuer = app.Configuration["Jwt:Issuer"]; });
// Authorization — compose roles from module definitions app.UseAuthorization(authz => authz.MapRole<BookingManager>());
// Multi-tenancy — resolve the tenant from an HTTP header app.UseMultiTenancy(mt => mt.UseHeader());
// Messaging — in-process Channels transport + auditing app.UseMessaging(msg => { msg.UseChannels(); msg.EnableAuditing(); });
// Background jobs app.UseJobs(jobs => jobs.WithWorkerCount(2));
// Declarative schema diff (replaces EF Core migrations) app.UsePragmaticMigrations();
// File storage — LocalDisk for dev; swap for Azure/S3 in prod app.UseStorage(sp => new LocalDiskFileStorage( Path.Combine(app.Environment.ContentRootPath, "wwwroot"), sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));});Available Use*() extensions (0.8 preview)
Section titled “Available Use*() extensions (0.8 preview)”These are the strategy extensions that ship today on IPragmaticBuilder:
| Extension | Purpose | Module |
|---|---|---|
UseAuthorization | Role/group/permission stores, resource authorizers, permission cache | Authorization |
UseJwtAuthentication | JWT bearer authentication | Identity.Local.Jwt |
UseAuthentication<THandler> | Plug a custom (or NoOp) authentication handler | Identity |
UseIdentityPersistence<TUser, TKey> | EF-backed identity stores, JIT provisioning | Identity.Persistence |
UseMultiTenancy | Tenant resolver (.UseHeader()); .UseDbPerTenant() for DB-per-tenant | MultiTenancy |
UseMessaging | Transport (.UseChannels() / .UseRabbitMq() / .UseKafka()), outbox, .EnableAuditing(), saga | Messaging |
UseJobs | Worker count, polling; .UseEfCorePersistence() for durable jobs | Jobs |
UsePragmaticMigrations | Declarative schema diff, auto-apply on startup | Migrations |
UseStorage | File storage backend (provider factory) | Storage |
UseI18N | Cultures, JSON translations, ProblemDetails localization | Internationalization |
UseNotifications | Delivery channels (.AddSmtp(...)), audience | Notifications |
UseEmail | Templated email rendering + transport | |
UseLogging | Console / rolling-file sinks | Logging |
UseMaintenanceMode | Runtime maintenance toggle + admin panel | Composition.Host |
UseAgent | Local coordination daemon connection | Agent |
UseControlPlaneHub | Cluster coordination hub | ControlPlane |
Default rule: if you don’t call a module’s Use*(), the framework registers a safe in-memory / passthrough / allow-all default. A fresh Program.cs with just await PragmaticApp.RunAsync(args) already boots and runs.
Tier 3 — Business Wiring (IStartupStep)
Section titled “Tier 3 — Business Wiring (IStartupStep)”Business decisions that don’t belong in a module strategy go in one or more IStartupStep implementations — classes the host discovers and runs in order.
public sealed class BookingStartup : IStartupStep{ public int Order => 100; // earlier = runs first
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IRoomPricingService, DynamicPricingService>(); services.AddDataScopeRule<HighSeasonRoomScopeRule, Room>(); }
public void ConfigurePipeline(WebApplication app) { app.MapHub<ReservationsHub>("/hubs/reservations"); }}IStartupStep is discovered automatically at compile time by the Source Generator — no manual registration. Multiple steps run in Order ascending (default: 1000).
When to use which
Section titled “When to use which”| Need | Tier |
|---|---|
| ”Which auth scheme?” / “Which cache provider?” | Tier 2 (IPragmaticBuilder.Use*()) |
| “Register a service the app needs” | Tier 3 (IStartupStep.ConfigureServices) |
| “Customize HTTP pipeline (SignalR hub, custom middleware)“ | Tier 3 (IStartupStep.ConfigurePipeline) |
| “Add a domain event handler” | Implicit — the SG discovers IDomainEventHandler<T> automatically |
| ”Change how a module itself behaves” | Tier 2 if supported, else ask / open issue |
appsettings.json — where does it fit?
Section titled “appsettings.json — where does it fit?”Configuration values (connection strings, feature flags, tenant URLs) live in appsettings.json as usual:
{ "ConnectionStrings": { "App": "Host=localhost;Database=app" }, "Pragmatic": { "Identity": { "Local": { "DefaultPasswordPolicy": { "MinLength": 12 } } }, "Messaging": { "RabbitMQ": { "Uri": "amqp://localhost:5672" } }, "RemoteBoundaries": { "Billing": { "BaseUrl": "http://billing.internal" } } }}The host binds Pragmatic:* sections to strongly-typed options classes via IOptions<T>. Per-module keys are documented in each module’s README.md.
Cheat sheet
Section titled “Cheat sheet”// Program.cs — minimal Pragmatic appusing Pragmatic.Composition.Hosting;
await PragmaticApp.RunAsync(args);// Every module's safe default applies automatically (in-memory / passthrough / allow-all).// Program.cs — production-shaped appusing Pragmatic.Composition.Hosting;
await PragmaticApp.RunAsync(args, app =>{ app.UseJwtAuthentication(jwt => jwt.SigningKey = app.Configuration["Jwt:Key"]!); app.UseAuthorization(authz => authz .MapRole<BookingManager>() .UsePermissionCache(TimeSpan.FromMinutes(5))); app.UseMessaging(msg => { msg.UseRabbitMq(); msg.EnableAuditing(); }); app.UseJobs(jobs => jobs.UseEfCorePersistence()); app.UsePragmaticMigrations(); app.UseStorage(sp => new LocalDiskFileStorage( Path.Combine(app.Environment.ContentRootPath, "wwwroot"), sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));});For the per-module Use*() signature and options, see the module’s page in the sidebar.