Configuration
This page lists every option you can configure in Nagare, organized by package.
Database connection
Before registering event stores or projections, set up a database connection. Each adapter has its own registration method:
// PostgreSQL
builder.Services.AddNagarePostgresStorage(builder.Configuration);
// SQL Server
builder.Services.AddNagareSqlServerStorage(builder.Configuration);
// SQLite
builder.Services.AddNagareSqliteStorage(builder.Configuration);
// MySQL
builder.Services.AddNagareMySqlStorage(builder.Configuration);All three read a connection string from IConfiguration. By default, they look for "DbConnectionString" in the connection strings section:
{
"ConnectionStrings": {
"DbConnectionString": "Host=localhost;Database=myapp;Username=postgres;Password=secret"
}
}You can pass a different name:
builder.Services.AddNagarePostgresStorage(
builder.Configuration,
connectionStringName: "EventStoreDb");Event store
Register an event store for each event type in your system:
// PostgreSQL
builder.Services.AddPostgresEventStore<BookEvent>();
// SQL Server
builder.Services.AddSqlServerEventStore<BookEvent>();
// SQLite
builder.Services.AddSqliteEventStore<BookEvent>();
// MySQL
builder.Services.AddMySqlEventStore<BookEvent>();By default, events are stored in a table called Events. You can specify a different name:
builder.Services.AddPostgresEventStore<BookEvent>(tableName: "BookEvents");Snapshot store
builder.Services.AddPostgresSnapshotStore<BookState>();Default table name is Snapshots. Override it the same way:
builder.Services.AddPostgresSnapshotStore<BookState>(tableName: "BookSnapshots");Checkpoint store
Each database adapter registers its own checkpoint store and lock provider:
builder.Services.AddPostgresCheckpointStore();
// or
builder.Services.AddSqlServerCheckpointStore();
// or
builder.Services.AddSqliteCheckpointStore();
// or
builder.Services.AddMySqlCheckpointStore();The PostgreSQL, SQL Server, and MySQL checkpoint stores also register a distributed lock provider (PostgresLockProvider, MsSqlLockProvider, or MySqlLockProvider). SQLite registers NoopLockProvider since it's single-process.
Aggregates
Full registration shorthand
Each adapter provides a method that registers the event store, snapshot store, and aggregate in one call:
builder.Services.AddPostgresAggregate<
BookAggregate, BookCommand, BookEvent, BookState>();This is equivalent to:
builder.Services.AddPostgresEventStore<BookEvent>();
builder.Services.AddPostgresSnapshotStore<BookState>();
builder.Services.AddAggregate<BookAggregate, BookCommand, BookEvent, BookState>();AggregateOptions
Control how aggregates load their event history:
| Property | Type | Default | Purpose |
|---|---|---|---|
InitializeBatchSize | int | 50 | Number of events to read per batch when loading an aggregate |
There's no explicit options registration. The batch size is a property on the aggregate options record. Configure it when you have aggregates with long event histories and want to tune the page size for loading.
Subscriptions
PipelineHostingOptions
Every subscription, projection, message subscription, and process-manager event route runs on the same pipeline runtime, configured through PipelineHostingOptions:
builder.Services.AddPipeline<BookProjection, BookEvent>(new PipelineHostingOptions
{
PollDelay = TimeSpan.FromMilliseconds(200),
MaxFailureBackoff = TimeSpan.FromSeconds(60),
});| Property | Type | Default | Purpose |
|---|---|---|---|
PollDelay | TimeSpan | 100ms | Delay between polls when no new events are available |
MaxFailureBackoff | TimeSpan | 30s | Cap for the exponential backoff between consecutively failing sessions |
FaultAfterConsecutiveFailures | int | 5 | Failing sessions before the readiness health check reports the pipeline as faulted |
Resilience | ResiliencePipeline? | null | Optional Polly pipeline wrapping each processing session |
DeadLetterPolicy | DeadLetterPolicy? | null | Null = conveyor-belt default (block hard and retry); set to opt into retry-then-park |
PollDelay controls latency. Lower values mean projections catch up faster after new events are written, at the cost of more frequent database queries. For most applications, 100ms is a good balance.
Failure handling is the conveyor-belt default: when a handler throws, the pipeline blocks hard at that element. The checkpoint never advances past it, nothing behind it is processed, and the same element is retried with exponential backoff (up to MaxFailureBackoff) until it succeeds — no attempt budget, no skipping. The block is per-pipeline and loud, not fatal: other pipelines keep running, and after FaultAfterConsecutiveFailures failing sessions the readiness health check flips so the stall is visible. Skipping an element is only possible by explicitly setting a DeadLetterPolicy — see Dead-letter handling.
Database-specific subscription registration
// Registers the subscription with the correct checkpoint store for the database
builder.Services.AddPostgresSubscription<BookProjection, BookEvent>();
builder.Services.AddSqlServerSubscription<BookProjection, BookEvent>();
builder.Services.AddSqliteSubscription<BookProjection, BookEvent>();Repository stores
Register read model repositories for dependency injection and health checks:
// Interface + implementation
builder.Services.AddRepositoryStore<IBookRepository, BookRepository>();
// Or just the implementation
builder.Services.AddRepositoryStore<BookRepository>();Both overloads also register the RepositoryStorageReadyHealthCheck.
Event upcasters
Register upcasters individually or as a chain:
// Individual registration (rebuilds the chain on each call)
builder.Services.AddEventUpcaster<BookAddedV1Upcaster>();
builder.Services.AddEventUpcaster<BookAddedV2Upcaster>();
// Or register the full chain at once
builder.Services.AddEventUpcasters(
new BookAddedV1Upcaster(),
new BookAddedV2Upcaster());See Event Versioning for details on writing upcasters.
Command middleware
builder.Services.AddCommandMiddleware<AuditMiddleware>();
builder.Services.AddCommandMiddleware<AuthorizationMiddleware>();Middleware executes in registration order. The first registered middleware is the outermost wrapper.
See Middleware for patterns and examples.
Messaging
Kafka transport
builder.Services.AddNagareKafkaTransport(builder.Configuration);This reads from the "Kafka" section of IConfiguration:
{
"Kafka": {
"BootstrapServers": "localhost:9092",
"Namespace": "my-app"
}
}| Property | Type | Required | Purpose |
|---|---|---|---|
BootstrapServers | string? | One of these | Standard Kafka bootstrap servers |
FullyQualifiedNamespace | string? | is required | Azure Event Hubs namespace (uses Azure Identity for auth) |
Namespace | string | Yes | Prefix for topic names and consumer groups |
Properties | Dictionary<string, string> | No | Additional Kafka client properties (SASL, SSL, etc.) |
For Azure Event Hubs, use FullyQualifiedNamespace instead of BootstrapServers:
{
"Kafka": {
"FullyQualifiedNamespace": "my-eventhub.servicebus.windows.net",
"Namespace": "my-app"
}
}MessageSubscriptionOptions
builder.Services.AddMessageSubscription<
InventoryBookHandler,
BookIntegrationEvent,
BookChannel>(options =>
{
options.BatchSize = 100;
options.IdleDelay = TimeSpan.FromMilliseconds(500);
});| Property | Type | Default | Purpose |
|---|---|---|---|
BatchSize | int | 100 | Acknowledged messages between broker offset commits |
IdleDelay | TimeSpan | 500ms | Delay when the consumer has caught up and no new messages are available |
DeadLetter | DeadLetterPolicy? | null | Null = conveyor-belt default (block hard and retry); set to opt into retry-then-park |
Require<T>() | — | none declared | Declares the union members this subscription reads; undeclared members are skipped as routing, not failure |
Failure handling follows the same conveyor-belt default as every other pipeline — see Messaging → Failure handling.
The consumer group is declared on the handler itself, via the GroupId property on IMessageHandler<T>:
public class InventoryBookHandler : IMessageHandler<BookIntegrationEvent>
{
public string GroupId => "inventory.book-availability";
public Task Handle(BookIntegrationEvent message, MessageContext context, CancellationToken ct)
{
// ...
}
}Use a capability-based prefix (e.g. "inventory.book-availability", "reference-data.carer") rather than a service-based name. If the handler is later moved to a different service during a split, its group ID — and therefore its consumed position — transfers naturally. See Messaging → The GroupId property for the full rationale.
Message producer registration
builder.Services.AddMessageProducer<
BookMessageMapper, // IMessageMapper implementation
BookEvent, // Source event type (from event store)
BookChannel, // Channel definition
BookIntegrationEvent // Target message type (published to transport)
>();See Messaging for the full producer/consumer pattern.
Lock providers
For single-instance deployments, the default NoopLockProvider is fine. For multi-instance deployments, register a distributed lock provider:
// PostgreSQL (registered automatically by AddPostgresCheckpointStore)
services.AddSingleton<ILockProvider>(sp =>
new PostgresLockProvider(connectionString));
// SQL Server (registered automatically by AddSqlServerCheckpointStore)
services.AddSingleton<ILockProvider>(sp =>
new MsSqlLockProvider(connectionString));Lock providers prevent multiple instances of the same service from processing the same subscription events simultaneously.
JSON serialization
Nagare uses System.Text.Json with these defaults:
PropertyNameCaseInsensitive: trueDefaultIgnoreCondition: WhenWritingNullReferenceHandler: IgnoreCyclesJsonStringEnumConverterfor enum serialization
These settings are applied to event serialization and document store serialization. If you need custom converters, register them through NagareJsonSettings before adding your event stores.
Full example
A complete Program.cs for a service using PostgreSQL:
var builder = WebApplication.CreateBuilder(args);
// Database
builder.Services.AddNagarePostgresStorage(builder.Configuration);
builder.Services.AddPostgresCheckpointStore();
// Aggregate (includes event store + snapshot store)
builder.Services.AddPostgresAggregate<
BookAggregate, BookCommand, BookEvent, BookState>();
// Subscriptions
builder.Services.AddPostgresSubscription<BookProjection, BookEvent>();
builder.Services.AddPipeline<BookProjection, BookEvent>(options =>
{
options.BatchSize = 25;
options.PollDelay = TimeSpan.FromMilliseconds(50);
});
// Read models
builder.Services.AddRepositoryStore<IBookRepository, BookRepository>();
// Middleware
builder.Services.AddCommandMiddleware<CorrelationMiddleware>();
// Upcasters
builder.Services.AddEventUpcaster<BookAddedV1Upcaster>();
// Messaging (Kafka)
builder.Services.AddNagareKafkaTransport(builder.Configuration);
builder.Services.AddMessageProducer<
BookMessageMapper, BookEvent, BookChannel, BookIntegrationEvent>();
// Observability
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("Nagare")
.AddOtlpExporter());
var app = builder.Build();
app.MapHealthChecks("/health/ready");
app.MapGet("/books/{id}", async (string id, IBookRepository repo) =>
{
var book = await repo.GetById(id);
return book is not null ? Results.Ok(book) : Results.NotFound();
});
app.Run();