Egil.Orleans.Messaging.Streams.EventHubs 0.1.80

dotnet add package Egil.Orleans.Messaging.Streams.EventHubs --version 0.1.80
                    
NuGet\Install-Package Egil.Orleans.Messaging.Streams.EventHubs -Version 0.1.80
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Egil.Orleans.Messaging.Streams.EventHubs" Version="0.1.80" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Egil.Orleans.Messaging.Streams.EventHubs" Version="0.1.80" />
                    
Directory.Packages.props
<PackageReference Include="Egil.Orleans.Messaging.Streams.EventHubs" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Egil.Orleans.Messaging.Streams.EventHubs --version 0.1.80
                    
#r "nuget: Egil.Orleans.Messaging.Streams.EventHubs, 0.1.80"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Egil.Orleans.Messaging.Streams.EventHubs@0.1.80
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Egil.Orleans.Messaging.Streams.EventHubs&version=0.1.80
                    
Install as a Cake Addin
#tool nuget:?package=Egil.Orleans.Messaging.Streams.EventHubs&version=0.1.80
                    
Install as a Cake Tool

Egil.Orleans.Messaging

Composable messaging infrastructure for Microsoft Orleans grains.

Egil.Orleans.Messaging provides building blocks for grains that need durable state changes and durable message handoff to move together:

  • IStateManager<T> wraps IPersistentState<T> so a grain does not keep observing uncommitted state after ambiguous write failures.
  • Outbox<T> stores messages alongside grain state and assigns durable sender sequence tokens.
  • OutboxProcessor<T> dispatches pending outbox items through registered postmen, with retry, reminder forwarding, failure reconciliation, and telemetry.
  • MessageTracker records receiver-side high-water marks for outbox messages and Orleans streams.
  • StreamManager gives grains a fluent subscription facade with resume-token and handler-error support.

Install

dotnet add package Egil.Orleans.Messaging

Provider-specific integrations are shipped as companion packages:

dotnet add package Egil.Orleans.Messaging.Streams.EventHubs
dotnet add package Egil.Orleans.Messaging.State.AzureStorage

Use the capability namespaces for the tools you need:

using Egil.Orleans.Messaging.Outboxes;
using Egil.Orleans.Messaging.State;
using Egil.Orleans.Messaging.Streams;
using Egil.Orleans.Messaging.Tracking;

Registration extension members live with the Orleans, hosting, and DI types they extend:

using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.Hosting;

State Manager

Register the default state manager factory on the silo:

siloBuilder.AddDefaultStateManager("state");

For Orleans Azure Table or Blob grain storage, install and configure the Orleans storage provider separately. The Messaging companion works through IPersistentState<T> and Azure SDK exceptions; it does not select or install the underlying provider. Install Egil.Orleans.Messaging.State.AzureStorage and register the Azure-aware factory instead:

siloBuilder.AddAzureStorageStateManager("state");

The Azure-aware manager uses Azure SDK RequestFailedException.Status and ErrorCode values to decide recovery. Optimistic-concurrency and rejected request failures such as HTTP 412, 409, 404, authentication/authorization failures, and payload/validation failures are treated as definite non-persistence, so writes and clears fail fast without an unnecessary recovery read. Ambiguous or transient outcomes, including HTTP 503 ServerBusy, HTTP 500 OperationTimedOut, HTTP 429 throttling, no-response failures, and timeout exceptions, still use read-back recovery.

Then wrap the Orleans persistent state facet during activation:

public sealed class OrderGrain(
    [PersistentState("state", "Default")] IPersistentState<OrderState> storage)
    : Grain, IOrderGrain
{
    private IStateManager<OrderState> state = default!;

    private OrderState CurrentState =>
        state.State ?? throw new InvalidOperationException("Order state is not initialized.");

    public override Task OnActivateAsync(CancellationToken cancellationToken)
    {
        state = this.RegisterStateManager("state", storage);
        return Task.CompletedTask;
    }

    public async Task RenameAsync(string name)
    {
        await state.WriteAsync(CurrentState with { Name = name });
    }
}

State is nullable because persistent storage can expose no value, including after ClearAsync. Initialize missing state or guard it before dereferencing, as CurrentState does above. Later snippets use state.State! only to keep their focus narrow and assume the surrounding grain has already established that invariant.

State types must be reference types and implement IEquatable<T>. For non-trivial state graphs, inherit from VersionedState so the recovery path compares a library-stamped version rather than relying on structural collection equality.

