DI Extensions Cheatsheet
Derived page. The behaviour described here is specified by the host-composition and
event-sourcing-store capabilities under openspec/specs/. Those specifications are the source;
this page explains and illustrates them. Where the two disagree, the specification is right and this
page is a bug.
The full menu of Add*Services() extensions Stratara exposes, by package.
Umbrella extensions (IHostApplicationBuilder)
These wire entire worker / host concerns in one call. Pick one per host.
| Extension |
Brings |
Use for |
builder.AddBackendServices() |
Mediator, Identity, Session, Security, Resilience |
ASP.NET API hosts |
builder.AddCommandWorkerServices() |
Common framework + command-handling worker (interactive lane) |
Worker hosts that consume the command topic |
builder.AddHeavyCommandWorkerServices(dop?) |
Common framework + dedicated heavy-command worker |
Worker hosts that drain long-running IHeavyCommand commands on a separate lane, so they don't starve interactive commands |
builder.AddEventProjectionWorkerServices() |
Common framework + projection worker |
Worker hosts that update read-models |
builder.AddSagaWorkerServices() |
Common framework + saga worker |
Worker hosts that orchestrate processes |
builder.AddEventProjectionServices() |
The projection worker stack without the bus-fed worker; the replay worker stays |
Hosts whose projections read the store, such as the Orleans execution model |
builder.AddSagaServices() |
The saga worker stack without the bus-fed worker |
Hosts whose sagas read the store, such as the Orleans execution model |
builder.AddEventStreamHashWorkerServices() |
Common framework + event-stream-hash worker |
Worker hosts that hash event streams for tamper-evidence |
builder.AddOutboxWorkerServices() |
Common framework + outbox-drain worker |
Worker hosts that publish from outbox_entry to the bus |
AddCommonFrameworkServices() is called transitively by every worker / backend extension above — you don't call it yourself.
À la carte (IServiceCollection)
What the umbrellas compose. Reach for these when a host needs one concern and not the rest — a tool
that dispatches commands but runs no worker, a test host, a migration runner.
| Extension |
What it does |
services.AddMediator() |
IMediator as a scoped service |
services.AddEventSourcing() |
The core event-sourcing services (event source, aggregation, snapshots) as scoped, plus the default trusted-type resolver |
services.AddMapping() |
The mapper the event-sourcing stack uses to materialize typed events from persisted rows |
services.AddSessionContext() |
The scoped session context and its accessor. Pair with app.UseMiddleware<SessionContextMiddleware>() in an ASP.NET host |
services.AddIdentity() |
The scoped identity accessors that resolve from the ambient session context |
services.AddBackgroundTasks() |
The in-process IBackgroundTaskQueue and its hosted service: capacity 100 pending items (queuing waits when full), each item in its own scope, status kept for the most recent 10 000 items. See Queue Background Work |
services.AddOutboxDispatcher() |
ICommandOutboxDispatcher + IEventBundleOutboxDispatcher (scoped) and the bus they publish through; binds Outbox (for Outbox:DurableBundles) when the host carries a configuration |
services.AddAuthorizingCommandOutboxDispatcher() |
Wraps the dispatcher so [RequireRole] / [RequirePermission] are enforced on the outbox path too, keeping the inner dispatcher resolvable |
services.AddPipelineBehaviorWithResult<T>() |
Registers an open-generic pipeline behaviour for the result-returning request shape |
services.AddTrustedTypeResolver() |
The default ITrustedTypeResolver if none is registered. Idempotent |
services.AddTrustedType<T>() |
Adds one type to the trusted-type allowlist — for types produced but never handled, such as a snapshot type no projection or saga anchors |
services.AddEventUpcaster<T>() |
Registers an IEventUpcaster and ensures the pipeline exists |
services.AddEventUpcasterPipeline() |
The default upcaster pipeline if none is registered. Idempotent; every AddEventUpcaster overload calls it |
Workers without the composite
The hosted services the Add*WorkerServices() umbrellas wire. Register one directly when the host
already has the framework services and needs a second lane.
| Extension |
What it runs |
services.AddMediatorWorker() |
The interactive command worker — subscribes to the command topic, restores the session context, dispatches through IMediator |
services.AddHeavyCommandWorker(dop?) |
The heavy-command lane, draining the heavy-command topic so IHeavyCommand work cannot starve the interactive lane |
services.AddOutboxWorker(configuration) |
The outbox-drain hosted service; binds OutboxOptions from configuration |
services.AddProjectionWorker(configuration) |
The projection runtime and its hosted service; binds ProjectionOptions |
services.AddSagaWorker(configuration) |
The saga runtime and its hosted service; binds SagaOptions |
services.AddProjectionHandling(configuration) |
The projection runtime and the replay worker, without the bus-fed worker; binds ProjectionOptions |
services.AddSagaHandling(configuration) |
The saga runtime without its hosted service; binds SagaOptions |
services.AddEventStreamHashWorker() |
The event-stream hashing worker and the anchor services behind it |
Domain registration (IServiceCollection)
These tell Stratara what to dispatch / project / saga. Call once per assembly that contains the relevant types.
| Extension |
Discovers |
Side-effect |
services.AddCommandHandlersFromAssemblyContaining<T>() |
ICommandHandler<TCmd> + IQueryHandler<TCmd, TResult> (the unified contract) |
Per-handler AddScoped |
services.AddQueryHandlersFromAssemblyContaining<T>() |
IQueryHandler<TQuery, TResult> |
Per-handler AddScoped |
services.AddProjectionsFromAssemblyContaining<T>() |
IProjection impls + their HandleAsync(SomeEvent) overloads |
Per-projection AddScoped<IProjection> — a fresh instance per bundle scope — + event-allowlist registration |
services.AddSagasFromAssemblyContaining<T>() |
ISaga impls + their HandleAsync(SomeEvent) overloads |
Per-saga AddScoped<ISaga> — a fresh instance per bundle scope — + event-allowlist registration |
services.AddAggregatesFromAssemblyContaining<T>() |
IAggregate impls + their Apply(SomeEvent) methods |
Adds each aggregate and each apply-target event type to ITrustedTypeResolver |
services.AddDomainEventTypesFromAssemblyContaining<T>() |
The Apply(SomeEvent) parameter types of the assembly's aggregates |
Adds only those event types to ITrustedTypeResolver — no aggregate types, no handler classes. For event-only hosts (projection/saga workers) that must deserialize bus/stream payloads without wiring handler dependencies |
Security + integrity
| Extension |
What it does |
services.AddStrataraFileKeyStore(configuration) |
Registers the production file-backed EnvelopeFileKeyStore (KEK-wrapped, versioned per-KeyScope DEKs) + FileMasterKeyProvider + the AES-GCM ISecureBlobEncryptor. Lives in Stratara.Security (dependency-light). Call before AddSecurity() so it wins the TryAdd race. |
services.AddSecurity() |
Wires ISecureJsonSerializer ([EncryptData]), the AES-GCM blob encryptor, and a Development-only DummyKeyStore fallback (TryAdd, so a real IKeyStore registered first wins). Adds the KeyStoreStartupProbe fail-fast guard. |
services.AddBusEnvelopeIntegrity(opts) |
Opt-in HMAC signing of CommandEnvelope + EventBundle |
services.AddStrataraBlobEncryption() |
The AES-GCM blob encryptor and its factory on their own, without the rest of AddSecurity() |
services.AddStrataraErasure() |
Composes the membership, API-key, setting and key-material sweeps into one erasure operation. Registers no store of its own — the four it sweeps must already be registered |
Validation
| Extension |
What it does |
services.AddStrataraValidation() |
Registers the validation pipeline behavior. Call before other AddPipelineBehavior* so it runs outermost. |
services.AddValidatorsFromAssemblyContaining<T>() |
Discovers + registers every concrete IValidator<T> in the marker's assembly as scoped. |
Tenant isolation
| Extension |
What it does |
services.AddStrataraTenantIsolation() |
Registers the tenant-isolation pipeline behavior. Acts only on requests implementing ITenantScopedRequest; rejects a request whose TenantId ≠ the session's data-owner tenant with TenantAccessDeniedException (→ HTTP 403). Call after AddStrataraValidation(). |
services.AddStrataraTenantIsolation(o => o.Mode = TenantIsolationMode.Strict) |
Strict mode — additionally routes every cross-tenant operation (actor tenant ≠ data-owner tenant) through ICrossTenantAuthorizer. The shipped default denies all; register your own ICrossTenantAuthorizer to grant the cross-tenant case (e.g. a platform admin). |
Resilience
| Extension |
What it does |
services.AddResiliencePipelines() |
Registers the six Polly named pipelines — ResilienceNames.MessageBus, .CommandDispatcher, .EventBundleDispatcher, .ConcurrencyConflict, .PrecedingFact, .ProjectionReplayBatch |
services.AddStrataraResilienceBehavior() |
Mediator behavior that dispatches IResilientRequest through its chosen pipeline |
Use the ResilienceNames constants rather than the literal pipeline strings.
Outbox transport (pick one per host)
| Extension |
Bus |
builder.AddMessaging() |
RabbitMQ — extends IHostApplicationBuilder, and is what the worker composites call. Binds Messaging, BusEnvelopeJson and MessageRetry (the redelivery bounds, validated at start-up) |
services.AddAzureServiceBus(connectionString) |
Azure Service Bus (connection-string) |
services.AddAzureServiceBusWithManagedIdentity(...) |
Azure Service Bus (DefaultAzureCredential) |
One transport per host — the explicit one wins. AddMessaging() registers IMessageBus for
RabbitMQ; the Azure Service Bus extensions replace it, so an explicit AddAzureServiceBus takes
effect even after a worker composite wired the RabbitMQ umbrella. Order no longer decides the
transport, but registering both in one host is still a smell — pick one.
Write store + database contexts (Stratara.EventSourcing.EntityFrameworkCore)
| Extension |
What it does |
services.AddNpgsqlWriteDbContextFactory<TContext>() |
Npgsql-backed IDbContextFactory<TContext> for the write-store context, plus a scoped IWriteUnitOfWork over it unless the host registered its own, plus the IStoreConflictDetector that makes a PostgreSQL unique violation a ConcurrencyException. The unit of work also needs ISessionContextProvider and ISecureJsonSerializer from AddSessionContext() / AddSecurity(), which every worker composite applies. A host on another provider registers its own IStoreConflictDetector; detectors accumulate |
services.AddNpgsqlReadDbContextFactory<TContext>() |
The same for a read-store context, plus a scoped IProjectionsUnitOfWork / IReadUnitOfWork over it unless the host registered its own |
services.AddNpgsqlIdentityDbContextFactory<TContext>() |
The same for an identity-store context, plus a scoped resolution of the context itself so ASP.NET Identity can inject it directly |
services.AddWriteStore(configuration) |
Binds EventSourcingOptions from the EventSourcing section, which carries no settings today. Snapshot cadence is the registered ISnapshotStrategy — VersionThresholdSnapshotStrategy (every 50 events) unless you register another |
services.AddCommandAuditing() |
CommandAuditBehavior for both command shapes — persists an audit row per dispatched command; queries pass through (Stratara.EventSourcing.Pipeline.CommandAudit) |
The schema comes with the context. A context derived from WriteDbContext<TContext> carries
the store's tables and their constraints, so a migration generated from it makes the database refuse a
second event at the same stream version (unique over bucket_id, stream_id, version on
event_stream_entry, and the same on snapshot) and a second integrity anchor at the same sequence
number (unique over bucket_id, sequence_number on event_chain_anchor). The framework's write,
read and identity contexts share one assembly and each filters ApplyConfigurationsFromAssembly by
namespace; a context of yours that shares an assembly with its siblings needs the same predicate, or
its model drifts from its migrations unnoticed until it meets a real database. See
The store declares its own schema.
Outbox coordination + projection replay (Stratara.Outbox.RabbitMQ)
| Extension |
What it does |
services.AddRedisOutboxLock() |
Replaces the no-op NullOutboxLock with the Redis-backed one, which is what makes more than one outbox-worker replica safe. Needs an IConnectionMultiplexer — AddCaching() from Stratara.Infrastructure registers one. Lease it via OutboxOptions.LockLeaseSeconds |
services.AddProjectionReplayState() |
Registers the projection-replay state — shared over Redis where an IConnectionMultiplexer is registered, held in process otherwise (warning 104_012) — and ProjectionReplayOptions with its defaults, so the replay marking is leased (LeaseSeconds, default 300) rather than outliving a crashed replay. Idempotent |
Orleans execution model (Stratara.Orleans, Stratara.Orleans.EntityFrameworkCore)
Each role is adopted with one call after the composite the host already has; the host's silo is
registered with UseOrleans. See Choose an Execution Model.
| Extension |
What it does |
silo.AddStrataraOrleans((s, name) => …) |
Extends ISiloBuilder. Registers the storage-backed grain directory the model's single-activation grains use, under the name it passes, and publishes the silo's singleton work in its metadata. A silo that runs the model's grains without it fails at start naming this call |
services.AddStrataraAggregateGrains() |
Runs every command that names an aggregate in that aggregate's grain. Register it after every other pipeline behaviour |
services.AddStrataraOrleansCommandDispatcher(opts?) |
Replaces the undecorated ICommandOutboxDispatcher with the durable-intent one: a command is recorded before the call returns and resumed after a crash, a bounded number of times. Composes with AddAuthorizingCommandOutboxDispatcher() in either order. Needs an intent store |
services.AddStrataraIntentStore<TWriteContext>() |
The ICommandIntentStore in the write context's outbox table. The dispatcher's host fails at start without an intent store |
services.ConfigureStrataraHeavyWork(o => …) |
The cluster-wide limit, the permit retry and the permit lease of heavy work |
services.AddStrataraProjectionGrains(opts?, hybrid?) |
Runs every projection in grains that read the store in commit order from a checkpoint. Call after builder.AddEventProjectionServices(); register the host's IProjectionViewTruncator before it |
services.AddStrataraSagaGrains(opts?, hybrid?) |
Runs every saga, and every stateful process, in grains that read the store. Call after builder.AddSagaServices() |
services.AddStrataraProjectionCheckpoints<TReadContext>() |
Keeps the store readers' checkpoints in the read context, keyed by consumer and partition. Deployments sharing a read store need distinct projection names, and at most one of them runs saga grains |
services.AddStrataraPortableCounterReader<TWriteContext>() |
The commit-order reader for any relational provider, and a start check that refuses a store holding an entry without a position. The write context adds PartitionCounterInterceptor itself, and a store with existing entries runs PartitionCounterBackfill.RunAsync once before the first start |
services.AddStrataraDurableTimers(opts?) |
IDurableTimers over the silo's reminder service. The host supplies one ITimerOwners and one ITimerHandler, before or after this call |
services.AddStrataraSingletonWork<TWork>(opts?) |
Runs an ISingletonWork once per cluster at its period, only on silos that registered it |
services.AddStrataraExecutionModelReset<TReadContext>(runtimeConnectionString, clearDirectory) |
IExecutionModelReset: clears the reminders and membership of the host's deployment, the checkpoints of the projections and sagas it registers, and the grain directory through the host's callback. Run it while no silo of the cluster runs |
Every setting these calls bind is validated when the host starts, and an invalid one fails the start
naming itself.
Observability (Stratara.ServiceDefaults)
| Extension |
What it does |
builder.ConfigureOpenTelemetry() |
OpenTelemetry logging, metrics and tracing with the default instrumentation (HTTP client, EF Core, RabbitMQ, runtime); wires the OTLP exporter when OTEL_EXPORTER_OTLP_ENDPOINT is set |
builder.ConfigureAspNetOpenTelemetry() |
Adds ASP.NET Core request instrumentation on top, filtering /health and /alive out of tracing |
builder.ConfigureSerilog() |
Serilog as the host's logging provider with Stratara's defaults (destructuring attributes, async console sink, OTLP sink when configured), reading the Serilog configuration section |
Test support (Stratara.Testing.EntityFrameworkCore)
| Extension |
What it does |
services.AddStrataraTestingEventStore() |
The event-sourcing write stack over an in-memory store, plus a test key store, encryptor and session context, plus the SQLite IStoreConflictDetector so a duplicate stream version is a ConcurrencyException in tests as it is on PostgreSQL. Register your aggregates with AddAggregatesFromAssemblyContaining<T>() so event payloads resolve |
Health checks
| Extension |
What it does |
builder.AddDefaultHealthChecks() |
Baseline self check, tagged live — surfaces on both /health and /alive once MapDefaultEndpoints() is called |
healthChecks.AddEventStoreHealthCheck(...) |
Verifies the write-side database is reachable. Needs the write store registered |
healthChecks.AddOutboxHealthCheck(...) |
Reports depth and age of the outbox backlog, degrading above a pending-entry threshold you pass in |
Identity directory (Stratara.Identity.EntityFrameworkCore)
TContext is any DbContext whose model includes the directory tables — derive from
IdentityDirectoryDbContext<TContext> or call modelBuilder.ApplyIdentityDirectoryModel() in your
own OnModelCreating.
| Extension |
What it does |
services.AddTenantMembershipStore<TContext>() |
EF ITenantMembershipStore (tenant_membership, active_tenant) — shares the request's context |
services.AddTenantMembershipStoreFromContextFactory<TContext>() |
Same store, a fresh context per operation (needs AddDbContextFactory<TContext>()) |
services.AddMembershipAuthorization() |
IAuthorizationProvider over tenant-scoped membership roles |
services.AddMembershipAuthorization<TUser>() |
Above ∪ global ASP.NET Identity roles |
services.AddMembershipCrossTenantAuthorizer(opts?) |
ICrossTenantAuthorizer for strict tenant isolation (membership OR a configured platform role) |
services.AddPermissionCatalog(c => …) |
Declares the permission vocabulary + role grants (throws on an undeclared grant) |
services.AddCatalogPermissionResolver() |
IPermissionResolver — membership roles through the catalog |
services.AddCatalogPermissionResolver<TUser>() |
Above ∪ global ASP.NET Identity roles |
services.AddSettingCatalog(c => …) |
Declares the setting vocabulary (defaults, IsInherited, IsEncrypted) |
services.AddSettingStore<TContext>() |
EF ISettingStore (setting_entry) + the ISettingProvider fallback facade |
services.AddSettingStoreFromContextFactory<TContext>() |
Same pair, a fresh context per operation |
services.AddApiKeyStore<TContext>() |
EF IApiKeyStore (api_key) — issue / import / validate / revoke / sweep |
services.AddApiKeyStoreFromContextFactory<TContext>() |
Same store, a fresh context per operation |
The …FromContextFactory variants exist because the plain registrations share one context across
every directory store in a request: a database context serves one operation at a time, and a store's
commit also commits whatever you have left unsaved on that context. A context per operation removes
both, and in exchange a store write no longer joins a transaction you opened on your own scoped
context. Calling both variants for the same store leaves whichever ran first in place. See
Tenant Membership.
[RequirePermission] is only enforced when the host also registers an authorizing mediator
(services.AddAuthorizingMediator<MembershipAuthorizationProvider>()). Without it — or without an
IPermissionResolver — the mediator's startup validator throws rather than let a guarded request
through unchecked.
ASP.NET specific
| Extension |
What it does |
builder.AddAspNetIdentity<TUser, TIdentityDbContext>() |
Channel-agnostic ASP.NET Core identity wiring (password/schema-v3/passkey defaults — no lockout) |
builder.AddAspNetIdentityWithSignInManager<TUser, TIdentityDbContext>() |
Above + lockout defaults + IStrataraSignInManager wrapper + localization |
builder.AddDevelopmentNoOpEmailSender<TUser>() |
Stub IEmailSender for Development — throws on every other environment name, including Staging (3.4.0; Production-only before) |
services.AddMembershipTenantClaim<TUser>() |
Stamps stratara:tenant_id into every issued principal (claims-factory decorator) |
services.AddMembershipTenantClaimsTransformation() |
Resolves stratara:tenant_id live per request — a tenant switch applies without re-issuing the sign-in |
services.AddStrataraPermissionPolicies() |
Turns every catalog permission into an on-demand policy → [Authorize("sims.read")] |
services.AddStrataraExternalLoginProvisioning<TUser>(opts?) |
JIT create/link of the local account on first external sign-in (fail-closed) |
services.AddStrataraProblemDetails() |
Turns a validation rejection into a 400 with the failures grouped by field, and an authorization or tenant-access refusal into a 403 — one shape for all three |
app.MapDefaultEndpoints() |
/health + /alive endpoints (Stratara.ServiceDefaults.AspNetCore) |
Authentication schemes (AuthenticationBuilder)
| Extension |
Scheme |
.AddStrataraOpenIdConnect(configuration) |
Interactive external login, from Identity:OpenIdConnect |
.AddStrataraJwtBearer(configuration) |
API access tokens, from Identity:JwtBearer (multi-issuer by iss) |
.AddStrataraApiKey(opts?) |
StrataraApiKey — X-Api-Key header (opt-in query parameter) |
.AddStrataraAuthSchemeSelector(opts?) |
Policy scheme routing by request shape: API key → Bearer → cookie |
The builder.Add* identity rows are extension members of IHostApplicationBuilder in the
Microsoft.Extensions.Hosting namespace (Microsoft convention since v3.0.15). The services.Add*
rows and the authentication-scheme extensions above live in Microsoft.Extensions.DependencyInjection.