Stratara.Testing
4.2.0
Prefix Reserved
dotnet add package Stratara.Testing --version 4.2.0
NuGet\Install-Package Stratara.Testing -Version 4.2.0
<PackageReference Include="Stratara.Testing" Version="4.2.0" />
<PackageVersion Include="Stratara.Testing" Version="4.2.0" />
<PackageReference Include="Stratara.Testing" />
paket add Stratara.Testing --version 4.2.0
#r "nuget: Stratara.Testing, 4.2.0"
#:package Stratara.Testing@4.2.0
#addin nuget:?package=Stratara.Testing&version=4.2.0
#tool nuget:?package=Stratara.Testing&version=4.2.0
Stratara.Testing
Derived. The behaviour described here is specified under
openspec/specs/. Those specifications are the source; this page explains and illustrates them.
Test doubles and assertion helpers for applications built on the Stratara framework. Unit-test your event-sourced aggregates, encryption, messaging, and session-aware code without spinning up Postgres or RabbitMQ testcontainers.
Contents
AggregateTestHarness<T>/Aggregate.Rehydrate<T>(...)— given/when/then rehydration of an aggregate from events, using the sameApply(...)dispatch as production. Throws on an unmapped event so a forgotten overload fails the test (opt out withIgnoringUnmappedEvents()).InMemoryKeyStore— anIKeyStorethat mints random 256-bit DEKs per scope and supports rotation / revocation / scope-erasure, without a master KEK or key file.TestBlobEncryptor.CreateAesGcm()— the real AES-GCMISecureBlobEncryptorover anInMemoryKeyStore, so blob round-trips exercise production encryption.InMemoryMessageBus— anIMessageBuswith synchronous in-process dispatch and aPublishedlist for assertions.TestSessionContext/TestSessionContextProvider— preset Actor/SubjectSessionContextvalues and anISessionContextProviderdouble.InMemoryTenantMembershipStore— anITenantMembershipStoremirroring the EF store's contract semantics, including the membership-guarded active-tenant selection and the erasure sweeps.InMemorySettingStore— anISettingStorewith exact-scope reads/writes, so the scoped-settings fallback chain can be exercised without a database.InMemoryApiKeyStore— anIApiKeyStoremirroring the EF store's contract semantics: issuance, idempotent import of a caller-supplied key, fail-closed validation, revocation, erasure sweeps, and machine keys materialized into a membership store you can share and inspect.TestTenants.Of("acme")— stable, deterministic tenant/user ids derived from readable slugs.TestEvent.Create(payload, ...)— wrap an event payload inIEvent<T>with realistic metadata.ProjectionTester.HandleAsync(projection, event)— invoke a projection's (private)HandleAsynchandler directly, so you can unit-test it against mocked repositories.
Example
var account = AggregateTestHarness<Account>
.Given(new AccountOpened(id, "Ada", 100m))
.And(new AmountWithdrawn(id, 30m))
.Build();
Assert.Equal(70m, account.Balance);
Dependencies
Stratara.Abstractions,Stratara.Contracts,Stratara.Shared,Stratara.SecurityMicrosoft.Extensions.DependencyInjection
Reference it from your test projects only (<PackageReference Include="Stratara.Testing" />). It is
not meant for production code paths — the InMemoryKeyStore and DummyKeyStore provide no
durability or KEK custody.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.11)
- Stratara.Abstractions (>= 4.2.0)
- Stratara.Contracts (>= 4.2.0)
- Stratara.Security (>= 4.2.0)
- Stratara.Shared (>= 4.2.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Stratara.Testing:
| Package | Downloads |
|---|---|
|
Stratara.Testing.EntityFrameworkCore
Spin up the real Stratara event-sourcing write stack (EventSource, aggregation, snapshots, the EF Core write store) against a shared in-memory SQLite database in one call — production code paths, no Postgres, no Docker. Builds on Stratara.Testing's in-memory doubles. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.2.0 | 25 | 9/18/2026 |
| 4.1.1 | 113 | 9/16/2026 |
| 4.1.0 | 57 | 9/16/2026 |
| 4.0.4 | 187 | 9/14/2026 |
| 4.0.3 | 181 | 9/3/2026 |
| 4.0.2 | 155 | 9/3/2026 |
| 4.0.1 | 112 | 9/2/2026 |
| 4.0.0 | 568 | 8/31/2026 |
| 4.0.0-preview.1 | 70 | 8/31/2026 |
| 3.4.0 | 292 | 8/28/2026 |
| 3.3.0 | 270 | 8/25/2026 |
| 3.2.3 | 125 | 8/22/2026 |
| 3.2.2 | 494 | 8/14/2026 |
| 3.2.1 | 366 | 8/2/2026 |
| 3.2.0 | 127 | 7/18/2026 |
| 3.1.7 | 134 | 7/1/2026 |
| 3.1.6 | 136 | 6/22/2026 |
| 3.1.5 | 135 | 6/22/2026 |
| 3.1.4 | 125 | 6/15/2026 |
| 3.1.3 | 135 | 6/10/2026 |
A hardening release for the Orleans execution model, and a new test-support package for it.
`Stratara.Testing.Orleans` runs the execution model inside the test's process. Beyond that, 4.2.0 closes
every gap known before release. A recorded command runs once and in its scope's order, and it is counted
against the bus's two bounds. A store reader paused by a rebuild comes back if its pauser dies, and the
rebuilt read model keeps every fact. A failing saga stops only itself. Every options section the framework
documents is read from configuration, and an anonymous caller is answered 401.
**Upgrading:**
- **Generate an EF Core migration.** The intent record gains a `conflict_count` column.
- **Check `appsettings.json` for `SessionContext:AllowTenantHeader` and
`Stratara:BlobEncryption:LegacyBlobsCarryPurpose`.** Both were ignored until now and take effect with
this release.
- **Upgrade the saga silos together.**
- **Start a rebuild only once every silo runs 4.2.0.**
### Added
- **`IProjectionCheckpointStore.FindAsync` and `CreateAsync`**: a checkpoint's position, or `null` where none exists, where
`GetAsync` answers `0` for a missing checkpoint and one at the beginning alike; and a first checkpoint written only where none exists, never replacing one. The
framework's store implements both; the defaults throw `NotSupportedException` naming the members, so a store of the
consumer's own keeps compiling and fails only where a store-reading saga needs them.
- **New package `Stratara.Testing.Orleans`** (test-support, the 28th package): `ExecutionModelTestHost` runs the Orleans
execution model in a test's own process — one silo, in-memory reminders and grain directory, the real write stack,
portable commit-order reader, checkpoint and intent stores on in-memory SQLite, and every period shortened to
seconds. A test registers its roles with the production calls, dispatches, waits for the readers
(`WaitForReadersAsync`) and asserts; `Timers`, `SeedAtHeadAsync`, `ResetAsync` and `ExecutionModelTestHostOptions`
(with `BeforeStart`) complete it. The package carries the `STRATARA1001` build check and refuses a stated non-development
environment.
- **Sample `Stratara.Sample.OrleansExecutionModel`**: a command in its aggregate's activation, a projection from the
store and a process timeout on the test host, in one console run; smoke-tested.
- **`AddStrataraTestingEventStore<TWriteDbContext>(connectionString, tenantId, configureContext)`**: a connection per
context to a shared database, and the caller's options — an interceptor — after the provider.
- **`AddCommandServices()`** (`Stratara.EventSourcing.WorkerDefaults`): the command worker stack without the bus-fed
mediator worker, beside `AddEventProjectionServices` and `AddSagaServices`. `AddCommandWorkerServices` is now
`AddCommandServices` plus the worker. A silo composed with it, the execution model's command dispatcher and intent
store, the aggregate grains and a store-reading role runs commands and applies their facts without a message broker,
as does an API host that only dispatches through the execution model's dispatcher.
- **Orleans: `AddStrataraSingletonWork<TWork>(string name, ...)` and `OutboxDrainWork.WorkName`.** A work registered
with its name is published in the silo's metadata without being constructed, so it is first constructed when the
silo is active; a work whose `Name` differs from the registered name fails the start naming both.
- **Orleans: log event `117_119`** (`LogEvents.Orleans.SingletonWorkFailed`) for a run of a singleton work that threw;
the work runs again at its next period, as before. **`117_120`** (`LogEvents.Orleans.RolesUnpublished`) for a silo
that does not start because it hosts roles or work it does not publish.
- **Orleans: a recorded command is signed and verified.** Where the host registered a bus-envelope signer, the record
the execution model writes before a dispatch returns carries the signature a bus command carries, and the drain
verifies it under `BusEnvelopeIntegrityOptions.Mode` before resuming it: under `Strict` an unsigned or invalid record
is kept at once with the reason, under `Permissive` it is resumed and logged. Log events `117_115`–`117_118`.
- **`ICommandIntentStore.ClaimAsync`** claims a batch of due commands; the default claims row by row through
`TryClaimAsync`, the shipped store in two statements.
- **Orleans: log event `117_008`** (`LogEvents.Orleans.HandlerStoppedWithSilo`) for a handler on a grain path cancelled
because its silo stopped.
- **`IProjectionCheckpointStore.AdvanceAsync`** advances a checkpoint from the position its writer last saw. The
default replaces the position through `SetAsync`, so a store of the consumer's own keeps compiling; the shipped
store refuses a write that finds another position or another reader.
- **Orleans: log events `117_005`–`117_007`** — `StoreReaderRetired` for a reader beyond the host's partition
count, `ResumeHeldBackByReplay` and `ResumeReleasedAfterReplay` once each when a full replay holds recorded
commands back and releases them.
- **Orleans: a host seeds its store readers at the head.** `IStoreReaderSeeding.SeedAtHeadAsync`, registered
with `AddStrataraProjectionGrains` and `AddStrataraSagaGrains`, writes a checkpoint at the store's current
head for every registered projection and saga and partition that has none, and reports how many it wrote and
how many existed. A host whose read models are current runs it once, while no silo runs, before its first
start — without it the first start re-applied the whole history. A consumer registered later still starts
at the beginning.
- **`ICommittedPositionReader.HeadAsync`** reports a partition's head — the position after which nothing
committed exists — with a default that walks the partition; both shipped readers answer in one query.
- **Orleans: `CommitTransactionIdBackfill`** stamps the commit record on a populated PostgreSQL event table in
append order and bounded batches, so the native reader's column is added without rewriting the table and
without the whole history landing under one transaction id.
- **Orleans: `AddStrataraExecutionModelReset` takes the schema the runtime tables live in.** `schema`
defaults to `public`; a reminder or membership table absent under it fails the reset naming the table
instead of reporting that nothing was removed, and the three runtime tables are cleared in one transaction.
- **Orleans: two log events.** `117_112` (`LogEvents.Orleans.IntentAttemptFailed`) for every attempt of a
recorded command that fails, with the command's id, type and aggregate; `117_113` (`HandOverFailed`) for a
hand-over that fails. A failing handler is seen before its command is kept.
- **Orleans: log event `117_114`** (`LogEvents.Orleans.PermitReclaimRefused`) for a running heavy unit that was
refused its permit when it registered again after the permit keeper was lost, and runs outside the
cluster-wide bound until a permit is free.
- **`SessionRequiredException`** (`Stratara.Abstractions.Session`): the failure of an operation that must be
attributed to a caller and runs with no session context — a save, a command audit, a dispatch on the bus path and
on the execution model's path. It derives from `InvalidOperationException` and keeps the message these operations
always used, so an existing `catch (InvalidOperationException)` and a log search on the text keep working; a host
catches it without referencing any store or broker package.
- **`ICommandIntentStore`: `RecordAsync(…, recordedAt, …)`, `TryRenewAsync`, `TryRenewFromAsync`,
`RecordConflictAsync` and `ReturnAttemptAsync`**, each with a default that does what a store written against 4.1
did — the record's time is the store's own and its count starts at zero, every hand-over runs, a conflict is
recorded as a failure, a stop gives nothing back.
`OutboxEntry.ConflictCount` and `RecordedIntent.ConflictCount` (defaulted) carry the conflict count.
### Changed
- **Orleans: each store-reading saga reads with a checkpoint of its own.** A saga's reader is keyed and checkpointed
under `sagas:<SagaName>` — the saga's type name — instead of the consumer `sagas` all sagas of a deployment shared,
so `orleans.reader.stalled`, the stall log `117_101` and the attempt log `117_102` carry the failing saga's consumer
name; a dashboard or alert that filters on `sagas` must be widened to the `sagas:` consumers. Seeding, the
execution-model reset and the wake-up after a commit name every saga's consumer, and the reset also removes the
shared `sagas` checkpoint of 4.1.x. A saga without a checkpoint starts where the deployment's sagas read in that
partition — the shared checkpoint of 4.1.x, or else the furthest checkpoint another saga holds — so a saga added
to a running deployment runs no side effect for the store's history. *Upgrade note:* nothing is migrated; on the
first start every saga starts at the shared checkpoint, and the shared reader, brought back by its 4.1.x keep-alive,
retires and logs `117_125` (information). While a 4.1.x saga silo runs beside a 4.2.0 one both apply the facts
above the shared checkpoint, so upgrade the saga silos together. A rollback to 4.1.x resumes the shared reader
from the shared checkpoint, which 4.2.0 never advanced, and applies what the per-saga readers applied since
again. Seeding a store with the shared checkpoint starts each saga there rather than at the head, so the shared
reader's backlog is not skipped. A host that registers two saga types of the same type name — in different
namespaces — no longer starts, naming both, because they would share one checkpoint; rename one before upgrading.
A saga's reader brought back on a silo that registers no saga of its name — a removed or renamed saga — retires,
unregisters its keep-alive and logs `117_126` (information) instead of stalling on every entry. A host with a
checkpoint store of its own must implement `IProjectionCheckpointStore.FindAsync` and `CreateAsync` (see *Added*)
to run store-reading sagas.
- **Security: `"SessionContext": { "AllowTenantHeader": true }` in the host's configuration now turns the tenant-header
fallback on.** `AddSessionContext()` did not read the `SessionContext` section, so such an entry did nothing unless
the host bound the section itself; it is read now (see *Fixed*). Before upgrading, search every environment's
configuration for `AllowTenantHeader` and remove the entry or set it to `false` unless the host means to accept the
`X-Tenant-Id` header. Likewise, a `Stratara:BlobEncryption:LegacyBlobsCarryPurpose` entry now takes effect for a host
that registers the blob encryptor through `AddSecurity()` or `AddStrataraBlobEncryption()` alone.
- **`AddStrataraProblemDetails()` answers a caller that is not authenticated with 401.** An authorization refusal, a
tenant-access denial and a `SessionRequiredException` alike: what such a caller lacks is an identity, not a
permission. Where the host has a default authentication scheme the mapping challenges through it — a bearer client
receives `WWW-Authenticate`, a cookie client is redirected, as `[Authorize]` would do — and writes the problem body
where the challenge leaves a 401; without a scheme it writes a 401 problem response. **A client that treated 403 as
"log in" now receives 401**, which is the status that says so. An authenticated caller's denial stays 403, and a
`SessionRequiredException` for an authenticated caller is not converted — it means the host did not run the
session middleware. A host that does not register the mapping sees nothing new but the exception type.
- **Orleans: `hybrid: true` without a bus dispatcher fails at registration** naming `AddOutboxDispatcher`; it ran
grain-only without a word.
- **The execution model's operating limits are documented**: a forwarded command whose handler outlasts the response
timeout fails its caller and commits (do not retry on a timeout); a resumed command is authorized from its recorded
session, so the authorization provider answers from the session, not the web request; the record of a command is
committed on its own, not with the caller's writes; a full replay applies the store twice to store-reading
projections; singleton work may overlap on two silos for one membership refresh after a death declaration, and the
failover latency follows from the membership settings. `ISingletonWork` and `IAuthorizationProvider` say so in their
XML documentation.
- **Orleans: a silo places grains on itself by the roles and work it publishes, before its own metadata reaches its
cache.** A silo that became active and started its store readers at once could fail its start with *No silo of the
cluster registered the projections role*, because its own metadata was not yet known to it.
- **Orleans: a wake-up nudge that cannot be sent no longer fails the dispatch after a commit.** A commit made before the
silo started, or while it stopped, threw from the bundle dispatcher although the facts were stored; the nudge is now
lost like any other and the readers' poll applies the facts.
- **The portable commit-order reader is verified on SQLite** as well as PostgreSQL, through the test host.
- **Migration guide: the command silo and the bus queues.** The command role's row names `AddCommandServices`, the
dispatcher and the intent store; *When the broker can go* states which hosts need the broker; *After the cut-over:
the bus queues* names the queues the bus workers own and the order in which to retire them.
- **Orleans: every registration of the execution model is idempotent.** A second call of
`AddStrataraProjectionGrains`, `AddStrataraSagaGrains` or `AddStrataraSingletonWork<TWork>` registered its wake-up
target or its work again, so every projection was woken twice per bundle and the seeding and reset listed each
consumer twice.
- **Orleans: a silo that hosts a role or singleton work without publishing it fails at start.** A silo that registered
the grain directory under the model's name itself rather than with `AddStrataraOrleans` started and was placed on as
if it hosted every role; it now fails naming the roles and works it found and `AddStrataraOrleans`. A silo that
hosts nothing placed by role is unaffected.
- **Orleans: the drain resumes a backlog as fast as the handlers take it.** While a pass finds a full batch due, the
next pass follows at once, for at most `OutboxDrainOptions.PollingInterval`; a backlog was resumed one batch per
period.
- **Orleans: a stopping silo tells the handlers on its grain paths.** Command handlers in an aggregate's activation or
a runner, heavy work and timer handlers receive a `CancellationToken` that is cancelled once the silo has been
stopping for `GrainCollectionOptions.DeactivationTimeout`; it was `CancellationToken.None`. A recorded command
stopped this way is resumed elsewhere with no attempt counted, a forwarded command fails back with a message saying
the silo stopped, and a timer stays registered and fires on the next silo.
- **Orleans: a timer registered from its own handler is kept.** A handler that registered its owner and purpose again
with the same due time lost the new timer when its tick unregistered itself.
- **Orleans: `IDurableTimers` refuses an owner id longer than the reminder table holds** (139 characters) on every
member, with a message naming the limit, instead of failing at the first tick.
- **Orleans: the heavy-work bound holds across the loss of the silo keeping the permits.** A permit keeper
that is activated admits no new heavy unit for one `PermitLease` and takes back every running unit that
registers again, so the units the lost keeper had admitted count against the bound before new ones start;
previously a new keeper handed out the whole bound at once beside them. A running unit whose permit was lost
now reads the answer when it registers again and keeps asking until it holds a permit. The first heavy
commands after a keeper's activation — including a cluster's first — start one lease later.
- **The migration guide says how a populated store adopts the execution model.** The transaction-id column
is added nullable, backfilled, then given its default and constraint — while nothing appends; the guide
states the upgrade order (schema before the first 4.1 host, 4.0 hosts keep running, bus outbox worker
before the first drain silo, seed, silos, API host, bus workers, queues), that a checkpoint is keyed by the
projection's simple class name, and the prerequisites of the Orleans packages.
- **Orleans: every role is placed on the silos that registered it.** A silo publishes the roles its
composition registers — commands with `AddStrataraAggregateGrains`, projections with
`AddStrataraProjectionGrains`, sagas with `AddStrataraSagaGrains`, timers with `AddStrataraDurableTimers`
and an `ITimerOwners` — and aggregates, command runners, heavy-work pools, projection, saga, process and
timer-owner grains are placed only on silos publishing their role, as singleton work already was. Roles
may be split across silos; a call for a role no silo registered fails naming the role and the registration.
The heavy-work pool is no longer a stateless worker on the calling silo: it is one pool per eight permits
of `HeavyWorkOptions.ClusterWideLimit`, placed on silos of the command role. An API host that only
dispatches may join as an Orleans client.
- **Orleans: a send that would close a cycle between aggregates is refused at once.** A handler running for
aggregate A that sends to B, whose handler sends back to A, used to wait for the runtime's response
timeout; the send back is now refused with a message naming both aggregates, and both commands fail at
once. Sends between aggregates form a directed acyclic graph.
- **Orleans: a read that fails counts as a stall.** A store that cannot be read, or a checkpoint the reader
refuses, is logged as `117_103` and counted in `orleans.reader.stalled` whichever wake-up or poll started
the read, not only on a nudge.
- **Orleans: the portable commit-order reader states its preconditions.** It is verified on PostgreSQL,
and through the test host on SQLite; every process that appends to a store it reads needs `PartitionCounterInterceptor` — the framework
does not add it, and `CommitOrderOptions.MaintainPartitionCounter` is only the value a write context reads
when it does; and the partition count cannot change once the store holds positions. On PostgreSQL the
native reader remains the one to use.
- **`PartitionCounterBackfill` positions a batch with one statement on PostgreSQL** instead of one per entry;
other providers keep the per-entry update, and the positions it hands out are the same.
- **Orleans: a recorded command is counted the bus's way — schema change.** The record counts the hand-over of the
dispatch as the first of `MessageRetryOptions.MaxDeliveryAttempts` when it is written, so a handler that keeps
failing runs as often as the bound says (before, once more), and a host that dies before that hand-over has used
the attempt, as a crashed delivery does on the bus — under a bound of 1 such a command is kept for an operator
without running. A concurrency conflict — `ConcurrencyException`, what the bus transports count as one — gives its
attempt back and counts against `MessageRetryOptions.MaxConflictRequeues` instead (before, against the delivery
bound of 3). A stop gives its attempt back, for the running handler and for the commands queued behind it. The
count lives in the new column `outbox_entry.conflict_count`: generate and apply an EF Core migration before the
first 4.2 host starts, as for 4.1 (see the migration guide). A command recorded under 4.1 carries no counted first
attempt and runs as 4.1 ran it. An operator returning a kept command also resets `conflict_count` and
`last_failure`.
### Deprecated
- **`CommitOrderOptions.MaintainPartitionCounter`** is obsolete: the framework never read it. A write context that
maintains the partition counter adds `PartitionCounterInterceptor`. The member is removed with the next major.
### Fixed
- **Orleans: a resumed command runs once however its resumptions meet.** Two claimers stamping the same millisecond
— the singleton drain during a failover, a bus outbox worker beside the drain during adoption — both handed the
command over, and a command naming no aggregate, or one whose first run had ended, ran twice; so did a hand-over
arriving after the command completed. The hand-over now carries the claim's stamp and runs only if its receiver
takes the record over from that stamp; one that finds it moved or the record gone is dropped and logged as
`117_124` (`IntentHandOverDropped`, Debug).
- **Orleans: commands one scope dispatches to one aggregate are resumed in dispatch order.** The record's time was
taken after the payload was serialized, so a slower first record resumed after the second — `SetPrice 10` after
`SetPrice 20`. It is now taken when the dispatch starts, strictly increasing per aggregate within the scope, and due
commands are resumed by that time, then by id.
- **Orleans: a silo that stops no longer takes an attempt from the commands it was running or had queued.** The
stopped handler, and every recorded command queued behind it that the stop gives up, gives back its attempt, so
rolling deploys during a long handler no longer keep commands for an operator.
- **Orleans: a hand-over the fence dropped no longer holds its command.** An aggregate's activation refused a later,
valid hand-over of the same command while the dropped one waited in its queue.
- **Orleans: a dispatch's hand-over that arrives after a resumption completed the command is dropped.** A hand-over
arriving later than a sixth of the grace checks that its command is still recorded and not kept.
- **`Stratara.Testing.Orleans`: a recorded command is resumed on the test host.** Its SQLite write store kept points
in time as text, which the intent store's due query cannot compare; they are kept as numbers now.
- **Orleans: a recorded command's time comes from the registered `TimeProvider`.** It was the wall clock while the
drain compared it with the registered clock, so a host or test with its own clock saw commands never become due, or
become due at once.
- **Orleans: a failing saga no longer repeats its siblings or stops them.** All store-reading sagas of a partition
shared one reader and one checkpoint, so an entry one saga failed on was retried for every saga on each poll and
wake-up, without a bound — a saga that had succeeded on it, sending an email or issuing a command, did so again
every few seconds until the failing one was fixed — and every later fact of the partition waited for it. A saga
that fails now stops only its own reading of the partition; every other saga applies the entry once and goes on.
- **An anonymous caller reaching an unguarded save or dispatch is no longer a server error.** The event source, the
command audit, the bus dispatcher and the execution model's dispatcher threw a plain `InvalidOperationException`
that the problem-details mapping let through as a 500 reading like a framework bug; they now throw
`SessionRequiredException`, which the mapping answers 401 for a caller that is not authenticated.
`RequireAuthorization()` on every endpoint is no longer needed to avoid that.
- **Orleans: a singleton work's settings apply to that work alone.** The callback given to
`AddStrataraSingletonWork<TWork>(configure)` and `AddStrataraSingletonWork<TWork>(name, configure)` configured one
settings object shared by every work, so two works registered with keep-alive periods of two and five minutes both
ran with whichever callback ran last. Each work now runs with the host's `SingletonWorkOptions` — what
`services.Configure<SingletonWorkOptions>(...)` sets — with its own callback applied on top, and a work registered
twice runs with the later registration's callback, once, rather than with both. Each work's settings are validated
when the host starts. **A host that relied on one work's callback to configure the others** sets the value for all
of them with `services.Configure<SingletonWorkOptions>(...)` instead; a host that already configures
`SingletonWorkOptions` directly is unaffected.
- **Every options type that names a configuration section is read from it.** `SessionContextOptions` (`SessionContext`),
`ProjectionReplayOptions` (`ProjectionReplay`) and — for a host without `AddStrataraFileKeyStore` —
`StrataraBlobEncryptionOptions` (`Stratara:BlobEncryption`) were registered and bound by nothing, so a value in the
documented section did nothing. `AddSessionContext()`, `AddProjectionReplayState()` and `AddStrataraBlobEncryption()`
now read the section from the host's configuration, directly or through `AddOutboxDispatcher()`, `AddSecurity()` and
the worker composites. A service collection without an `IConfiguration` keeps the defaults; a value configured in
code after the registration takes precedence, and calling a registration again does not re-apply the section over
it. `AddStrataraOrleansCommandDispatcher()` likewise reads `MessageRetryOptions` (`MessageRetry`) and validates it at
start when no bus transport is registered before it — a host whose commands run only through the execution model
set the resume bound in that section to no effect; where a transport registered first already reads the section, it
stays the only reading. A test now holds every public options type with a section name to being read from it.
- **A projection replay lease of zero or less is refused at start.** `ProjectionReplayOptions.LeaseSeconds <= 0` fails
the host with an `OptionsValidationException` naming `ProjectionReplay:LeaseSeconds`; it was accepted, and on the
in-process replay state the marking lapsed at once and publication resumed in the middle of a rebuild.
- **Orleans: `hybrid: true` keeps a bus dispatcher registered by factory or as an instance.** `AddStrataraProjectionGrains`
and `AddStrataraSagaGrains` kept publishing bundles to the bus only when the dispatcher had been registered by type; a
factory- or instance-registered one was removed and bundles stopped reaching the bus.
- **Orleans: `PartitionCounterBackfill` no longer renumbers positioned entries.** Unpositioned entries take the
positions after the partition's counter, so a checkpoint written before a backfill stays true; previously the
backfill shifted every positioned entry and a resumed reader re-applied one entry and lost another.
- **Orleans: the portable reader stops at its partition's unpositioned entry however many other partitions hold.**
The probe was cut at 64 unordered rows of any partition.
- **Orleans: positions within one save follow each stream's version order**, by contract rather than by the change
tracker's enumeration.
- **Orleans: a store reader applies each entry under its own tenant.** A projection, a saga and the services they
depend on are resolved after the entry's session is set — one scope per run of entries recorded under one
session — so a dependency that takes the tenant when constructed no longer sees the first entry's tenant, or
none, for a whole read. A process reads its state under the session of the fact it is handed.
- **Orleans: a checkpoint is no longer rewound by a stale activation**, nor written under another reader's name;
the refused reader reads the checkpoint again. `SetAsync` of the shipped store refuses a row held by another
reader.
- **Orleans: a reader of a partition beyond a lowered partition count retires** instead of returning every
keep-alive period to be refused and counted as stalled.
- **Orleans: an aggregate's order runs to the end however long it takes.** The grain ran its accepted
commands through an ordinary call to itself, which the runtime timed out after `MessagingOptions.ResponseTimeout`
(thirty seconds); the grain read the timeout as "the run never started" and failed every command still
waiting back to its caller, dropping the leases of the recorded ones. The call is one-way now; the
response timeout bounds a caller's wait for one forwarded command only, which the operations guide says.
- **Orleans: a running command is renewed whether or not its handler yields.** The lease and permit
renewals ran on the activation's scheduler, so a heavy handler that computed without awaiting past
`IntentGrace` was never renewed, was claimed by the drain and ran a second time. Both renewals run from
timers of their own.
- **Orleans: a heavy hand-over is leased while it waits for a worker.** A burst that queued units for
longer than the grace handed the queued ones over twice; the pool accepts a hand-over at once and its
lease covers the wait for a slot and a permit. A hand-over the pool already holds is not accepted twice.
- **Orleans: two rebuilds of one projection no longer interleave.** The pause was a flag: the first rebuild's
resume let the readers re-read and advance their checkpoints before the second rebuild truncated, which
left the read model empty for good. Pauses are counted, the readers resume when the last pauser resumes,
and a rebuild requested during a full replay is refused naming the replay.
- **Orleans: the reset is resolved from a scope in the documentation and its example.** It is a scoped
service; resolving it from the root provider throws wherever scope validation is on.
- **Orleans: a batch cut short by the reader's shutdown still records the checkpoint for the entries it applied.**
…the complete notes for this release are in the changelog: https://github.com/yesbert/Stratara/blob/v4.2.0/CHANGELOG.md