Outbox

Store an Outbox<T> on the grain state and commit messages with the business state change:

[GenerateSerializer]
public sealed record OrderState : VersionedState
{
    [Id(0)] public string? Name { get; init; }

    [Id(1)] public Outbox<IOrderEvent> Outbox { get; init; } =
        Outbox<IOrderEvent>.Create(GrainId.Create("order", "example"));
}

public async Task SubmitAsync()
{
    var next = state.State! with
    {
        Outbox = state.State!.Outbox.Add(new OrderSubmitted())
    };

    await state.WriteAsync(next);
    await outboxProcessor.PostInBackgroundAsync();
}

Add(message) uses system UTC. When a grain uses an injected clock, sample it at the call site and pass the instant with Add(message, timeProvider.GetUtcNow()). The persisted outbox never retains the provider, so serialization and state rehydration need no clock re-registration.

Use OutboxProcessor<T> to dispatch pending items and acknowledge only the items that were posted successfully. Use OutboxMessageEnvelope<T> as the processor item type so the acknowledgement callback can remove exactly the posted items by token:

public sealed class OrderGrain : Grain, IOutboxGrain
{
    private OutboxProcessor<OutboxMessageEnvelope<IOrderEvent>> outboxProcessor = default!;

    public override Task OnActivateAsync(CancellationToken cancellationToken)
    {
        outboxProcessor = this.RegisterOutboxProcessor(new OutboxProcessorOptions<OutboxMessageEnvelope<IOrderEvent>>
        {
            PendingItems = () => [.. state.State!.Outbox],
            AcknowledgePostedAsync = async (items, ct) =>
            {
                await state.WriteAsync(state.State! with
                {
                    Outbox = state.State!.Outbox.RemoveRange(items.Select(item => item.Token))
                });
            },
            ReconcileFailedAsync = (_, _) => ValueTask.CompletedTask,
        })
        .AddPostman<OutboxMessageEnvelope<IOrderEvent>>(
            envelope => PublishSubmittedAsync(envelope.Message));

        return Task.CompletedTask;
    }
}

AcknowledgePostedAsync receives exactly the items that posted successfully — not necessarily a contiguous prefix of the pending list. Different message types dispatch through different postmen concurrently, and an item without a matching postman fails in place while later items can still succeed. Never acknowledge by position (for example outbox.Take(items.Length)); that can remove a failed item and lose it. When the processor item type is a bare message type instead of the envelope, the callback must map each received item back to its token — which is only reliable when payloads are unique — so prefer the envelope form above unless per-message-type postman registration (shown below) is required.

IOutboxGrain forwards reminder ticks to the single attached processor. Register exactly one processor per grain activation; a second registration throws. Add multiple postmen to that processor when item subtypes need different delivery behavior. The grain remains responsible for its own message contracts, posting target, and dead-letter policy. Postman matching is first-match-wins: register specific message types before base interfaces or catch-all handlers.

Failed dispatches are reported through ReconcileFailedAsync. That callback is where the owning grain applies retry, dead-letter, max-depth, or trimming policy, because the grain owns the durable outbox state. The attempt counts passed to the callback are in-memory per activation (and pruned once an item is no longer pending), so policies that must survive activation restarts need to persist their own counters on the items or grain state.

The outbox tools do not require the state manager. When persisting the outbox with plain IPersistentState<T> writes, the pipeline stays at-least-once on its own: items only leave durable state when the grain removes them in AcknowledgePostedAsync after a successful post, so a failed or ambiguous state write leaves them pending and at worst causes duplicate delivery, never loss. Be aware that Outbox<T>.Equals is an O(1) fingerprint (sender, sequence metadata, count, and first/last pending tokens), not deep payload equality — safe for dirty-checks and write recovery, but not a substitute for comparing message contents item by item.

If a post run fails before reconciliation completes — for example when the run exceeds ProcessingTimeout or an acknowledgement callback throws — the processor arms its retry timer and durable reminder before rethrowing, so pending items are retried without requiring another explicit post. Successful posts never pay reminder I/O: PostInBackgroundAsync schedules an in-memory grain timer only, and the durable reminder is registered lazily when a run fails or leaves items pending.

