Skip to content

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
TierWhereWhatWho writes it
Topology[Module], [Boundary], [BelongsTo<T>], [UsePackage<T>]Compile-time structure and module dependenciesSource Generator (automatic)
Module StrategyProgram.cs via IPragmaticBuilder.Use*()Infrastructure choices: auth handler, storage backend, cache providerDeveloper (explicit)
Business WiringIStartupStepServices, query filters, OpenAPI, feature-specific DIDeveloper (explicit)

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 (UsePackage metadata 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>>()));
});

These are the strategy extensions that ship today on IPragmaticBuilder:

ExtensionPurposeModule
UseAuthorizationRole/group/permission stores, resource authorizers, permission cacheAuthorization
UseJwtAuthenticationJWT bearer authenticationIdentity.Local.Jwt
UseAuthentication<THandler>Plug a custom (or NoOp) authentication handlerIdentity
UseIdentityPersistence<TUser, TKey>EF-backed identity stores, JIT provisioningIdentity.Persistence
UseMultiTenancyTenant resolver (.UseHeader()); .UseDbPerTenant() for DB-per-tenantMultiTenancy
UseMessagingTransport (.UseChannels() / .UseRabbitMq() / .UseKafka()), outbox, .EnableAuditing(), sagaMessaging
UseJobsWorker count, polling; .UseEfCorePersistence() for durable jobsJobs
UsePragmaticMigrationsDeclarative schema diff, auto-apply on startupMigrations
UseStorageFile storage backend (provider factory)Storage
UseI18NCultures, JSON translations, ProblemDetails localizationInternationalization
UseNotificationsDelivery channels (.AddSmtp(...)), audienceNotifications
UseEmailTemplated email rendering + transportEmail
UseLoggingConsole / rolling-file sinksLogging
UseMaintenanceModeRuntime maintenance toggle + admin panelComposition.Host
UseAgentLocal coordination daemon connectionAgent
UseControlPlaneHubCluster coordination hubControlPlane

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.

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

NeedTier
”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

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.

// Program.cs — minimal Pragmatic app
using Pragmatic.Composition.Hosting;
await PragmaticApp.RunAsync(args);
// Every module's safe default applies automatically (in-memory / passthrough / allow-all).
// Program.cs — production-shaped app
using 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.