Stratara.Shared
4.8.0
Prefix Reserved
dotnet add package Stratara.Shared --version 4.8.0
NuGet\Install-Package Stratara.Shared -Version 4.8.0
<PackageReference Include="Stratara.Shared" Version="4.8.0" />
<PackageVersion Include="Stratara.Shared" Version="4.8.0" />
<PackageReference Include="Stratara.Shared" />
paket add Stratara.Shared --version 4.8.0
#r "nuget: Stratara.Shared, 4.8.0"
#:package Stratara.Shared@4.8.0
#addin nuget:?package=Stratara.Shared&version=4.8.0
#tool nuget:?package=Stratara.Shared&version=4.8.0
Stratara.Shared
Derived. The behaviour described here is specified under
openspec/specs/. Those specifications are the source; this page explains and illustrates them.
License: MIT.
Umbrella of shared utilities for the Stratara framework. Re-exports the Tier-A/B stack (Abstractions, Contracts, Diagnostics, Domain, Resilience, SessionContext) so consumers can pull one package and reach every common type.
Contents
- Source-generated
Logger*Extensionsfor command flows. The outbox, messaging, saga, and projection surfaces have since moved to the packages that own them (Stratara.Outbox.RabbitMQ,Stratara.Sagas,Stratara.Projections) — they keep theStratara.Shared.Diagnostics.Extensionsnamespace, which is why they can look like they still live here. - Domain-event helpers + merge primitives used across the framework.
- Re-export of every Tier-A/B Stratara public type via project-reference fan-out.
Quick reference
// One package reference reaches every Tier-A/B public type
using Stratara.Abstractions.Mediator; // ICommand, IQuery, IMediator
using Stratara.Contracts.Session; // SessionContext
using Stratara.Diagnostics; // ApplicationDiagnostics
// Source-generated logger extensions provided by Shared
logger.LogCommandWorkerStarted();
Dependencies
Transitively depends on every Tier-A/B package: Stratara.Abstractions, Stratara.Contracts, Stratara.Diagnostics, Stratara.Domain, Stratara.Resilience, Stratara.Sessions.
| 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.Abstractions (>= 10.0.13)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- Microsoft.Extensions.Options (>= 10.0.13)
- Microsoft.Extensions.Resilience (>= 10.11.0)
- OpenTelemetry.Api (>= 1.20.1)
- Stratara.Abstractions (>= 4.8.0)
- Stratara.Contracts (>= 4.8.0)
- Stratara.Diagnostics (>= 4.8.0)
- Stratara.Domain (>= 4.8.0)
- Stratara.Resilience (>= 4.8.0)
- Stratara.Sessions (>= 4.8.0)
NuGet packages (12)
Showing the top 5 NuGet packages that depend on Stratara.Shared:
| Package | Downloads |
|---|---|
|
Stratara.Identity.Core
Channel-agnostic identity primitives for the Stratara stack — login/claims/access-token records, sign-in / authentication-state / token-storage abstractions, and a typed HttpClient helper. Shared contract for web, mobile, and desktop hosts. |
|
|
Stratara.Projections
Projection runtime for the Stratara event-sourced stack — projection-handler discovery, change-set creation, update application, and projection manager. Sits between read-model repositories and the event-bundle dispatcher. |
|
|
Stratara.Identity.AspNetCore
Channel-agnostic ASP.NET Core identity wiring for the Stratara stack — AddAspNetIdentity and AddAspNetIdentityWithSignInManager extensions, IStrataraSignInManager wrapper, EF stores, i18n'd failure messages, optional passkey support, external-login OpenID Connect + JWT-bearer helpers, and hardened JIT external-login provisioning. Consumers wire their own AuthenticationStateProvider (Blazor Server, MAUI, etc.). |
|
|
Stratara.EventSourcing.EntityFrameworkCore
EF Core persistence for the Stratara event-sourced stack on PostgreSQL — write-store, read-store, ASP.NET Identity store — including a shared identity store with its migrations — shared conventions, value generators, and UnitOfWork primitives. Targets Npgsql; maps pgvector types for a model that uses them, and needs no database extension otherwise. |
|
|
Stratara.Outbox.RabbitMQ
Outbox-pattern command and event dispatch for the Stratara event-sourced stack — RabbitMQ IMessageBus implementation, retry worker, mediator command worker, and Redis-coordinated projection-replay state. Azure Service Bus support ships as the sibling Stratara.Outbox.AzureServiceBus package. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.8.0 | 0 | 11/6/2026 |
| 4.7.1 | 99 | 11/5/2026 |
| 4.7.0 | 676 | 10/30/2026 |
| 4.6.1 | 241 | 10/29/2026 |
| 4.6.0 | 333 | 10/28/2026 |
| 4.5.0 | 408 | 10/28/2026 |
| 4.4.10 | 339 | 10/26/2026 |
| 4.4.9 | 249 | 10/26/2026 |
| 4.4.8 | 384 | 10/24/2026 |
| 4.4.7 | 248 | 10/23/2026 |
| 4.4.6 | 412 | 10/22/2026 |
| 4.4.5 | 805 | 10/14/2026 |
| 4.4.4 | 521 | 10/10/2026 |
| 4.4.3 | 268 | 10/10/2026 |
| 4.4.2 | 611 | 10/2/2026 |
| 4.4.1 | 557 | 9/29/2026 |
| 4.4.0 | 292 | 9/27/2026 |
| 4.3.1 | 362 | 9/25/2026 |
| 4.3.0 | 537 | 9/23/2026 |
| 4.2.0 | 303 | 9/18/2026 |
The result of a full review of the framework's source: recorded changes to fields marked for encryption are now
encrypted, blobs are bound to their whole scope, a personal access token follows its user's membership, and a replay
keeps deleted tenants and saga positions where they belong. **Read the upgrade notes before moving:** RabbitMQ 4.2 is now
the minimum; protected values written by this release cannot be read by earlier hosts, so readers go first; tokens of
former or pending members get a `401`; and a few start-up checks are stricter. Two products can now share one Redis by
naming themselves.
### Added
- **`ISecureBlobEncryptor.DecryptAsync(stream, scope, purpose)`** decrypts a blob only if it was written for that
purpose; a blob written for another purpose fails authentication. It is a default interface method, so an existing
implementation keeps compiling — and keeps not checking the purpose until it overrides the method. The
framework's encryptor enforces it, and encrypted settings now read through it.
- **`StrataraBlobEncryptionOptions.WriteV2Blobs`** keeps new blobs in the previous format during a rolling upgrade
(see *Blob ciphertext is bound to its whole scope* below). Default `false`.
- **`KeyUnavailableException`** tells a key that no longer resolves from any other failure when decrypting. It
derives from `InvalidOperationException`, as the failure it replaces.
- **`RedisCoordinationOptions.ProductName`** (section `RedisCoordination`) puts a product's name into every Redis key
and channel Stratara uses for projection replays and the outbox lock, so two products can share one Redis. Without
it, two products on one server ran each other's replay requests and saw each other's active flag. Letters, digits,
`.`, `_` and `-` only; anything else is refused at start. **Nothing changes if no name is set. Set it on all of a
product's hosts in one deployment, with no replay running:** a host without the name, or on an earlier release, does
not meet named hosts, keeps publishing during their replay, and holds a different outbox lock. Nothing is migrated;
old keys stay behind.
- **`IProjectionCheckpointStore.ListAsync`** and the `StoredCheckpoint` record list stored positions by consumer
prefix. The default implementation throws `NotSupportedException`, so an existing store keeps compiling; a replay
on such a store runs without the saga-position guard below and logs warning `117_139`.
### Fixed
- **A recorded field-changed event can be replayed.** After a round trip through the store, the value of a
`FieldChangedEvent<TAggregate>` comes back as untyped JSON, and applying it with `ApplyPropertyChanged` threw an
`ArgumentException` — an aggregate that had ever taken a change set could not be rebuilt from its stream.
`PropertyAccessorCache.SetValueByName` now converts such a value to the property's type before assigning it.
No migration: events already in the store replay as they are.
- **A change set leaves out properties the aggregate cannot take.** `ChangeSetBuilder` compared every property the
aggregate could read under the same name. It now takes a property only where the aggregate's property is settable
and of a compatible type — assignable, nullability of a value type aside, or a sequence whose elements are — and
skips an absent value for a property that cannot be absent, so an update no longer records a change that replay
cannot apply. `ChangeMerger` copies a property through whose type cannot hold the aggregate's value instead of
throwing an `InvalidCastException`, and keeps the submitted value where the current one is absent and the payload's
property cannot be.
- **An unedited collection is no longer reported as changed.** `ChangeSetBuilder` and `ChangeMerger` compared
collection properties by reference, so a list that arrived with an update always counted as edited and could
overwrite a concurrent change to it. Lists and arrays are now compared element by element in order, sets
regardless of order, dictionaries key by key; any other type keeps its own equality.
- **A collection of another shape can be applied to an aggregate.** `PropertyAccessorCache.SetValueByName` converts
a collection that is not of the property's type — an array for a list — the way a recorded value is read back, so
applying a field change in memory and replaying it from the store give the same result.
- **The `IEventUpcaster.TargetEventTypeName` documentation no longer describes an in-place shape change.** An
upcaster whose target equals its source has always been refused as a cycle when the event is read; the
documentation said otherwise. A reshaped payload moves to a new type name.
- **The hash-chain worker no longer chains across an entry it has not chained yet.** An entry's timestamp is taken
when it is appended, not when it commits, so an entry with a lower sequence number can carry a later timestamp than
the one after it. The worker's batch skipped such an entry and linked its successor to its predecessor; the skipped
entry was chained on a later pass and verification then reported a break at that point. A batch now ends before the
first unchained entry that is not yet settled, and a batch whose predecessor is not chained is left for the next
pass instead of being chained off the genesis seed. A custom `IEventStreamRepository` gets the second guard for
free; its `GetUnhashedEventsAsync` should return a gap-free run as the updated documentation describes.
- **Events appended after a nested command are attributed to the outer command.** A command dispatched from inside
another command's handler left its own id as the session's causation, so what the outer handler appended afterwards
named the inner command as its cause. With command auditing on, the outer command's causation is restored once
the inner one returns or fails. The outermost command still leaves its id on the session.
- **The snapshot shape guard sees properties declared on a base class.** An auto-property without a public setter
loses its state when an aggregate is restored from a snapshot, and the start-up guard rejects it — but it missed
one declared on an aggregate's base class. **A host whose aggregate inherits such a property now fails at start-up**
with the guard's usual message, where it lost that state silently before.
- **`AddStrataraTestingEventStore` keeps a write unit of work registered before it**, as the PostgreSQL registration
does; it replaced one silently.
- **`MigrateAllDatabasesAsync` disposes the contexts it creates** — it left one undisposed per database.
- **The command-host examples register command auditing.** The XML examples of `AddCommandWorkerServices`,
`AddCommandServices` and `AddHeavyCommandWorkerServices`, the `Stratara.Orleans` README and two guides showed a
host whose first append is refused for a missing causation; they now call `AddCommandAuditing()`.
- **Publishing on Azure Service Bus reuses one sender per topic.** Every publish opened a new sender, and with it a
send link that stayed open until the client was disposed — a host publishing at a steady rate accumulated links
until the connection's quota was reached. The bus now opens one sender per topic and closes them when it is
disposed.
- **One unreadable outbox row no longer stops the outbox.** A stored entry that could not be read back threw out of
the drain pass; entries are drained oldest first, so every pass ended on the same row and nothing behind it was
published again. Such an entry is now logged as error `106_109` (`OutboxEntryUnreadable`), left where it is, and
the pass continues. A failure while draining commands no longer keeps event bundles from being drained in the
same pass.
- **`AddAuthorizingCommandOutboxDispatcher()` holds whichever order it is called in.** `AddOutboxDispatcher()` — and
with it every worker composite — registered a plain dispatcher on top of the authorizing one when it ran second,
so commands marked `[RequireRole]` or `[RequirePermission]` were enqueued without a check, silently. It now leaves
the decorator in place.
- **Calling both `ConfigureOpenTelemetry()` and `ConfigureAspNetOpenTelemetry()` no longer stops the host.** With
`OTEL_EXPORTER_OTLP_ENDPOINT` set, the documented quick start registered the OTLP exporter twice and the host
failed at start-up with `NotSupportedException`. Either method, or both, in any order and any number of times,
now configures telemetry once.
- **The 5000 ms default OTLP export timeout applies.** It was set through the process environment after the host's
configuration had been read, so the exporter kept its own 10 s default. The default is now placed in the host's
configuration where no timeout is configured, and the process environment is left alone.
- **Blob ciphertext is bound to its whole scope.** The framework's blob encryptor authenticated a blob against its
tenant and purpose only, so a blob written under one user's scope decrypted when read under another user's scope,
or under the tenant-level scope, of the same tenant. New blobs are written in a format (leading byte `0x03`) whose
authenticated data covers the sensitivity level, the tenant, the user and the purpose. Blobs already stored read
as before. **Releases before this one cannot read the new format:** where older hosts still read blobs that newer
hosts write, set `Stratara:BlobEncryption:WriteV2Blobs` to `true` until every host is upgraded.
- **An encrypted setting cannot be read as another setting.** The purpose a blob was written for was taken from the
blob itself on read, so the ciphertext of one encrypted setting, copied into another setting's row in the same
scope, decrypted there. The setting store now requires the purpose of the setting it reads.
- **A setting whose key was revoked or erased no longer makes its scope unreadable.** Reading a scope decrypted
every encrypted setting in it and failed as a whole when one key was gone — which is the state an erasure leaves
behind. Such a setting now reads as absent and is logged as warning `112_011` (`EncryptedSettingUnreadable`); the
rest of the scope is returned. Altered ciphertext, a value that is not ciphertext, and a key store failing for
another reason still fail the read; a key identifier altered to one that does not resolve reads as absent, like a
deleted row. A custom `ISecureBlobEncryptor` signals a lost key with the new `KeyUnavailableException`.
- **A key revoked or erased by another process stops resolving at once.** A process that held a key of the file key
store in memory kept decrypting with it after another process sharing the file had revoked it or erased its
scope. A lookup now checks whether the file changed before it answers from memory.
- **A bearer principal carries its subject as the name identifier.** `AddStrataraJwtBearer` left the principal
without `ClaimTypes.NameIdentifier`, the claim the session, the permission policies and the tenant claim read — a
bearer request ran with an empty actor and every permission policy refused it. The validated principal now
carries its `sub` under that claim type; a token-validated handler the host configures still runs.
- **`AddStrataraPermissionPolicies()` works after `AddAuthorization()`.** It registered its policy provider only
where none was registered, and `AddAuthorization()`, `AddAuthorizationCore()` and `AddControllers()` register the
framework's default — every `[Authorize("<permission>")]` endpoint then failed with "policy not found". The
default provider is now replaced; a provider the host supplies is kept.
- **Repeating a failed API-key revocation or tenant sweep removes the machine membership it left behind.** A revoke
that failed after recording the revocation returned early when repeated, and a tenant sweep deleted the keys
before the memberships they identify.
- **The `MembershipClaimsPrincipalFactory` remarks describe what it does**: no tenant claim where a user has several
memberships and none selected.
- **A replay applies each entry in a scope of its own session.** The full replay built every projection and its
scoped dependencies once per batch, before any entry's session was set, and applied the entries of every tenant
through those instances — a dependency that takes its tenant when it is constructed wrote all of them without
one. Entries are now applied in a fresh scope per run of entries recorded under the same session, with the
session set before anything is resolved, as the execution model's readers already do.
- **A replay fails before it empties anything where the forgotten-tenant store is missing.** With a projection that
forgets deleted tenants registered but no store for it, the replay skipped the clearing step, emptied every read
model and then failed on the first fact. It now fails at once with the registration message.
- **The tenant read model ignores a fact older than what it holds.** `TenantProjection` recorded the version of the
last fact it applied but never compared it, so a redelivered or late rename, activation, deactivation, locale
change or reassignment overwrote newer state. A fact that is not newer than the row is now skipped. A late
`TenantCreated` arriving after the tenant's deletion still re-creates the row.
- **A handler that times out stalls its partition visibly on the Orleans execution model.** A cancellation thrown
from inside a projection or saga handler — an HTTP or database timeout — that the reader itself had not requested
matched none of the reader's failure paths: nothing was logged, the partition was not marked stalled, a wait
reported `TimedOut` instead of `Stalled`, and the entries of the batch that had already applied were applied again
on every retry. It is now treated as the failure it is: logged, counted, and checkpointed below the failing
entry.
- **An entry retried after a missing preceding fact starts from a fresh scope on the Orleans execution model**, as
it does on the bus. The retry reused the failed attempt's scoped dependencies, so state that attempt had left
behind — a tracked change, a staged write — was applied a second time.
- **`InMemoryMessageBus` gives the handlers of one subscription competing delivery.** It handed every message to
every handler on the topic, so several workers on one subscription — which is what `ProjectionWorker` and
`SagaWorker` open — each applied every bundle, and a saga issued its command once per worker. Each message now
reaches one handler per subscription, in turn, as on RabbitMQ and Azure Service Bus; different subscriptions
still each receive every message. **A test that relied on every handler of one subscription seeing every message
needs separate subscription names.**
- **`ProjectionTester` runs the overload production runs.** For a projection declaring both `HandleAsync(TData, …)`
and `HandleAsync(IEvent<TData>, …)` it tried the enveloped overload first; the framework tries the payload one
first.
- **JSON attributes are honoured on a type with field-level encryption.** A type with at least one property marked
`[EncryptData]` is stored property by property, and that path ignored the type's `System.Text.Json` attributes
when writing while the read applied them: a property renamed with `[JsonPropertyName]` came back empty, a
`[JsonIgnore]` property was stored — in plaintext — although it was excluded, and a property's `[JsonConverter]`
was skipped. A renamed property now round-trips, an ignored property is no longer stored, and a property's
converter is applied when writing too. Stored names do not change and payloads already stored still read; a
property with its own converter is stored in the converter's form from now on, which is what an upcaster sees in
new events.
- **A heavy command waiting for a permit keeps waiting while the permit keeper cannot be reached.** When the silo
keeping the permits died or the call timed out, the failure was recorded as a failed attempt of a handler that had
never run; repeated failovers could park a command that was only waiting. The wait now continues until the keeper
answers or the silo stops.
- **A host whose sagas need a session to be constructed starts.** The timer start-up check constructed every
registered saga without a session and failed the start where a saga's dependency refuses that. It now decides from
the registrations.
- **`IntentGrace` must be longer than three times `CompletionWindow`.** The start-up validation only required the
grace to be longer than the window. A completed command's record is no longer renewed while its completion is
being collected, and its last renewal can be two thirds of the grace old — with a window longer than the rest, the
drain handed the command over again and its handler ran a second time. **A host configured with a window of a
third of the grace or more now fails at start-up**; the defaults (30 s and 20 ms) are far inside the bound.
- **A personal access token validates only while its user is an active member of the key's tenant.** Validation
checked existence, revocation and expiry, so a token kept authenticating with its tenant claim after the membership
had ended, and an endpoint that needs only authentication served that tenant's data. **Such a token now gets a
`401` while the membership is former, pending or missing**, and works again if the membership becomes active. The
token is not revoked, and machine keys are unaffected. **A custom `IApiKeyStore` must apply the same check**; the
compiler does not flag it. A service modelled as a user without an active membership should move to a machine key.
- **A recorded change to a field marked for encryption is encrypted.** A change set, or a `FieldChangedEvent<T>`
appended directly, wrote the new value of an `[EncryptData]` property in plaintext to the event store and the bus,
where a later erasure could not reach it. It is now stored as ciphertext at the property's level and for the
stream's owner; the property name stays readable, and once the key is destroyed replay sets the property to null or
its type's default and continues. No API change and no migration. **Upgrade every host that reads the stream or
consumes its events before any host that writes, and roll back all hosts together:** an earlier host fails on such
a value for a string, number, identifier, enum or collection property, and silently gets a wrong value for a
record, class, `object` or `JsonElement` property. **Changes already recorded in the clear stay as they are**, keep
replaying, and are not reached by an erasure, because rewriting them would break the tamper-evident chain; the
guide *Encrypt Sensitive Data* shows how to find them and what the options are. Upcasters see ciphertext for such a
value, and rewriting the recorded property name makes it unreadable.
- **A drain pass attempts each stored message once.** Republishing from the outbox ran every entry through the
dispatcher's retries, about 1.4 s for each entry that could not be published, so a backlog such as heavy commands
with no worker bound made a pass outlive its lock and kept event bundles waiting behind the commands. The next pass
is now the retry, and a first dispatch keeps its retries. A brief publish failure of a stored message therefore waits
for the next polling interval, and a policy re-registered under a dispatcher name no longer affects republication.
- **Removing a membership, a tenant selection or a setting and writing it again in one scope no longer throws.** The
stores deleted directly in the database and left the tracked copy behind, so on the scoped registration the second
write failed with EF's identity-conflict `InvalidOperationException`. A consumer's other pending changes are not
touched.
- **A fact of a forgotten tenant no longer adds a row.** A projection declaring `IForgetsDeletedTenants` consulted its
record only after reporting a missing prerequisite, and a fact that adds a row reports nothing, so a fact read after
its tenant's deletion left a row nothing removes — on every full replay, and whenever a partition lags. A declaring
projection is now not called for a fact of a tenant it recorded as deleted, except the deletion facts, and the
pass-over is logged as `104_014`. **A fact that removes something elsewhere is passed over too**, and each other fact
costs one read of the record per declaring projection: about 0.3 to 0.6 ms per fact against a local PostgreSQL, so
several minutes per million facts on a replay. Projections that do not declare it are unchanged.
- **A truncator cannot restart every saga.** `IProjectionViewTruncator` recommended `TRUNCATE ... CASCADE` across the
read-store schema, which also holds the saga positions, and a replay that emptied them sent every saga back to the
beginning. The contract now names what must survive. On a host with `AddStrataraProjectionGrains`, a replay also
writes back any saga position its truncator removed or moved back, and logs warning `117_138` naming the truncator;
error `117_140` names positions it could not write back, and that replay fails. A host with saga grains and bus-fed
projections has the contract only.
### Changed
- **RabbitMQ 4.2 is the minimum version.** On 3.13, 4.0 and 4.1 a broker restart removes a topic exchange and its
bindings, so a message for a subscription not yet bound again is lost, silently, while the publisher sees success.
From 4.2 they survive. **A deployment below 4.2 updates its broker first.** The framework does not check the version.