Background outbox postage allows unrelated grain calls to continue while postmen await I/O by default. IPostman<T> services should be state-free with respect to the owning grain. Inline lambda postmen may read activation-local state, but should not write it; durable changes belong in AcknowledgePostedAsync or ReconcileFailedAsync. Postmen run on Orleans' activation scheduler, not on the .NET thread pool. Acknowledgement and failure callbacks are non-interleaving by default, so they do not interleave with normal grain calls unless InterleaveReconciliationCallbacks is enabled. Reentrant grains can still interleave according to Orleans' normal scheduling rules. Pending items in a post run are dispatched concurrently. Successful items are still acknowledged as one ordered batch after all dispatches complete, and failed items are reconciled as one batch.

For reusable delivery code, implement and register keyed postman services:

[OutboxPostman("orders")]
public sealed class OrderEventPostman : IPostman<OrderSubmitted>
{
    public async ValueTask PostAsync(OrderSubmitted message, CancellationToken ct)
    {
        await publisher.PublishAsync(message, ct);
    }
}

services.AddOutboxPostman<OrderEventPostman>();

Then resolve the postman by name from the grain activation service provider:

outboxProcessor = this.RegisterOutboxProcessor(options)
    .AddPostman<OrderSubmitted>("orders");

For common Orleans targets, use the built-in helpers instead of writing the callback by hand:

outboxProcessor = this.RegisterOutboxProcessor(options)
    .AddStreamPostman<OrderSubmitted>(
        "order-streams",
        message => StreamId.Create("submitted-orders", message.OrderId));
outboxProcessor = this.RegisterOutboxProcessor(options)
    .AddGrainPostman<OrderSubmitted, IOrderProjectionGrain>(
        (message, grainFactory) => grainFactory.GetGrain<IOrderProjectionGrain>(message.OrderId),
        (grain, message) => grain.ApplyAsync(message));

Receiver Dedup

MessageTracker accepts a message only when its stream token, stream cursor, or outbox token advances the stored high-water mark:

if (!state.State!.Tracker.ProcessMessage("prices", token, out var tracker))
{
    return;
}

await state.WriteAsync(state.State! with { Tracker = tracker });

Use LatestStreamSequenceToken("prices") when all you need is the previous resume token. Keep using LatestStream("prices") when you need the full cursor or must distinguish "no stream tracked" from "tracked stream with a null token".

The tracker can also evict old sender or stream entries when your retention policy allows it.

Streams

Use StreamManager to configure stream subscriptions from OnActivateAsync. Pass a tracker snapshot when you want persisted resume tokens, or omit it when the grain does not track stream positions:

streamManager = this.RegisterStreamManager(state.State!.Tracker)
    .ConfigureExplicitSubscription<PriceChanged>(
        "StreamProvider",
        "prices",
        async (message, cursor) =>
        {
            if (!state.State!.Tracker.ProcessMessage(cursor, out var tracker))
            {
                return;
            }

            await state.WriteAsync(state.State! with { Tracker = tracker });
        });

await streamManager.EnsureExplicitSubscriptionsAsync(cancellationToken);

The string namespace overload derives a stream id from the complete receiving GrainId, including its grain type and compound-key extension. Publishers must use the same helper with the target grain identity:

var customer = grainFactory.GetGrain<ICustomerGrain>(customerId);
var streamId = StreamManager.CreateStreamId("prices", customer.GetGrainId());
var stream = streamProvider.GetStream<PriceChanged>(streamId);

This convention follows the grain type, so renaming that type changes the derived stream id. Use the StreamId overload for an application-owned id that must survive grain-type changes, or when a custom grain identity cannot round-trip through Orleans' textual GrainId representation:

streamManager = this.RegisterStreamManager(state.State!.Tracker)
    .ConfigureExplicitSubscription<PriceChanged>(
        "StreamProvider",
        StreamId.Create("prices", customerId),
        HandlePriceChangedAsync);

The previous key-only convention is not compatible with these full-identity stream ids. Recreate existing durable subscriptions and update publishers together, or preserve the previous id through the explicit StreamId overload.

Tracked resume tokens are a per-subscription choice. The default is to pass the previous token when a tracker snapshot is supplied. Opt out when a subscription should attach without a resume token:

streamManager = this.RegisterStreamManager(state.State!.Tracker)
    .ConfigureExplicitSubscription<PriceChanged>(
        "StreamProvider",
        "prices",
        HandlePriceChangedAsync,
        useTrackedResumeToken: false);
this.RegisterStreamManager()
    .ConfigureImplicitSubscription<PriceChanged>(
        "prices",
        async (message, cursor) => await UpdateProjectionAsync(message));

