Stream
An aggregate whose stored state is the events it appended, the Kit's second write model, which commits through its own Unit of Work on Marten.
What it is
A Stream is an aggregate whose stored state is the ordered events it has appended, loaded by folding them. It is the Kit's second write model, beside EF Core aggregates, stored by Marten in the same PostgreSQL database. Its Unit of Work commits its events with the Operation's Notifications and Integration Events in one transaction. A Stream belongs to the tenant of the Operation that wrote it.
When to use it
When an aggregate is best kept as the events it went through. In the Proving Sample, Ledger is a Stream Module beside the EF Core Modules Catalog, Inventory and Billing: it keeps accounts, posts entries to them, and reads an account's balance by folding its entries. Stream or EF Core aggregate compares the two write models.
How it works
A Stream derives from StreamAggregate. Each behaviour applies its event to the Stream, then raises it, and the Unit of Work appends what was raised. Marten folds the stored events through the same public Apply methods:
// A Stream: every behaviour applies its event, then raises it for the Unit of Work to append, and Marten folds the
// stored events through the same Apply methods, which are public because its source generator dispatches only to public
// ones. Marten stores an event under its type's name without the namespace (account_opened_domain_event), so an event
// type's name stays unique among the Stream Modules of the Host's one store.
internal sealed class Account : StreamAggregate
{
public string Holder { get; private set; } = "";
public decimal Balance { get; private set; }
/// <summary>Whether the other Modules were told of the account, which its activation records.</summary>
public bool IsActive { get; private set; }
public static Account Open(AccountId id, string holder)
{
var account = new Account();
var opened = new AccountOpenedDomainEvent(id, holder);
account.Apply(opened);
account.Raise(opened);
return account;
}
public void Post(decimal amount)
{
var posted = new EntryPostedDomainEvent(new AccountId(Id), amount);
Apply(posted);
Raise(posted);
}
public void Activate()
{
var activated = new AccountActivatedDomainEvent(new AccountId(Id));
Apply(activated);
Raise(activated);
}
public void Apply(AccountOpenedDomainEvent opened)
{
Id = opened.AccountId.Value;
Holder = opened.Holder;
}
public void Apply(EntryPostedDomainEvent posted) => Balance += posted.Amount;
public void Apply(AccountActivatedDomainEvent activated) => IsActive = true;
}A handler loads and starts Streams through IStreams, and saves nothing:
// The entry appends at the version the account was read at: another entry appended meanwhile fails this one with the
// Kit's concurrency conflict, and nothing is appended.
internal sealed class PostEntryCommandHandler(IStreams streams) : CommandHandler<PostEntryCommand>
{
public override async Task<Result> Handle(
PostEntryCommand command,
CancellationToken cancellationToken
)
{
if (
await streams.LoadAsync<Account>(new AccountId(command.AccountId), cancellationToken)
is not { } account
)
{
return AccountErrors.NotFound;
}
account.Post(command.Amount);
return Result.Success();
}
}A Stream Module declares its write model in its Composition Root with AddPouryaKitStreams, with a schema of its own, and the Host registers its one Marten store with UsePouryaKitMarten, after UsePouryaKitWolverine when it also has EF Core Modules. A Stream Module needs no migration. Each event is a Domain Event, so a Notification Handler subscribes to it as to any other. Registering shows Ledger's Composition Root and the Host.
What the Kit guarantees
| Guarantee | Stated in |
|---|---|
| One commit, concurrency, a send that fails after the commit, Receipts, tenancy, one message store | What the Kit guarantees |
| What a query may do with a Stream | Reading and changing Streams |
| What a Stream Module cannot do | Limits |
Failure messages
Next steps
Delayed and recurring commands
A delayed command continues the Operation that scheduled it, later; a recurring command runs on a cadence as a named system Actor, continuing nothing.
Saga
A Module's long-running reaction across Integration Events that keeps its own state between steps, and the one reaction that runs outside an Operation.