Use FCQRS from C#
C# applications use the same FCQRS model as F# applications: commands describe requests, events record outcomes, an aggregate decides and folds, and projections build queryable views. The C# API adds base classes, action factories, delegates, and host-builder registration around that model.
This guide uses the current host-builder API. The lower-level ActorApi and ActorWiring APIs remain
available for custom composition, but most applications do not need them.
Compiler requirement
The examples use C# discriminated unions. At the time of writing, the union keyword requires a .NET
11 preview SDK and <LangVersion>preview</LangVersion>. FCQRS targets net10.0 and can run in a
net11.0 host. If preview language features are not acceptable for your project, define the domain in
an F# class library and keep the host, endpoints, and projections in C#.
FCQRS writes an explicit case discriminator for unions in the journal. Do not replace its event serializer with a caseless union representation: two cases can have the same field shape but different domain meaning.
1. Define commands, events, and state
Use commands for intent and persisted events for facts. Deferred events are replies that are published and folded but not stored. Their fold should leave state unchanged because recovery cannot replay them.
|
2. Implement the aggregate
HandleCommand chooses an action. ApplyEvent reconstructs state from stored events. Keep both
functions deterministic and free of database, network, clock, and random-number calls.
|
EntityName is part of persistent identity. Treat it as stable after deployment. See
Evolve persisted events before renaming domain types or cases.
3. Register the runtime
The host starts aggregates first, then sagas, the saga starter, and finally the projection. Register
one projection handler per FCQRS runtime — a second AddProjection call throws
InvalidOperationException; that one handler may update several read-model tables.
|
MyProjection stands in for your projection component — the transactional handler and offset store
from Add a projection. For a durable projection, commit its read-model changes
and offset in the same transaction. On the next start, pass the committed offset instead of zero.
4. Send from an endpoint or application service
Registration adds a typed Handler<DocumentCommand, DocumentEvent> to dependency injection. The
handler waits for the matching aggregate reply. It does not by itself wait for a projection. If no
matching reply arrives within akka.fcqrs.command-timeout (default 30s) — for example because the
aggregate decided UnhandledEvent or the filter never matches — the handler raises
TimeoutException instead of waiting forever.
Handlers are keyed by command/event type pair. Two aggregates sharing the same TCommand/TEvent
pair make the plain registration ambiguous; resolving it then throws with guidance. Resolve the
aggregate you mean through the keyed registration instead, e.g.
services.GetKeyedService<Handler<C, E>>(typeof(MyShard)) or [FromKeyedServices(typeof(MyShard))].
|
After projected.Task completes, query the read model maintained by this subscription. Use a bounded
cancellation policy because projection subscriptions are in-memory request coordination, not a durable
queue. The complete ordering and notification rules are in Read your writes.
5. Test without starting the host
Construct the aggregate and call its two methods directly:
|
Use a FakeTimeProvider when a test creates time-dependent envelopes. Add replay fixtures for old
events before changing their serialized shape. See Test your domain.
Where to continue
- Define an aggregate explains all aggregate actions and snapshot choices.
- Write a saga shows cross-aggregate coordination and C# registration.
- C# interop and serialization explains union representation and compatibility.
FCQRS