Install Egil.Orleans.Messaging.Streams.EventHubs when using Orleans Event Hubs streams and the enriched adapter/token support:

using Egil.Orleans.Messaging.Streams.EventHubs;
using Orleans.Hosting;

Registering the enriched adapter also registers Event Hubs sequence-token JSON converters, so MessageTracker and StreamCursor can persist and restore EnrichedEventHubSequenceToken without downcasting it to the Orleans base event token:

siloBuilder.AddEventHubStreams("event-hubs", configurator =>
{
    configurator.UseEnrichedDataAdapter();
});

The core package can consume provider-specific token metadata through IStreamSequenceTokenMetadata without taking a direct Event Hubs dependency. Custom stream providers that expose custom StreamSequenceToken types should register a JsonConverter<TToken> with StreamSequenceTokenJsonConverters during startup.

Scope

This package is messaging infrastructure, not an event-sourcing or CQRS framework. It wraps Orleans state, outbox dispatch, receiver deduplication, and stream subscription management while leaving domain modeling, read models, transport targets, and operational policy to the application.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.80 56 8/10/2026

BREAKING CHANGES:
- prevent grain-derived stream ID collisions
 Derive namespace-based explicit subscription IDs from the complete Orleans GrainId and expose the same helper to producers. Reject textual identities that do not round-trip instead of silently collapsing them.
 Cover compound keys, hex-parseable strings, cross-kind collisions, Orleans binary serialization, and StreamId text parsing. Document producer wiring and migration.
- split provider-specific messaging packages
 Move Event Hubs stream enrichment into Egil.Orleans.Messaging.Streams.EventHubs so the core package no longer carries the Event Hubs dependency.
 Add Egil.Orleans.Messaging.State.AzureStorage with Azure Table/Blob storage-aware state manager registration and failure classification for optimistic concurrency failures.
 Rename WriteFailureKind to StorageFailureKind so write and clear recovery can share the same provider classification model.
 Update CI to pack all messaging packages and document the companion package model.
- make stream resume tokens configurable
 Make MessageTracker fully optional for StreamManager registration and let each implicit or explicit subscription opt out of passing the tracked resume token to Orleans.
 Remove the StreamCursor constructor that accepted StreamId so cursors are built from the stream namespace directly. Add tests and docs for the resume-token policy and include the outbox postman API plan.
- split messaging tools by capability
 Move messaging tools into capability namespaces and matching test folders, with Outboxes used to avoid a namespace/type name collision.
 Replace StreamManager's AddSubscription/SubscribeAsync surface with explicit implicit/explicit configuration APIs and IImplicitStreamGrain forwarding through an attached grain component.
New Features:
- coordinate outbox postman processing
 Refactor outbox processing into dedicated dispatch, postman registry, and reconciliation collaborators.
 Coordinate manual and timer-backed drains through a single active drain so callers wait for the current pass before processing the next pending snapshot. Preserve per-postman ordering while allowing different postmen to dispatch concurrently.
 Remove the prototype OutboxProcessor2 implementation from the compiled package.
- preserve stream token json types
 Add an explicit JsonConverter registry for concrete StreamSequenceToken types and route StreamCursor and MessageTracker token payloads through that registry.
 Register Event Hubs token converters from the enriched adapter setup so enriched checkpoints round-trip without downcasting provider metadata.
- post outbox items concurrently
 Dispatch pending outbox items concurrently on the Orleans activation scheduler and remove the thread-pool execution mode.
 Background posting now allows delivery to interleave by default while durable acknowledgement and failure reconciliation remain non-interleaving unless explicitly opted in.
- classify azure storage failures
 Classify deterministic Azure Storage failures as non-persistence outcomes and keep ambiguous transient results on the read-back recovery path.
 This lets StateManager skip unnecessary recovery reads for rejected mutations while preserving correctness for throttling, timeouts, and service failures.
- add outbox postman helper APIs
 Add built-in stream and grain postman helpers so common Orleans fan-out targets can use the outbox processor without hand-written dispatch callbacks.
 Cover helper behavior through Orleans tests and fill validation coverage for keyed postman registration edge cases.
- add keyed outbox postman services
 Add IPostman<TMessage>, named postman attributes, IServiceCollection registration helpers, and an OutboxProcessor.AddPostman overload that resolves keyed postman services from the grain activation service provider.
 Cover keyed registration, scoped multi-contract resolution, successful keyed dispatch, and keyed dispatch failure reconciliation.
