Spring Host Architecture
Overview
Spring has three runtime host applications plus one local-development orchestrator. The runtime hosts are thin shells that wire infrastructure, and none contains business logic. The domain project defines what the system does. The hosts define how it runs.
| Host | Role | References |
|---|---|---|
Spring.Runtime | Orleans silo - runs grains, event sourcing, sagas | Spring.Domain + Mississippi Runtime SDK |
Spring.Gateway | ASP.NET API + Blazor host - serves endpoints and static files | Spring.Domain + Mississippi Gateway SDK |
Spring.Client | Blazor WebAssembly - UI shell with state management | Spring.Domain (compile-only) + Mississippi Client SDK |
Spring.AppHost | .NET Aspire orchestration for local development | Coordinates the runtime, gateway, storage, and emulator resources |
Spring.Runtime: The Orleans Host
The silo runs Orleans grains that execute commands, apply events, run effects, and manage saga orchestration. Its Program.cs is infrastructure wiring only.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// One call registers all domain aggregates, sagas, effects, and `EventReducer`s
builder.Services.AddSpringDomainSilo();
// Infrastructure: notification service stub
builder.Services.AddSingleton<INotificationService, StubNotificationService>();
// Infrastructure: telemetry, storage clients, event sourcing providers
builder.Services.AddHttpClient();
builder.Services.AddOpenTelemetry()
.WithTracing(/* ... */)
.WithMetrics(/* ... */);
builder.AddKeyedAzureTableServiceClient("clustering");
builder.AddKeyedAzureBlobServiceClient("grainstate");
builder.AddAzureCosmosClient("cosmos", /* ... */);
// Mississippi infrastructure
builder.Services.AddInletSilo();
builder.Services.ScanProjectionAssemblies(typeof(BankAccountBalanceProjection).Assembly);
builder.Services.AddJsonSerialization();
builder.Services.AddEventSourcingByService();
builder.Services.AddSnapshotCaching();
builder.Services.AddCosmosBrookStorageProvider(/* ... */);
builder.Services.AddCosmosSnapshotStorageProvider(/* ... */);
// Orleans configuration
builder.UseOrleans(siloBuilder =>
{
siloBuilder.AddActivityPropagation();
siloBuilder.UseAqueduct(options =>
options.StreamProviderName = "StreamProvider");
siloBuilder.AddEventSourcing(options =>
options.OrleansStreamProviderName = "StreamProvider");
});
WebApplication app = builder.Build();
app.MapGet("/health", /* ... */);
await app.RunAsync();
The single line builder.Services.AddSpringDomainSilo() registers every aggregate, saga, CommandHandler, EventReducer, effect, and projection defined in Spring.Domain. This method is source-generated by Mississippi - you do not write it manually.
What the Silo Owns
Beyond Program.cs, the silo contains a small set of non-generated support files:
Grains/GreeterGrain.cs- A simple demo grain (not event-sourced) that demonstrates basic Orleans communication.Grains/GreeterGrainLoggerExtensions.cs- Logging extension declarations used by the greeter grain.Services/StubNotificationService.cs- A stub implementation ofINotificationServicethat logs instead of sending real notifications.Services/StubNotificationServiceLoggerExtensions.cs- Logging extension declarations used by the stub notification service.
These files are infrastructure/support concerns rather than domain business logic.
(GreeterGrain.cs | StubNotificationService.cs)
Spring.Gateway: The API Host
The gateway host serves ASP.NET controllers, the Inlet SignalR hub, and the static files for the Blazor client. It also connects to the Orleans silo as a client.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
SpringAuthOptions springAuthOptions =
builder.Configuration.GetSection("SpringAuth").Get<SpringAuthOptions>() ?? new();
builder.Services.Configure<SpringAuthOptions>(builder.Configuration.GetSection("SpringAuth"));
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = springAuthOptions.Scheme;
options.DefaultChallengeScheme = springAuthOptions.Scheme;
})
.AddScheme<AuthenticationSchemeOptions, SpringLocalDevAuthenticationHandler>(
springAuthOptions.Scheme,
_ => { });
builder.Services.AddAuthorizationBuilder()
.AddPolicy("spring.generated-api", policy => policy.RequireAuthenticatedUser())
.AddPolicy("spring.write", policy => policy.RequireRole("banking-operator"))
.AddPolicy("spring.transfer", policy => policy.RequireRole("transfer-operator", "banking-operator"))
.AddPolicy("spring.auth-proof.claim", policy => policy.RequireClaim("spring.permission", "auth-proof"));
// Infrastructure: telemetry, Orleans client
builder.Services.AddOpenTelemetry()
.WithTracing(/* ... */)
.WithMetrics(/* ... */);
builder.AddKeyedAzureTableServiceClient("clustering");
builder.UseOrleansClient(clientBuilder =>
clientBuilder.AddActivityPropagation());
// ASP.NET and Mississippi infrastructure
builder.Services.AddControllers();
builder.Services.AddOpenApi(/* ... */);
builder.Services.AddJsonSerialization();
builder.Services.AddAggregateSupport();
builder.Services.AddUxProjections();
builder.Services.AddSignalR();
builder.Services.AddAqueduct<InletHub>(options =>
options.StreamProviderName = "StreamProvider");
if (springAuthOptions.Enabled)
{
builder.Services.AddInletServer(options =>
{
options.GeneratedApiAuthorization.Mode =
GeneratedApiAuthorizationMode.RequireAuthorizationForAllGeneratedEndpoints;
options.GeneratedApiAuthorization.DefaultPolicy = "spring.generated-api";
options.GeneratedApiAuthorization.AllowAnonymousOptOut = true;
});
}
else
{
builder.Services.AddInletServer();
}
builder.Services.ScanProjectionAssemblies(
typeof(BankAccountBalanceProjection).Assembly);
// Source-generated gateway registrations
builder.Services.AddAuthProofAggregateMappers();
builder.Services.AddBankAccountAggregateMappers();
builder.Services.AddMoneyTransferSagaAggregateMappers();
builder.Services.AddAuthProofProjectionMappers();
builder.Services.AddBankAccountBalanceProjectionMappers();
builder.Services.AddBankAccountLedgerProjectionMappers();
builder.Services.AddFlaggedTransactionsProjectionMappers();
builder.Services.AddMoneyTransferStatusProjectionMappers();
WebApplication app = builder.Build();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapOpenApi();
app.MapScalarApiReference(/* ... */);
app.MapControllers();
app.MapInletHub();
app.MapGet("/health", /* ... */);
app.MapFallbackToFile("index.html");
await app.RunAsync();
Spring.Gateway currently registers the generated aggregate and projection mapper extensions explicitly in Program.cs. Those mapper methods are source-generated from the annotations in Spring.Domain. The gateway still does not contain CommandHandler code, EventReducer code, or domain-specific business logic. It maps HTTP requests to Orleans grain calls and hosts the transport endpoints around that generated surface.
When SpringAuth:Enabled is true, the gateway enables generated API force mode with:
GeneratedApiAuthorization.Mode = RequireAuthorizationForAllGeneratedEndpointsGeneratedApiAuthorization.DefaultPolicy = "spring.generated-api"GeneratedApiAuthorization.AllowAnonymousOptOut = true
This applies a default authenticated policy to generated HTTP APIs while preserving explicit GenerateAllowAnonymous opt-outs.
Development Auth-Proof Mode
Spring includes an opt-in development mode that proves generated endpoint authorization behavior.
The complete setup, endpoint matrix (200/401/403), and troubleshooting guidance are documented on Spring Auth-Proof Mode.
What the Gateway Owns
The gateway has no domain-specific code files. Its Program.cs configures middleware and infrastructure. The API controllers that accept commands and return projections are entirely source-generated from the domain annotations.
Spring.Client: The Blazor UI
The client is a Blazor WebAssembly application that dispatches commands and subscribes to projections through the Mississippi client builder.
WebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped<AuthSimulationHeadersHandler>();
builder.Services.AddScoped(sp =>
{
AuthSimulationHeadersHandler authSimulationHeadersHandler = sp.GetRequiredService<AuthSimulationHeadersHandler>();
authSimulationHeadersHandler.InnerHandler = new HttpClientHandler();
return new HttpClient(authSimulationHeadersHandler)
{
BaseAddress = new(builder.HostEnvironment.BaseAddress),
};
});
builder.AddMississippiClient(client =>
{
client.AddMississippiSamplesSpringDomainClient();
client.Reservoir(reservoir =>
{
// UI features
reservoir.AddDualEntitySelectionFeature();
reservoir.AddDemoAccountsFeature();
reservoir.AddAuthSimulationFeature();
reservoir.AddReservoirBlazorBuiltIns();
reservoir.AddReservoirDevTools(options =>
{
options.Enablement = ReservoirDevToolsEnablement.Always;
options.Name = "Spring Sample";
options.IsStrictStateRehydrationEnabled = true;
});
// Real-time projection updates via SignalR
reservoir.AddInletClient();
reservoir.AddInletBlazorSignalR(signalR => signalR
.WithHubPath("/hubs/inlet")
.ScanProjectionDtos(typeof(BankAccountBalanceProjectionDto).Assembly));
});
});
await builder.Build().RunAsync();
The client now starts with builder.AddMississippiClient(...), uses the generated AddMississippiSamplesSpringDomainClient() domain compositor on MississippiClientBuilder, and then drops into client.Reservoir(...) for hand-written UI features plus Inlet registrations. The client still never directly calls Orleans grains or knows about event-sourcing internals.
How the Client References the Domain
The client's .csproj file uses a compile-only reference to Spring.Domain:
<ProjectReference Include="..\Spring.Domain\Spring.Domain.csproj"
ExcludeAssets="runtime" />
The ExcludeAssets="runtime" flag means the source generators can see domain types at compile time (to generate client-side DTOs and dispatchers), but the domain assembly is not deployed to the browser. The client only ships the generated code.
Source-Generated Registration Methods
Mississippi's generators produce builder-based client feature registrations and host-specific domain registrations from the annotations in Spring.Domain:
| Method | Host | What It Registers |
|---|---|---|
AddSpringDomainSilo() | Runtime | Aggregate grains, saga grains, CommandHandlers, EventReducers, effects, projection grains |
Add{Domain}Server() | Gateway | Domain-level gateway registration convenience method for generated API/controller mapper registrations |
Add{Aggregate}AggregateFeature(), Add{Saga}SagaFeature(), AddProjectionsFeature() | Client | Reservoir-level client feature registrations for generated state, reducers, effects, and projection support |
Add{Domain}Client() | Client | Mississippi client-builder convenience method that aggregates the generated Reservoir-level feature registrations |
Gateway generators can emit a domain-level convenience method, but Spring.Gateway currently composes the generated mapper registrations explicitly in Program.cs. The client-side feature generators still target IReservoirBuilder, while the domain client generator now targets MississippiClientBuilder and routes its work through client.Reservoir(...). Spring uses the generated domain client method for the write-side and projection slice, then adds hand-written UI and Inlet composition on the same Reservoir builder.
Spring.AppHost is separate from those generated methods. It is an Aspire entry point that provisions Azurite, Cosmos emulator resources, Orleans configuration, and project startup order for local development.
For more details on domain registration generators, see Domain Registration Generators.
The Key Insight
Compare the domain project to the host projects:
| Metric | Spring.Domain | Spring.Runtime | Spring.Gateway | Spring.Client |
|---|---|---|---|---|
| Domain business logic ownership | All domain business logic | No domain business logic | No domain business logic | No domain business logic |
| Business rules | All | None | None | None |
| Infrastructure wiring | None | Compact host setup in Program.cs | Compact host setup in Program.cs | Compact host setup in Program.cs |
| External dependencies | Primarily Mississippi abstractions, plus minimal framework/build dependencies (Microsoft.Orleans.Sdk, Microsoft.Extensions.Http) | Azure Storage, Cosmos, Orleans, OpenTelemetry | Orleans Client, ASP.NET, Blazor hosting | Blazor WASM, SignalR |
The hosts are replaceable shells. The domain is the permanent asset. You could swap Cosmos for PostgreSQL by changing only the runtime host's storage configuration. You could replace Blazor with React by writing a new client that calls the same generated API. The business logic in Spring.Domain would not change.
Summary
Mississippi's source generators transform domain annotations into infrastructure wiring. Spring.Runtime stays a thin Orleans host, Spring.Gateway composes generated gateway mapper registrations around its transport infrastructure, and Spring.Client now starts with AddMississippiClient(), uses the generated domain-level client method, and composes the remaining client features through client.Reservoir(...).
Next Steps
- Overview - Return to the Spring Sample App overview
- Key Concepts - Revisit the concept reference for all patterns used
- Domain Registration Generators - Deep dive into how generation works