- add stream token tracker overloads
 Allow MessageTracker callers to process stream sequence tokens directly with explicit stream namespace or provider-qualified namespace, and expose latest stream sequence token lookup for resume-token use cases.
 Clarify stream manager tests by using distinct provider-name and namespace constants for explicit subscriptions.
- make stream tracking optional
 Allow grains to register StreamManager without providing a MessageTracker when they do not persist stream high-water marks.
 The tracked overloads remain source-compatible and still supply resume tokens when a tracker snapshot is provided.
- implement outbox processor stream flow
 Implements the outbox processor registration and draining path, including postman dispatch, success acknowledgement, failure reconciliation, retry timer/reminder scheduling, and stream-backed integration coverage.
 Adds outbox and stream receive telemetry plus focused Orleans in-process tests for stream delivery, error paths, resume behavior, JSON converter branches, and tracker semantics.
- implement stream manager subscriptions
 StreamManager now separates subscription configuration from activation-time subscription setup via RegisterStreamManager, AddSubscription, and SubscribeAsync. This lets grains await Orleans stream subscription establishment without blocking activation and keeps subscription failures visible.
 MessageTracker now records receive-lag histograms for accepted enriched stream cursors and outbox tokens, with shared telemetry plumbing kept in MessagingTelemetry. The Messaging test suite adds an in-process Orleans cluster with memory streams covering ValueTask, Task, default error handling, and resume-token behavior.
- add keyed state manager registration and remove write policy
 Add InitializeStateManager(...) that resolves keyed IStateManagerFactory<T> by storage name, with IServiceCollection and ISiloBuilder helper APIs for keyed singleton registrations.
 Refactor state management to StateManagerBase<T>/DefaultStateManager<T>, remove WritePolicy and force-write behavior, and drop reflection-based ETag mutation.
 Update state manager tests and API design docs to match the new factory wiring and write semantics.
- implement enriched event hub adapter token stamping
 Implement EnrichedEventHubAdapter overrides to stamp traceparent on queued messages, create EnrichedEventHubSequenceToken from cached Event Hub messages, and return enriched stream positions that preserve offset/sequence/event index and optional traceparent.
 Add focused tests for traceparent stamping behavior and enriched token construction from cached message metadata.
- wire enriched event hub data adapter registration
 Implement UseEnrichedDataAdapter extension to validate input and register EnrichedEventHubAdapter through the Event Hub configurator using Orleans Serializer from DI.
 Add focused tests for null-argument validation and configurator callback registration.
- implement StreamCursor enriched token accessors
 Enable StreamCursor JSON converter attribute and implement TryGetEnqueuedTime, TryGetStreamProviderName, and TryGetTraceParent to expose enrichment metadata when backed by EnrichedEventHubSequenceToken.
 Add focused tests for positive and negative cases across enqueued time, stream provider name, and traceparent access.
- implement StateManager extension wrapper
 Implement AsStateManager<T> to validate input and wrap IPersistentState<T> with StateManager<T> for grain-facing usage.
 Add focused tests for successful wrapping and null-argument validation.
- implement StateManager persistence workflow
 Implement StateManager<T> state cache, read/write/clear operations, force-write ETag override, and conflict handling that reconciles equivalent persisted state before rethrowing non-equivalent failures.
 Add focused tests for read refresh, successful writes, write-failure reconciliation behavior, rollback on conflicting writes, rollback when post-failure read fails, and VersionedState version stamping.
- implement OutboxMessageEnvelope STJ converter factory
 Enable JSON converter factory attribute on OutboxMessageEnvelope<T> and implement OutboxMessageEnvelopeJsonConverterFactory with closed generic converters for read/write round-trip serialization.
 Add tests for attribute wiring, CanConvert behavior, round-trip serialization, and missing-token payload validation.
- implement OutboxSequenceToken STJ converter
 Enable JSON converter attribute on OutboxSequenceToken and implement OutboxSequenceTokenJsonConverter read/write paths. The converter now round-trips sequence, sender GrainId shape, timestamp, and epoch without requiring caller-side serializer options.
 Add focused tests for converter attribute registration, round-trip behavior (including multiple GrainId key shapes), and payload validation for missing sender.
- MessageTracker
- Outbox<T> implemented
- stub types with XML docs
Bug Fixes:
- deliver enriched Event Hub tokens
 Wrap Event Hubs batch containers so stream consumers receive broker enqueue time, provider name, and trace context on batch and per-event sequence tokens. Cover the cached-message and Orleans serialization pipeline deterministically.
- preserve clear concurrency conflicts
 Keep InconsistentStateException observable when clear recovery finds a missing record, while adopting the provider's recovered state. Document the throwing-path state contract and cover it with a deterministic regression test.
- expose provider state as nullable
 Annotate IStateManager<T>.State for storage providers that can expose null after reads or clears, and handle null recovery values without dereferencing them. Preserve Orleans activated defaults while documenting and testing both nullable and non-null missing-record shapes.
- decouple Azure storage provider
 Keep only Azure.Core as a runtime dependency of the Azure Storage state-manager companion. Move Table and Blob SDK references to tests and leave Orleans provider selection to consumers, avoiding unwanted provider and version coupling.
- stabilize message tracker JSON schema
 Pin every private tracker model property to its canonical wire name so serializer naming policies cannot change persisted state. Require both root collections so mismatched or malformed payloads fail instead of silently clearing deduplication state.
- tolerate additive stream token JSON
 Read cursor, discriminator-envelope, built-in token, and Event Hub token properties by name while ignoring unknown fields. Required fields, known value types, and duplicate properties remain strict so rolling upgrades gain forward tolerance without accepting ambiguous payloads.
- preserve concurrent state after write failures
 Adopt durable state after a successful recovery read before rethrowing mismatches and concurrency conflicts. This keeps recovered state paired with the refreshed ETag so retries cannot overwrite concurrent writes.
- retry inherited reminder cleanup
 Retain a reminder handle loaded from a previous activation before unregistering it, so a transient unregister failure can be retried by the next empty outbox drain.
- reject duplicate outbox processors
 Fail immediately when a grain activation attempts to register a second processor, preserving the original reminder component and its durable retry path. Document the one-processor invariant and cover different-type registration through the reminder DIM.
- preserve active outbox reconciliation
 Keep reconciliation ownership visible until callbacks complete so an interleaving empty post cannot cancel acknowledged work. Processing timeouts now accept a scoped TimeProvider, and tests use deterministic gates and clocks.
- keep transient clocks out of persisted outboxes
 Accept an explicit UTC append timestamp while preserving the system-clock convenience overload. Persisted outboxes no longer retain a TimeProvider, so serialization and rehydration need no clock re-registration.
- prevent outbox drain handoff deadlock
 Release the drain gate between background dispatch and non-interleaving reconciliation, and let a foreground drain finish any pending batch before taking a new snapshot. This prevents non-reentrant grain calls from blocking the reconciliation turn while preserving direct PostAsync completion semantics.
- preserve outbox behavior after rehydration
 Fall back to TimeProvider.System when Orleans rehydrates an outbox without its non-serialized clock. Document custom-provider re-registration and cover the Orleans deep-copy path so the first mutation no longer fails.
- preserve sender timestamp in tracked outbox tokens
 MessageTracker.LatestOutbox reconstructed the last accepted
 OutboxSequenceToken using the receiver's Received wall-clock time as
 the token Timestamp. Token equality includes Timestamp, so whenever
 sender and receiver clocks differ the returned token did not equal the
 token that was actually accepted, breaking equality-based consumers and
 any logic reading the timestamp as sender time.
 Outbox entries now store the sender-stamped timestamp of the last
 accepted token alongside the receiver-side Received time (which remains
 the basis for eviction). LatestOutbox returns the original timestamp.
 JSON payloads written before the new property fall back to Received,
 matching the previous behavior.
- prune in-memory outbox attempt counters after reconciliation
 The outbox processor tracks per-item dispatch attempt counts in an
 in-memory dictionary keyed by item equality. Entries were only removed
 when an item posted successfully, so items that left the outbox any
 other way - dead-lettered or dropped in ReconcileFailedAsync, or removed
 directly by the grain - kept their entries for the lifetime of the
 activation, a slow memory leak invisible to the owning grain.
 The processor now prunes counters for items no longer pending after
 each reconciliation. A consequence is that an equal item enqueued after
 its predecessor was dead-lettered starts a fresh attempt sequence
 instead of inheriting stale counts.
 Also documents the attempt-count semantics on ReconcileFailedAsync:
 counts are per-activation and keyed by item equality, so retry and
 dead-letter policies that must survive activation restarts need to
 persist their own counters.
- detect diverged outbox tails during write recovery
 Outbox equality now compares the full first and last pending tokens,
 including their timestamps, instead of only their sequence numbers.
 Items are only appended at the tail, so two outbox histories that diverged
 by adding different messages - for example duplicate grain activations
 racing an ambiguous storage write - always differ in their highest pending
 token. Comparing the full token lets the state-manager recovery read-back
 distinguish such diverged states instead of mistaking them for a lost
 response, which could silently drop the local activation's message.
 Histories that diverged only by removals can still compare equal; recovery
 then at worst re-delivers an already posted item (at-least-once) but never
 loses a pending message. Equality remains O(1) and still ignores payloads.
- arm outbox retry when a foreground post run fails
 A failed PostAsync run - processing timeout, cancellation, or an exception
 from an acknowledgement or reconciliation callback - previously skipped the
 retry scheduling that normally happens after reconciliation. On a first-ever
 foreground post no timer or reminder existed yet, so announced messages
 stayed in the durable outbox until the next post or activation.
 The processor now arms its retry timer and reminder before rethrowing the
 post-run failure, but only on the failure path, so successful posts still
 avoid the reminder registration write.
- accept tokenless stream messages
 Do not persist a stream tracking entry when Orleans delivers a null StreamSequenceToken. Without a token there is no high-water mark to compare, so tokenless stream messages are accepted without changing the tracker instead of causing later tokenless messages to look like duplicates.
- harden outbox review behavior
 Remove posted outbox tokens by identity so successful items after a failed gap can still be acknowledged. Also makes outbox token/envelope types Orleans-friendly with required init properties and rewrites the STJ converters to delegate through typed converters while preserving the existing JSON shape.
- tighten stream subscription configuration
 Fail fast when Orleans invokes an implicit stream subscription without a configured handler, and add explicit StreamId subscription overloads so explicit streams are not limited to the grain-keyed convention.
 Clarify outbox ownership, first-match postman dispatch, thread-pool postman safety, and future provider-specific package boundaries in public docs.
- fail fast for missing grain components
 Throw clear configuration errors when implicit stream and outbox marker interfaces receive callbacks before their corresponding components have been attached.
 This avoids silently swallowing stream subscription or reminder callbacks when a grain implements the marker interface but forgets to call the registration helper during activation.
- resume streams from tracked cursors
 Use the activation-time MessageTracker snapshot to provide provider-aware resume tokens when StreamManager attaches implicit handles, resumes explicit handles, or creates missing explicit subscriptions.
 Add focused tests for provider-specific cursor lookup and the Orleans stream APIs that receive those tokens.
- tighten messaging manager invariants
 Fix stream subscription initialization so handles are collected without concurrent List mutation, repeated SubscribeAsync calls are rejected, and stream handlers receive nullable sequence tokens instead of relying on null-forgiving calls.
 Recover StateManager clear failures with a read-back path, clamp negative receive-lag telemetry, validate malformed outbox JSON with JsonException, and document the intentional outbox equality fingerprint invariant.
 Rename the deferred outbox processor activation API references to RegisterOutboxProcessor to align with the other activation-time helpers while leaving the implementation deferred.
- restrict StreamCursor JSON tokens to built-in Orleans types
 Limit StreamCursorJsonConverter to Orleans built-in stream sequence token types and remove custom token adapter serialization paths. Unsupported token kinds now throw a NotSupportedException with an actionable message that explains the stability rationale and points users to the GitHub issues page for feature requests.
 Update StreamCursorJsonConverterTests to assert unsupported custom tokens and unknown token kinds fail with the new guidance message while keeping built-in token round-trip coverage.
Performance:
- register outbox retry reminder lazily
 PostInBackgroundAsync and follow-up drain scheduling no longer register the
 durable reminder up front; they arm only the in-memory grain timer. The
 reminder - which costs a storage write - is now registered only when a post
 run fails or completes with items still pending, including when a background
 dispatch or reconciliation callback throws. Unregistering also caches the
 one-time lookup for a leftover reminder from a previous activation, so an
 empty outbox no longer pays a reminder-table read on every successful drain.
 Successful posts therefore incur no reminder I/O. The trade-off is that a
 silo crash between scheduling a background post and the first failed run
 loses the cross-activation retry trigger; pending items then wait for the
 next activation's post instead of a reminder tick.