AnchorFlow.Testing 0.3.1

dotnet add package AnchorFlow.Testing --version 0.3.1
                    
NuGet\Install-Package AnchorFlow.Testing -Version 0.3.1
                    
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="AnchorFlow.Testing" Version="0.3.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AnchorFlow.Testing" Version="0.3.1" />
                    
Directory.Packages.props
<PackageReference Include="AnchorFlow.Testing" />
                    
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 AnchorFlow.Testing --version 0.3.1
                    
#r "nuget: AnchorFlow.Testing, 0.3.1"
                    
#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 AnchorFlow.Testing@0.3.1
                    
#: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=AnchorFlow.Testing&version=0.3.1
                    
Install as a Cake Addin
#tool nuget:?package=AnchorFlow.Testing&version=0.3.1
                    
Install as a Cake Tool

AnchorFlow v2

The current package version is 0.3.1, including security dependency updates, modernized messaging with role-isolated native durability and global application Write/Read data through the ordinary command, context-registration, event, process and notification contracts.

Broker size budgets apply to the final internal dispatch, including native metadata and JSON escaping. Oversize source work fails permanently before its transaction commits. Oversize target admission rolls back and retains the complete original message in the dead letter store.

AnchorFlow v2 is a small application-facing framework around Wolverine-owned messaging and durability. This workspace contains the completed US-01 through US-27 implementation: application contracts; provider-neutral tenancy; relational schema lifecycle and production providers; Wolverine-owned Local, RabbitMQ and Azure Service Bus durability; command Unit of Work, idempotency and retry; CRUD/DDD ordering; durable subscribers, projections and notification barriers; ProcessManager, Progress, operations and retention; runtime roles, health, fairness and observability; deterministic Testing SDK and local examples; production topology, scale and crash recovery; qualitative performance and architecture audits; live Azure Service Bus validation; public API compatibility; and release-candidate reconciliation. ProcessManager follow-up outcomes for process-owned commands additionally support typed OneOf and AllOf Event/Result groups through the same production runtime.

Setup

Applications start from one AnchorFlow call:

using AnchorFlow;
using AnchorFlow.AspNetCore.Health;
using AnchorFlow.AspNetCore.Tenancy;
using AnchorFlow.Core.Hosting;
using AnchorFlow.EntityFrameworkCore;
using AnchorFlow.Projections;
using AnchorFlow.ProcessManager;
using AnchorFlow.Progress;

services.AddAnchorFlow(options =>
{
    options.UseTenancy(tenantStoreSource, provisioningStepExecutor, TimeSpan.FromMinutes(1));
    options.UseDatabasePerTenant(
        "Host=database;Database=orders_[TenantKey]",
        "Host=database;Database=orders_read_[TenantKey]");
    options.UsePostgreSql(postgreSqlConnectionString);
    options.UseNativeDurability(durability =>
    {
        durability.UsePostgreSql(postgreSqlConnectionString);
    });
    options.ConfigureRuntimeRoles(roles => roles.Role = AnchorFlowRuntimeRole.Combined);
    options.ConfigureRuntimeWork(work =>
    {
        work.MaxConcurrentSubscriberDispatches = 64;
        work.MaxConcurrentProjectionDispatches = 64;
        work.MaxConcurrentProcessManagerDispatches = 64;
        work.MaxConcurrentTenantDispatches = 64;
        work.MaxConcurrentStoreDispatches = 128;
        work.MaxQueuedDispatchAdmissions = 256;
    });
    options.UseWriteDbContexts(write => write.Add<AppWriteDbContext>());
    options.UseProjectionDbContexts(projection => projection.Add<AppProjectionDbContext>());
    options.AddProjection<OrderAccepted, OrderSummaryProjection>();
    options.AddProjection<AnchorFlowProgressChanged, OrderProgressProjection>();
    options.AddProcessManager<OrderApprovalProcess>();
    options.AddProcessStateUpgrade<OrderApprovalState, OrderApprovalStateFrom1>();
    options.ConfigureProcessManagers(processes =>
    {
        processes.WorkerId = "orders-worker-01";
        processes.LeaseDuration = TimeSpan.FromSeconds(30);
        processes.ConcurrencyRetryMaxAttempts = 3;
        processes.ConcurrencyRetryDelay = TimeSpan.Zero;
        processes.DefaultExpectedReplyTimeout = TimeSpan.FromMinutes(30);
        processes.UnexpectedMessagePolicy = ProcessUnexpectedMessagePolicy.Ignore;
        processes.MaxStateBytes = 1024 * 1024;
        processes.MaxDurableCollectionValueBytes = 1024 * 1024;
    });
});

services.AddAnchorFlowAspNetCoreTenancy();
services.AddAnchorFlowRuntimeHealthChecks();

app.UseAnchorFlowAspNetCoreTenancy();

Use options.UseSqlServer(sqlServerConnectionString) plus durability.UseSqlServer(sqlServerConnectionString) for SQL Server production stores, or options.UseSqlite("Data Source=anchorflow.db") for local development and lightweight tests. Each provider can also take separate write and projection connection strings when those stores are physically separated. Tenant SharedDatabase routes reuse the configured provider connection strings. PostgreSQL and SQL Server DatabasePerTenant routes require UseDatabasePerTenant(...): its Write and Projection templates must place every occurrence of the exact configured placeholder (default [TenantKey]) in the provider database/catalog value. AnchorFlow replaces every ordinal occurrence only with the validated technical TenantKey; the key is distinct from the business TenantId, may contain ASCII letters, digits, _, -, and ., and must be unique under provider-canonical database identity. The same authoritative Write route backs application EF work and Wolverine native durability; applications never register tenant message databases separately.

UseNativeDurability(...) configures the underlying Wolverine runtime internally: durable local queues or the selected external broker, provider-native message storage, EF transaction middleware, scheduled-message durability policy and AnchorFlow metadata propagation. Normal application code must not configure Wolverine handlers, envelopes, endpoints, inbox/outbox tables, or broker internals directly. durability.AutoCreateMessageStorageOnStartup() is an explicit startup option for provider-owned native message storage creation or update; leaving it off avoids implicit runtime schema creation.

RabbitMQ and Azure Service Bus are selected inside the same AnchorFlow setup path:

services.AddAnchorFlow(options =>
{
    options.UseNativeDurability(durability =>
    {
        durability.UsePostgreSql(postgreSqlConnectionString);
        durability.UseRabbitMq(rabbit =>
        {
            rabbit.ConnectionString = rabbitMqConnectionString;
            rabbit.ManagementEndpoint = new Uri(rabbitMqManagementUrl); // HTTP(S) base URL ending in '/'
            rabbit.EndpointPrefix = "orders-worker";
            rabbit.ExchangePrefix = "orders-events";
        });
    });
});
services.AddAnchorFlow(options =>
{
    options.UseNativeDurability(durability =>
    {
        durability.UseSqlServer(sqlServerConnectionString);
        durability.UseAzureServiceBus(azure =>
        {
            azure.ConnectionString = serviceBusConnectionString;
            azure.TopicPrefix = "orders";
            azure.SubscriptionPrefix = "orders-worker";
        });
    });
});

Only one of Local, RabbitMQ or Azure Service Bus can be selected. External broker startup readiness probes use redacted failures and never include connection strings, credentials, host details or payload bytes in public exception messages. RabbitMQ requires ManagementEndpoint in both provisioning modes, including the default AutoProvision = true. Set it to the actual HTTP(S) Management API base URL ending in /; the configured RabbitMQ credentials must have topology read access to the selected virtual host. AMQP alone cannot enumerate bindings. AnchorFlow rejects incompatible existing internal exchanges, queues and bindings before native provisioning or intake. AutoProvision may create missing objects and expected bindings during startup; subsequent readiness requires the complete topology and never repairs it. Api, Worker and NotificationDispatcher hosts sharing an internal exchange must use the same EndpointPrefix: that exchange has exactly one Work/notification queue pair.

Existing Azure Service Bus internal subscriptions must each have exactly one rule named anchorflow-work or anchorflow-notification, with the exact corresponding role correlation filter and no action. Different rule names, extra rules and SQL actions fail validation before native provisioning can change the subscription. Missing objects are allowed only during startup with AutoProvision; running readiness requires complete topology. Production namespaces are checked through their Management API. The emulator has an immutable fixture catalog and no Management API, so emulator delivery tests do not prove this administration path.

Live Azure Service Bus release evidence is isolated in tests/AnchorFlow.LiveAzureServiceBus.Tests and is not part of the normal solution run because it requires a real namespace, permissions and credentials. Run it explicitly with filters such as Category=LiveAsbConnectionString, Category=LiveAsbTokenCredential, Category=LiveAsbNamedKey, Category=LiveAsbSas or Category=LiveAsbRuntime. The live profile reads only these environment variable names and must never print their values: ANCHORFLOW_LIVE_ASB_CONNECTION_STRING, ANCHORFLOW_LIVE_ASB_FULLY_QUALIFIED_NAMESPACE, ANCHORFLOW_LIVE_ASB_NAMED_KEY_NAME, ANCHORFLOW_LIVE_ASB_NAMED_KEY, ANCHORFLOW_LIVE_ASB_SAS and optional ANCHORFLOW_LIVE_ASB_TENANT_ID. TokenCredential mode uses non-interactive AzureCliCredential against the configured namespace. For an externally pre-created topology, the least-privilege TokenCredential runtime identity needs namespace-scoped Service Bus data send and receive rights plus read access to the topic/subscription metadata. For the US-26 public AutoProvision proof, the tested identity must also have Service Bus topology-management rights for the run-owned topic/subscription; the closest built-in namespace-scoped Azure role for that proof is Azure Service Bus Data Owner. The current US-26 operator identity is documented as a functional live identity, not as a least-privilege identity, when effective permissions show broader management access.

Runtime roles are configured through options.ConfigureRuntimeRoles(...). Api = 1 owns application/API requests and durable publication. Worker = 4 starts command/subscriber/projection/ProcessManager consumers. NotificationDispatcher = 8 releases and delivers projection notifications. Combined = 13 keeps the single-host setup path; masks 5, 9 and 12 combine the corresponding roles. Bit 2 is invalid. The former Publisher role and its separate readiness name/tags were removed. The retained MessagePublisher = 8 health capability selects the aggregate readiness check and does not add another role check. ApiRequiresTransportForReadiness remains optional and defaults to false. Roles require UseNativeDurability(...); an Api host registers publication recovery and transport startup validation while starting no Worker or notification intake. Role selection is immutable after AddAnchorFlow(...) builds the host service graph.

Messaging uses Wolverine 6.51.0. Each physical write, control or read store has a publication schema (the configured schema, normally wolverine), a Work schema (wolverine_work) and a notification schema (wolverine_notification). Schema names leave room for these suffixes. Api activates publication recovery, Worker publication plus Work recovery, and NotificationDispatcher publication plus notification recovery. Passive store resolution never authorizes intake. Explicit schema management provisions and validates these stores, native stale-recovery defaults/indexes, ordering delivery bindings and tenant-bound dead-letter metadata. Run IAnchorFlowSchemaManager.ManageConfiguredStoresAsync(AnchorFlowSchemaManagementRequest.Migrate) before activating an upgraded deployment. Historical native dead letters are populated in one bounded AdoptionRowBatchSize batch per physical role store and explicit migration invocation, using the native serializer and trusted store catalog. Repeat that operator invocation until validation succeeds; unresolved bindings remain preserved and readiness fails with a redacted error. There is no v1 production-data migration.

RabbitMQ uses separate internal Work/notification routing keys and durable queues. Azure Service Bus uses the existing public integration topic plus one internal topic, ${TopicPrefix}-internal, with ${SubscriptionPrefix}-internal-work and -internal-notification subscriptions. Their only rules match the protected anchorflow-role property. SubscriptionPrefix is at most 28 characters to leave room for the internal suffixes. The internal topic must have duplicate detection disabled; conflicting existing production topology fails startup/readiness. SDK callbacks own bounded manual settlement and cancellation. A successful semantic commit or a committed native retry/dead-letter transfer is required before acknowledgement. Retries retain business identities and attempts, use fresh technical envelope IDs, and stay within the originating store. ProcessManager timeout cancellation retains its original native identity and destination. Native scheduling and recovery remain Wolverine-owned.

Runtime work admission is configured through options.ConfigureRuntimeWork(...). Defaults are finite and host-local: 128 total concurrent AnchorFlow semantic dispatches, 64 concurrent subscriber dispatches, 64 projection dispatches, 64 ProcessManager dispatches, 64 dispatches per tenant/system scope, 128 dispatches per store, 64 dispatches per subscriber/projection/ProcessManager identity and 256 queued admissions. When all applicable slots are busy, AnchorFlow queues work by a structural round-robin fairness key so a hot tenant, store, subscriber, projection or ProcessManager type cannot drain ahead of independent cold work. When the queue is full, AnchorFlow rejects new admission; a rejected subscriber, projection or ProcessManager dispatch is parked through Wolverine scheduling (250 ms, 1 s, 3 s, then every 5 s, the step chosen by the time since the dispatch envelope was created, so a dispatch first rejected late parks at the last step immediately) without consuming a Wolverine failure attempt, so it is never dead-lettered for backpressure alone, a later real failure still receives its remaining finite retry budget, and Wolverine-owned retry and cancellation paths remain the transport mechanics. AnchorFlow emits AnchorFlow.Runtime Activities and Metrics for admitted, completed, released, queued, rejected and semantic boundary outcomes across command, commit, idempotency, retry, ordering, subscriber, projection, projection-barrier, ProcessManager, progress, notification, dead-letter, replay, skip and schema lifecycle boundaries; retention reporting remains US-20 operations scope. Tags use only low-cardinality dimension values such as work kind and whether tenant/store/projection/subscriber/process scope is present; AnchorFlow never tags tenant ids, store ids, message ids, payloads, headers, connection details or raw exceptions.

Testing support is supplied by AnchorFlow.Testing. It contains deterministic command, consumer, aggregate, projection and ProcessManager route harnesses plus instance-scoped tenant, user, scheduler, clock and view-notification fakes. Harnesses can supply exact AnchorFlow envelopes and identity metadata to the production driver. services.AddAnchorFlowTesting() registers the public IAnchorFlowMessagePump; the pump advances registered AnchorFlow step sources one deterministic step at a time or until idle with a finite MaxSteps bound and redacted diagnostics. The pump exercises the production persistence, durable delivery, retry, ordering and notification-barrier semantics, including the release order of every recipient delivery lane of projection notifications: it executes pending notification deliveries by the release order their release pass handed over and fails closed when more than 500 deliveries are pending at once or a delivery carries no release order; test fakes keep state per instance and are not provider or broker proof.

Local examples live in examples/AnchorFlow.Examples.sln. They use one normal services.AddAnchorFlow(...) composition path, AnchorFlow Local transport, PostgreSQL-backed application/runtime storage and no Wolverine API in example application code. The smoke path runs a CRUD command through ICommandBus and an AnchorFlow-managed EF Core write DbContext, tracks a DDD aggregate whose event is collected by the framework, then proves subscriber, projection/view-notification barrier and ProcessManager/Progress outcomes through the productive Local runtime. The examples are for local adopter orientation. The completed production validation covers PostgreSQL and SQL Server with RabbitMQ and the official Azure Service Bus emulator, Shared Database and Database-per-Tenant isolation, 1 -> 3 -> 1 scale, and abrupt crash/recovery boundaries through real Testcontainers and hosted or broker delivery. Fakes and the finite pump are not provider proof.

Operations reads are exposed through IAnchorFlowOperations. GetCachedReport() returns the last immutable bounded background sample without provider fan-out on the request thread, and reports only redacted physical store ids, provider names, bounded signals, stale state and hashed provider errors. ReadTenantDetailsAsync(...) requires an exact tenant, write/projection store kind and supported category, caps page size, and uses opaque DataProtection scope-bound continuation tokens over existing command, ProcessManager, dead-letter, projection and notification state without returning tenant labels, payloads, headers, connection strings or raw exceptions. Read-only operations queries (the cached report sample, process, dead-letter and tenant-detail reads and the preview of an AnchorFlow ProcessManager dead-letter row) run at most three attempts without delay, each attempt on a fresh DbContext, and retry only a failure whose exception chain contains a DbException the provider reports as transient (DbException.IsTransient; for PostgreSQL through Npgsql this includes deadlocks, serialization failures, timeouts and connection failures) or a SQL Server deadlock victim (error 1205); every other failure, and the last failure after the third attempt, surfaces immediately. The preview of a Wolverine-managed native dead letter, read when no AnchorFlow ProcessManager dead-letter row matches, retention cleanup and dead-letter replay are not retried. Detail pages, dead-letter lists and previews fail closed by default: applications must explicitly authorize the exact tenant-scoped read through IAnchorFlowOperationsAuthorizer. Production deployments that page operations details across restarts or multiple workers must configure a stable application discriminator and a persisted key ring shared by those workers; losing or isolating that key ring deliberately makes outstanding operations continuation tokens fail closed. Authorized ProcessManager-dispatch dead letters managed by the configured native transport can be previewed and replayed only through IAnchorFlowOperations, with exact tenant/write-store scope, actor, reason and the current preview concurrency token. AnchorFlow persists a durable replay claim before dispatch; concurrent valid requests have one truthful winner, and a retry with the same operation id reconciles an interrupted audit without another replay. Preview metadata remains bounded and redacted to the recovery identity, low-cardinality source and scope, payload-presence flag, safety decision and fence token; it exposes no transport payload, headers, envelopes or raw exceptions. This authorization, claim and audit behavior resolves the configured provider store for both Shared Database and Database-per-Tenant tenancy. Retention cleanup accepts one exact write-store scope: a tenant through the AnchorFlowRetentionCleanupRequest constructor, global application data in the global write store through AnchorFlowRetentionCleanupRequest.ForGlobal(...), or the system scope in the main store through AnchorFlowRetentionCleanupRequest.ForSystem(...). A request never reaches rows of another scope, so a tenant request never deletes or retires global or system rows and a global or system request never touches tenant rows; IAnchorFlowOperationsAuthorizer receives global and system requests with an empty tenant id and IsGlobal or IsSystemLevel set. Every request takes a bounded batch size and only AnchorFlow-owned semantic cleanup scopes: command idempotency, completed ProcessManager graphs, resolved dead letters and completed ordering admissions. Wolverine-owned technical inbox/outbox cleanup remains delegated to Wolverine durability instead of being exposed as an AnchorFlow tenant/cutoff/MaxCount deleted-row operator contract; subscription and notification histories are visible in operations details but are not accepted cleanup scopes in US-20. The current provider-safe cleanup deletes completed command-idempotency rows, dead-letter rows with an applied audit older than the cutoff, and eligible completed ProcessManager graphs with no open expectations, no open process commands and no unresolved dead letters, and retires completed ordering admissions (admissions completed before the cutoff whose recorded write store and tenant scope match the request exactly lose their lane-assignment rows, and their AnchorFlow_OrderingMessages row is kept as a Retired tombstone that still holds the scope key, work id, write store, tenant scope and timestamps and only clears the claim owner and lease; lane counters are never deleted, global and system-level admissions are retired only by a global or system request, and admissions recorded before command schema version 6 carry no tenant/store attribution and are never retired). Because a work id can be admitted again at any later time, for example after a broker redelivery, a dead-letter replay of the source or a repeated fan-out once the native inbox no longer remembers the source message, the tombstone keeps that re-admission deduplicated: it allocates no lane sequence, a late claim of the retired work id is reported as completed, and the work is never handed off a second time, independent of the chosen cutoff. Because such a re-admission has no time limit, tombstones are never purged: AnchorFlow_OrderingMessages and its indexes keep growing by one row per admission ever made, while this cleanup bounds only the lane-assignment rows; capacity planning for the write store must account for that growth. The cleanup result counts retired admissions; retired tombstones are not reported as backlog. The cached operations report additionally surfaces pending and claimed ordering admissions per write store as subscription signals with counts and oldest age only.

IAnchorFlowOperations is the only public surface for authorized Wolverine-managed ProcessManager-dispatch dead-letter preview and replay; no Wolverine envelope or payload type enters the application contract.

AnchorFlow.AspNetCore supplies tenant binding and health-check adapters without exposing Wolverine endpoints or envelopes. UseAnchorFlowAspNetCoreTenancy() resolves route, header or claim tenant identity before endpoint code runs, rejects missing or conflicting tenant scope with HTTP 400, and binds the same tenant/store context used by command, query and projection paths. AddAnchorFlowRuntimeHealthChecks() registers process-local liveness plus API, worker and notification-dispatcher readiness checks. Liveness does not probe database or broker dependencies; readiness evaluates only dependencies required by the active role, schema readiness, transport readiness and whether the host is still accepting work.

Current Package Boundaries

  • AnchorFlow.Abstractions contains stable application-facing contracts only, including provider-neutral storage, operations and retention result contracts.
  • AnchorFlow.Core depends only on AnchorFlow.Abstractions and contains provider-neutral runtime role, bounded work-admission, redacted telemetry and operations facade contracts.
  • AnchorFlow.ViewNotifications contains app-owned view notification delivery contracts, view attributes, descriptor caching and notification payload contracts.
  • AnchorFlow.Projections contains projection handler contracts, stable manifest construction, projection notification staging and all-projections barrier primitives.
  • AnchorFlow.ProcessManager contains typed ProcessManager definition contracts, stable process type/version/alias metadata, command/reply/timeout/failure contracts, compensation primitives, unexpected-reply policy options, durable list/dictionary/blob state helpers, deterministic state-upgrade registration, provider-neutral route classification, ownership lease options, bounded concurrency retry settings and exact-scope operations contracts.
  • AnchorFlow.Progress contains ProcessManager-bound progress reporters, committed progress-change events, bounded snapshot readers, localization helpers and progress view-notification payloads.
  • AnchorFlow.Testing contains deterministic harnesses, instance-scoped fakes and public bounded message-pump contracts for application tests.
  • AnchorFlow.AspNetCore contains ASP.NET Core tenant middleware and health-check adapters over AnchorFlow-owned contracts.
  • AnchorFlow.EntityFrameworkCore contains generic EF Core registration, scoped DbContext binding, repository/provider contracts, ProcessManager route-state and durable-collection persistence mappings, the internal normal ProcessManager transition executor, controlled ProcessManager dead-letter persistence, operations cache/sampler/detail/action/retention readers, and schema manifest/readiness infrastructure.
  • AnchorFlow.Sqlite provides the local SQLite EF provider and schema lifecycle implementation.
  • AnchorFlow.PostgreSql provides the PostgreSQL EF provider, schema lifecycle implementation, database-time clock, and fencing primitive.
  • AnchorFlow.SqlServer provides the SQL Server EF provider, schema lifecycle implementation, database-time clock, and fencing primitive.
  • AnchorFlow.Wolverine provides the optional internal adapter to Wolverine-owned durable messaging, Local transport, RabbitMQ, Azure Service Bus, provider message storage, durable subscriber dispatch, durable projection dispatch and durable ProcessManager dispatch. It exposes only AnchorFlow-named setup types.
  • AnchorFlow is the beginner composition package and references the current application-facing foundations.

Additional testing and operations packages are added only by their owning stories when the boundary has a demonstrated purpose.

Application Contracts

Commands inherit Command<TResult> or implement ICommand<TResult> and run through a scoped ICommandBus. Command handlers implement ICommandHandler<TCommand, TResult> and receive an ICommandContext; the handler remains the visible Unit of Work while AnchorFlow owns SaveChanges, transaction commit, idempotency replay/conflict/in-progress decisions, retry scopes and durable outgoing message storage. Handlers should return their typed result and should not call EF SaveChanges, start transactions or call brokers directly.

Queries implement IQuery<TResult> and run directly through a scoped IQueryProcessor; a query does not enter an AnchorFlow message, retry, transaction, inbox, or outbox path. Expected business outcomes use AnchorFlowOperationResult and the stable AnchorFlowOperationOutcome vocabulary.

Business events derive from AnchorFlowEvent<TEntity,TId> for one primary resource or AnchorFlowManyEvent<TEntity,TId> for many primary resources. They are emitted through ICommandContext.Emit(...) and carry deterministic affected-resource metadata for later projection, subscriber, and notification ordering. Integration events are separate contracts that implement IAnchorFlowIntegrationEvent and expose a non-empty IntegrationOrderingKey; AnchorFlow converts that key to an opaque af- lane id using provider, store, tenant/system scope, and the key before transport metadata is written.

ProcessManagers can start from either a business event or an integration event, but a start type must be exactly one of those concepts. Integration-event starts are target-side broker ingress behavior: the sender publishes the integration event, and the target worker validates trusted Wolverine metadata before binding the tenant/store and admitting internal ProcessManager dispatch work. GetProcessId(TStartEvent) alone selects the target workflow instance; source process ids, saga ids and process-type headers stay source metadata. Duplicate broker delivery and later start messages for the same target id converge idempotently, and the raw integration ordering key is not stored or exposed in process state or work ids.

[AnchorFlowMessageContract("orders.integration.accepted", 1)]
public sealed record OrderAcceptedIntegration(string OrderId, string SourceLane) : IAnchorFlowIntegrationEvent
{
    public IntegrationOrderingKey OrderingKey => new(SourceLane);
}

public sealed class OrderIntegrationState
{
    public string OrderId { get; set; } = string.Empty;
}

[AnchorFlowProcessType("orders.integration")]
public sealed class OrderIntegrationProcess : ProcessManager<OrderIntegrationState, OrderAcceptedIntegration>
{
    public override ProcessId GetProcessId(OrderAcceptedIntegration startEvent) => new(startEvent.OrderId);

    private Transition When(
        OrderAcceptedIntegration startEvent,
        OrderIntegrationState state,
        ProcessContext context)
    {
        _ = context;
        state.OrderId = startEvent.OrderId;
        return Transition.Done;
    }
}

Event subscribers remain ordinary AnchorFlow application components. Register them with options.AddSubscriber<TEvent,TSubscriber>(), where TSubscriber implements IMessageHandler<TEvent> and may use IConsumerContext. AnchorFlow compiles a bounded immutable subscriber manifest, durably fans out one stable Wolverine-owned work item per source event/subscriber pair before source acknowledgement, and carries subscription-group plus affected-resource ordering metadata so overlapping subscribers serialize while independent groups progress. A subscriber scope rehydrates validated tenant, correlation and causation metadata before user code, enlists only AnchorFlow-managed Write DbContexts that are actually resolved in that scope, collects aggregate and explicit AnchorFlow events, permits at most one ordinary follow-up command with an explicit or v1 fallback idempotency key and an independent committed Unit of Work, and acknowledges the source work only after the durable subscriber transaction commits. Permanent subscriber failure is released only after the matching Wolverine native dead letter is durably observable.

Projection handlers remain ordinary AnchorFlow application components. Register them with options.AddProjection<TEvent,TProjection>(), where TProjection implements IProjection<TEvent>. AnchorFlow compiles a stable projection manifest, admits one Wolverine-owned projection dispatch per source event/projection pair, validates manifest version and projection identity before user code, and executes projection work through the EF projection executor in a fresh scope. Projection read-model writes, checkpoint rows, completion rows and notification candidates commit in one projection transaction; retry uses the existing bounded command retry options/classifiers rather than a second retry runtime. Notification candidates remain blocked until every expected projection identity for the event has a terminal successful completion, then the app-owned IAnchorFlowViewNotificationDelivery interface is invoked with stable notification identity. A release marks the released candidates and stores one durable Wolverine delivery message per released notification; each message invokes IAnchorFlowViewNotificationDelivery for exactly that one notification, loaded by identity from the projection store of its tenant or system/global scope, so its retries and its final dead letter belong to that notification alone and never block other notifications or later releases of the same scope. A dead-lettered notification is not released again. A release stores its delivery messages durably and hands them to the runtime before it commits the marks, so any failure up to that commit leaves the candidates unreleased; the repeated release returns delivery messages that were already stored but never handed off to Wolverine's inbox recovery instead of skipping them, so every released notification has a delivery message that runs while the node lives. DeliveryAttempts is 0 on the first attempt and increases with every Wolverine retry of the delivery message. As in v1, notifications of one recipient delivery lane (projection store, tenant or system/global scope, channel and recipient key, which defaults to the view scope) are delivered serially on the node that released them, in release order (availableAtUtc, then NotificationId); independent lanes are delivered in parallel. A failing notification leaves its lane for its scheduled retries and never blocks the following notifications of that lane. There is no delivery order across lanes, across nodes, for a retried notification or for a delivery message recovered after a failed release. Delivery is at least once; applications deduplicate by NotificationId, which stays the same when a failure or crash before the release commit repeats the release, and order view updates by the view sequence. The completed production topology, scale and abrupt child-process fault matrices cover this projection and notification-barrier behavior.

A tenant-neutral application start creates a global process. Process-owned commands without an explicit tenant, results, replies, compensation, timeouts and scheduled continuations inherit that immutable process scope, including after restart. An explicit command tenant that differs from the process is rejected before transition changes are committed. Global, trusted System and tenant A/B processes with the same ProcessId, ProgressId or collection names remain separate, even when A/B events share a canonical resource lane. This extension does not move a running process across scopes or introduce cross-scope reply routing. Global progress projections use the ordinary all-projections barrier and the existing fixed Global Read startup release recovery; applications still own notification recipients and permission checks.

ProcessManager state may include DurableList<T>, DurableDictionary<TKey,TValue> and DurableBlob<T> properties. AnchorFlow binds those helpers to the immutable global, system or tenant process scope during transitions, persists their entries and chunks in the same EF transaction as process state, expected replies and process-owned commands, and rejects over-limit serialized state or collection values before partial rows are committed. Durable collection continuation tokens use authenticated Data Protection and remain bound to provider, store, global/system/tenant scope, tenant, process owner and collection. Production deployments that page durable collections across restarts or multiple workers must configure a stable application discriminator and a persisted key ring shared by those workers; losing or isolating that key ring deliberately makes outstanding tokens fail closed. AddProcessStateUpgrade<TState,TUpgrade>() registers parameterless deterministic upgrade steps where FromVersion advances exactly one schema version; registries with gaps, duplicates, orphan upgrades or unsupported newer persisted versions fail closed before mutation. Production ProcessManager execution also requires ProcessManagerRuntimeOptions.WorkerId so PostgreSQL and SQL Server can claim provider-database-time ownership leases per process instance; an explicit single-worker local/test opt-out disables that lease requirement. IProcessManagerOperations exposes bounded exact-scope process reads through ProcessManagerScope; tenant reads require a known write-store route, system reads require the system store id, global reads use new ProcessManagerScope("global-write", null, false) and require that exact fixed Write store, and page size is capped at 500. Defaults are 64 KiB state and durable-value chunks, 1 MiB complete process state, 1 MiB durable collection values, a 16 MiB hard maximum for configured state/value limits, a 30-second process lease, three fresh retry attempts, and no retry delay.

Process-owned commands can declare one flat typed outcome group through the normal AnchorFlow fluent API:

context.PublishCommand(command).OneOf(outcomes => outcomes.Event<FooChanged>().Result<FooUnchanged>());

Expect<TReply>() remains the source-compatible shorthand for a OneOf group with one Result<TReply> member. OneOf completes on the first valid event or result; later sibling messages are terminally suppressed only for that process instance. AllOf invokes each declared event or result member at most once, in arrival order, and completes after the last member. Groups are flat and may contain any positive number of typed members, with no fixed arity limit. A terminal ProcessCommandFailed or ProcessCommandTimedOut closes the still-open group through the existing technical feedback path. Empty, duplicate, non-business-event, missing-When, and unknown configured-contract definitions fail before persistence or message processing. These expectations belong only to the process-owned command; ordinary event subscriber and projection fan-out remains unchanged. The completed provider and broker validation exercises these rules through Local, PostgreSQL and SQL Server, RabbitMQ and the official Azure Service Bus emulator, both tenant-isolation modes, and the scale and crash-recovery paths.

Deterministic mixed-outcome batch

An application ProcessManager can stage ten independently identified commands through the same ProcessContext transition. Each command declares one typed OneOf(Event, Result) group; the productive mixed-batch validation covers eight event winners and two result winners. Typed state counts terminal outcomes and publishes exactly one follow-up when the tenth outcome arrives:

private Transition When(BatchStarted started, BatchState state, ProcessContext context)
{
    state.BatchId = started.BatchId;
    for (var index = 0; index < 10; index++)
    {
        var stableKey = $"{started.BatchId}:{started.Items[index].StableKey}";
        context.PublishCommand(new BatchItemCommand(stableKey))
            .OneOf(outcomes => outcomes
                .Event<BatchItemAccepted>()
                .Result<BatchItemUnchanged>());
    }

    state.CommandsPublished = 10;
    return context.Transition;
}

private Transition When(BatchItemAccepted _, BatchState state, ProcessContext context) =>
    CompleteOutcome(state, context);

private Transition When(BatchItemUnchanged _, BatchState state, ProcessContext context) =>
    CompleteOutcome(state, context);

private static Transition CompleteOutcome(BatchState state, ProcessContext context)
{
    state.CompletedOutcomes++;
    return state.CompletedOutcomes == 10
        ? context.PublishCommand(new BatchFollowUp(state.BatchId)).FireAndForget()
        : context.Transition;
}

BatchState is application-owned typed state, so arrival order may vary without changing the tenth-outcome rule. AnchorFlow.Testing can drive the same exact envelopes through the finite public pump; it does not introduce an alternate message runtime.

ProcessManager handlers can report language-neutral progress through ProcessContext.Progress. Progress operations, sections, counters and issues are persisted in the same fenced EF transition transaction as process state and commands. IAnchorFlowProgressReader materializes bounded snapshots from committed operations: GetGlobalAsync(progressId, cancellationToken) explicitly reads the fixed global Write store, while the existing GetAsync(progressId, tenantId, isSystemLevel, cancellationToken) retains its tenant/system behavior. The additive global reader and IAnchorFlowProgressStore.GetGlobalOperationsAsync have default interface implementations that fail explicitly with NotSupportedException for older custom providers; existing implementations remain source compatible without an added abstract member. The built-in EF store implements global reads, and localization remains a read-side concern with fallback text preserved. Each committed progress id emits an AnchorFlowProgressChanged framework event with the latest snapshot version; applications register normal projections for that event and call context.AddProgressNotification(snapshot). Progress notifications are ordinary projection notification candidates and remain behind the US-12 all-projections barrier before IAnchorFlowViewNotificationDelivery is invoked.

Durable-message metadata uses AnchorFlowMessageContractId and a positive AnchorFlowMessageContractVersion; application IDs may not claim the reserved anchorflow. namespace. AnchorFlowJsonSerializerOptions.Create() supplies the explicit System.Text.Json policy: declared polymorphism is supported, while undeclared interface, abstract, object, collection, and dictionary payload positions are rejected. MessageBoundaryValidator checks payload bytes, headers, and affected resources before any persistence or transport boundary; its typed failure result never contains caller payload or header values. Defaults are a 1 MiB payload, 64 headers, 128-byte header names, 4 KiB header values, 32 KiB encoded headers, and 256 affected resources; all are fail-closed against their framework hard ceilings.

CRUD And Aggregates

CRUD command handlers mutate AnchorFlow-managed write DbContexts, emit business facts through ICommandContext, and let the command Unit of Work commit domain changes, idempotency state, and outgoing messages atomically. ICommandBus.PublishBulkAsync(...) splits a bounded item set into deterministic command chunks; each chunk runs through the normal command Unit of Work and returns an AnchorFlowBulkChunkResult<TResult> after its transaction commits. Bulk CRUD chunks are one technical commit unit: after rollback, AnchorFlow cannot infer per-item success from normal tracked EF changes. AnchorFlowBulkChunkFailurePolicy and AnchorFlowBulkConflictCapability describe technical vocabulary only; application code still owns business meaning for skipped, conflicted, rejected, merged, or updated items.

DDD aggregate roots derive from AggregateRoot<TAggregateRoot,TId> and declare a stable [AnchorFlowAggregateType("...")]. Aggregate methods call the protected Emit(...) API with events derived from AggregateEvent<TAggregateRoot,TId>. Emit(...) validates the target id, requires the aggregate to implement the matching IEmit<TAggregateRoot,TId,TEvent>, invokes a public Apply(TEvent) method, stamps aggregate type/id/sequence metadata, and records the event as uncommitted. If Apply(...) throws, the stamp and sequence are rolled back.

AnchorFlow collects uncommitted aggregate events from aggregate roots tracked by AnchorFlow-managed write DbContexts. Collection preserves tracking and emission order for each aggregate root, stages the events before outgoing message persistence, and clears them only after the command transaction commits. Aggregate support is not event sourcing: current aggregate state remains in application EF tables, and outbox events are not a historical replay stream.

Tenancy And Stores

Tenant-scoped work uses the single TenantStoreRegistrySnapshot catalog payload. It distinguishes the tenant-neutral Main store from tenant Write and Projection stores and supports both SharedDatabase and DatabasePerTenant routing. UseDatabasePerTenant(...) pins the catalog to DatabasePerTenant; without templates, composition pins it to SharedDatabase. A fixed catalog keeps its declared mode and a dynamic catalog pins its first valid mode, so an initial or refreshed mode mismatch is rejected before publication. Shared routes never carry a TenantKey. Store records carry stable non-secret identifiers only; connection strings, credentials, and raw provider handles never enter application-facing contracts.

Use a fixed snapshot when the catalog is static, or an application-owned TenantStoreRegistrySource when tenants can be added at runtime:

services.AddAnchorFlow(options =>
{
    options.UseTenancy(tenantStoreSource, provisioningStepExecutor, TimeSpan.FromMinutes(1));
});

Without a cache duration, each top-level resolution loads the source freshly. A positive cache duration single-flights a successful load until expiry. An async lookup for an unknown tenant in that cache performs one authoritative refresh before rejection, so a newly published tenant is usable immediately in a fresh scope, including on another previously cached registry. Successful routing hits make no additional source call. Synchronous resolvers continue to use their already loaded snapshot. A caller may cancel only its own wait; the source is cancelled only after every joined waiter has left. Failed, cancelled, or invalid refreshes never extend stale authority. A refresh affects later scopes only: TenantStoreGuard validates the requested tenant against the ambient tenant context and binds one immutable route before application work. Missing, unknown, re-bound, or mismatched routes fail before its callback runs. For a dynamic source, ProvisioningPlan.ForTenant(...) includes the technical key and finishes by asking the application executor to persist the catalog; AnchorFlow then forces a source refresh and confirms the precise published routes before reporting success. The application executor must prepare required framework and application schemas before publishing those routes. Fixed snapshots retain the original three provider steps and atomically publish the new in-memory snapshot without asking the executor to publish an application catalog.

AddAnchorFlow(...) owns the concrete registry, IAnchorFlowStoreRegistry, ITenantStoreResolver, and the provisioning executor supplied to UseTenancy(...); pre-registering any of those services fails fast so every interface resolves the same instance. ITenantContextAccessor remains replaceable for application identity integration. ITenantStoreResolver.ResolveAsync(...) and ResolveMainAsync(...) are available when application code needs cancellation-aware route resolution; normal scoped work should let AnchorFlow bind the route before application code runs.

HTTP applications add services.AddAnchorFlowAspNetCoreTenancy() and place app.UseAnchorFlowAspNetCoreTenancy() before tenant-scoped endpoints. Adapt an existing scoped TenantInfo to ITenantContextAccessor; do not run an independent ambient tenant resolver beside it. Background jobs carry TenantId explicitly and create and dispose a fresh DI scope for every tenant iteration before setting the tenant context and resolving AnchorFlow services. A scope never switches tenant or store after binding.

For first-tenant bootstrap, create a snapshot with only the tenant-neutral Main route and an explicit TenancyMode; provisioning then adds the first tenant's Write and Projection routes through the same ordered path.

EF Core And Relational Providers

AnchorFlow-managed DbContexts are resolved lazily through IAnchorFlowDbContextProvider<TDbContext> or AnchorFlowRepository<TDbContext>. Ordinary Database-per-Tenant contexts are constructed only after TenantStoreGuard establishes the exact tenant and Write or Projection binding. Shared contexts use the fixed common store of their role without requiring a tenant binding; unrequested registered contexts are not created.

Global application data uses the existing UseWriteDbContexts and UseProjectionDbContexts registries. Shared contexts can contain both global and tenant tables; ordinary Add calls select the common database of their role with or without an ambient tenant. The application owns its row filters, tables and authorization.

services.AddAnchorFlow(options =>
{
    options.UsePostgreSql(globalWriteConnection, globalReadConnection);
    options.UseDatabasePerTenant(tenantWriteTemplate, tenantReadTemplate);
    options.UseTenancy(LoadTenantStoreSnapshotAsync, provisioningExecutor, TimeSpan.FromMinutes(5));
    options.UseWriteDbContexts(contexts => contexts
        .Add<TenantDirectoryDbContext>(useGlobalStore: true)
        .Add<TenantOrdersDbContext>());
    options.UseProjectionDbContexts(contexts => contexts
        .Add<TenantDirectoryReadDbContext>(useGlobalStore: true)
        .Add<TenantOrdersReadDbContext>());
});

UseSqlServer(write, projection) and local UseSqlite(write, projection) provide the same role selection. Existing Add<TDbContext>() and Add<TDbContext>(configureOptions) signatures remain unchanged. The additive Add<TDbContext>(bool useGlobalStore) and Add<TDbContext>(configureOptions, bool useGlobalStore) overloads default to ordinary tenant binding when omitted in Database-per-Tenant mode. true selects the fixed global role independently of the ambient tenant. In Shared mode both bindings use the same common role store. Each Context type has exactly one Write/Projection role and binding; duplicate or contradictory registrations fail at setup.

Global stores retain the provider's fixed base connections when tenant templates are configured. With one base connection, Projection shares the physical Write database while retaining its Projection role. Global application descriptors have a null TenantId and their own logical identities; Main/Control remains technical.

In a fresh application scope, inject a normal Shared Context or a fixed global Database-per-Tenant Context directly, through an application service, or through an AnchorFlowRepository<TDbContext>. All paths share one scoped instance. Role readiness is established before the application-owned catalog source runs, so that source can read its global repository without a recursive source call. Ordinary Database-per-Tenant Contexts still require a valid tenant/store binding. Context construction preserves the caller's tenant and existing binding. No row filters are added to application entities: a directory entity's TenantId is ordinary application data.

Direct Write-store reads observe authoritative data even while a separate Projection database lags. Reads create no command Unit of Work, inbox or outbox and never save automatically; tracked reads are allowed and the application controls use outside a managed invocation. This registration and read path does not select a command's message scope.

Schema management validates or explicitly migrates the fixed global framework Write and Projection roles under the existing provider schema locks before loading the tenant catalog, including roles with no application DbContext registration. This includes ordinary Database-per-Tenant contexts, because a tenant-neutral command or event selects that role. A catalog source can then open a fresh scope and read its global repository without calling itself recursively. Validated global roles remain readable if later catalog or tenant readiness fails; normal runtime admission still requires all configured schema, durability, and transport prerequisites. Application tables, migrations, bootstrap, and permissions remain application-owned; validate-only never initializes them. Context resolution performs no additional schema or catalog query.

Publish global commands through the existing bus and handler/result contracts:

var result = await commandBus.PublishAsync(new InitializeTenantDirectory(), cancellationToken);

Normal commands choose Command.TenantId first, otherwise the current validated invocation tenant, otherwise ITenantContextAccessor set by application code or ASP.NET middleware. AnchorFlow captures this choice once before creating the command scope and retains it through fresh retry attempts. An explicit command target may differ from the caller's tenant; authorization remains application-owned. Without either tenant, a normal command uses global application data rather than technical Main/Control state. A tenantless request allowed by the ASP.NET resolver therefore supports platform operations. Technical framework system messages retain their trusted System metadata. The caller's ambient context is restored after execution.

A settings job can publish an ordinary command in a fresh tenant-neutral application scope and pass SettingsTenantId or AssignedTenantId as business payload. These fields never select a store. A present unknown tenant fails closed instead of falling back to Global. Handlers keep their normal signatures/results and leave saves, transactions, event collection, ordering, and native outbox work to AnchorFlow. Idempotency partitions distinguish Global, System, and every tenant.

Shared commands with or without a tenant may mutate global and tenant tables in one mixed context or across normal contexts from several bounded contexts. Compatible write participants share exactly one connection and transaction, including direct, indirect, and lazy resolution. For example, a platform caller with no tenant can explicitly target Tenant A while assigning a globally unique vehicle to a Tenant-A train atomically. Database-per-Tenant chooses the effective tenant's database or the fixed global database for the entire command, including result/idempotency/native outbox. Ordinary contexts can join that selected global command store; reads outside a command still require a valid binding or fixed global registration.

Database-per-Tenant commands may read a context registered with Add<TContext>(useGlobalStore: true) outside the tenant transaction. Before domain saves or outgoing persistence, AnchorFlow explicitly detects changes in every resolved context and rejects foreign Added/Modified/Deleted/owned/navigation changes and uncommitted aggregate events, including with AutoDetectChangesEnabled = false. This deterministic error is never retried by a transient classifier. A clean foreign read is never saved, enlisted, or collected. ExecuteUpdate, ExecuteDelete, and raw SQL bypass tracking and remain application-owned outside this guarantee; handlers must not use them to mutate foreign stores.

CRUD and DDD use the existing AnchorFlowEvent<TEntity,TId>, AnchorFlowManyEvent<TEntity,TId>, AggregateEvent<TAggregate,TId>, builder and AffectedResourceKey(entityType, resourceId) contracts. The application supplies complete stable identifiers, including composite mapping keys and additional affected resources; no resource flag or EF key lookup is required. Shared resource lanes use the logical resource type plus its full ID across tenant-neutral and A/B callers and across contexts or event types. Database-per-Tenant adds the selected application store scope internally. Aggregate type ID, aggregate ID, sequence and resource versions survive serialization. Producer metadata is captured at emission/collection and passed with persisted consumer work. Strict subscriber and projection modes retain their invocation lanes while also protecting every overlapping Shared resource; disjoint invocation lanes remain independent. Unordered and Independent modes add no resource ordering. DDD checkpoints compare the canonical aggregate stream across Shared caller changes, so an older redelivery cannot regress a read model. Message/receipt/process/notification identities remain separate from resource ordering. The native outbox belongs to the current transaction's origin store; logical message targets never select the origin database or transmit its connection.

Within an active Subscriber, await commandBus.PublishAsync(command, cancellationToken) runs the ordinary command executor with fresh contexts and an independent transaction. It returns the actual result after the child command commits. The subscriber then commits its own changes and receipt; a later subscriber error, cancellation, or host loss cannot roll back that child. Redelivery reuses a stable idempotency key derived from the complete validated origin scope, subscription, event and command type, and returns the persisted result (including null or typed business results) without repeating domain or event effects. Explicit keys and the maximum of one follow-up remain. An explicit command TenantId selects its target; otherwise the current validated invocation or application tenant is inherited, and a tenant-neutral caller selects Global. A DpT child may select a different target store because it owns its separate unit of work. Unsaved parent changes are not visible in that fresh command. Handlers may live in a separate application assembly. Direct managed subscriber writes remain atomic in the subscriber's own transaction.

Global projections use the fixed Read role. Normal Shared Projection contexts may contain global and tenant views together; compatible contexts from several bounded contexts join the same physical connection and transaction. In Database-per-Tenant mode, Add<TReadContext>(useGlobalStore: true) selects the fixed global Read store. Tenant projections may read unchanged fixed Global Read contexts outside their tenant transaction; any foreign tracked changes or uncommitted aggregate events fail before commit. Use a matching Global command/event for a global mutation. A foreign read provides no common cross-database snapshot guarantee, and Write contexts cannot participate as projection writers.

Read-model changes, checkpoints, all-expected projection completions, notification candidates and release intents commit together. Missing or failed expected projections block delivery. Each release request targets the event whose last expected projection completed and additionally sweeps a bounded set of the oldest other ready candidates in the same scope, so a release never rescans every pending event. Recovery releases are untargeted and read a bounded batch of the oldest ready candidates of the scope. Sweeps and untargeted releases skip candidates whose expected projections are still missing or failed, so blocked notifications never displace releasable ones; the barrier itself still decides every release. A release whose bounded read was full enqueues exactly one continuation release for the same scope (and event, when targeted), so an event or scope with more ready candidates than one batch is delivered completely without another trigger. Otherwise, when the scope still holds a barrier-satisfied candidate whose availableAtUtc lies in the future, the release schedules exactly one durable Wolverine wake-up release at the earliest such time, so delayed notifications are delivered once they become available without another trigger. A release that marks nothing and has no remainder enqueues no continuation, and barrier-blocked candidates never schedule wake-ups; their projection completion releases them. A separate Global Read database keeps its own durable release intent; normal replacement-host startup drains pending global intents before business admission and readiness. Recovery failure or cancellation keeps admission closed, while notification recovery can still perform its required handoff. Automatic and manual mappings retain view keys, ignored properties and sequence rules. Global ViewNotification.IsGlobal is true and its retained non-null TenantId is an empty compatibility value. The delivery context carries null TenantId, IsGlobal true and IsSystemLevel false. The application owns recipient choice and authorization through IAnchorFlowViewNotificationDelivery; the empty string never identifies a recipient or tenant.

An application-owned catalog initializer may run a global bootstrap command after Global Write framework-role validation while full tenant/runtime readiness is still false. The application prepares its own tables and implements uniqueness/concurrency policies for competing initializers. A bootstrap that emits messages also needs native message-storage prerequisites in the origin database: missing storage fails before domain commit, and AnchorFlow never substitutes another outbox or drops messages. Bootstrap commits recoverable native rows without starting delivery before full schema readiness. Failure or cancellation withholds full readiness; retries/restarts use fresh command scopes and roll back unsuccessful domain/outgoing work.

For the current complete tenant list, call await registry.RefreshAsync(cancellationToken) and enumerate snapshot.TenantStores.Where(route => route.StoreKind == AnchorFlowStoreKind.Write); each tenant occurs exactly once. Read application metadata from the global directory repository. GetSnapshotAsync may return a cached snapshot.

Write DbContexts registered with UseWriteDbContexts(...) can be injected either directly as TDbContext or through IAnchorFlowDbContextProvider<TDbContext>. During a command, AnchorFlow enlists only the managed Write DbContexts that were actually resolved in that command scope, including contexts resolved indirectly by repositories or application services.

Multiple AnchorFlow-managed DbContexts may share one transaction only in the same scope for a compatible provider, physical database, and role, with one selected Database-per-Tenant scope or the common Shared role store described above. The EF Core integration reuses one scoped provider connection and enlists compatible resolved contexts in that single relational transaction. Other scope/store/provider/database mismatches fail before transaction enlistment; no distributed transaction fallback is provided.

Command retry behavior can be adjusted during setup:

services.AddAnchorFlow(options =>
{
    options.ConfigureCommandExecution(command =>
    {
        command.Retry.MaxAttempts = 3;
        command.Retry.DelayProvider = retry => TimeSpan.FromMilliseconds(100 * retry.Attempt);
        command.OptimisticConcurrency.MaxAttempts = 3;
        command.OptimisticConcurrency.DelayProvider = retry => TimeSpan.FromMilliseconds(50 * retry.Attempt);
    });
});

The retained shared retry-delay API is also available for source-compatible setup and maps to the same command retry profiles:

services.AddAnchorFlow(options =>
{
    options.ConfigureRetryDelays(retry =>
    {
        retry.TransientFailure.Mode = AnchorFlowRetryDelayMode.Exponential;
        retry.TransientFailure.BaseDelay = TimeSpan.FromMilliseconds(100);
        retry.TransientFailure.MaxDelay = TimeSpan.FromSeconds(1);
        retry.OptimisticConcurrency.Mode = AnchorFlowRetryDelayMode.Fixed;
        retry.OptimisticConcurrency.BaseDelay = TimeSpan.FromMilliseconds(50);
    });
});

When native durability is enabled, Local messages created inside that managed transaction are stored as Wolverine native durable work using the same active provider transaction. Ordered application messages are represented at commit by an internal durable release request. The internal release queue and the broker-mode internal dispatch queue are deliberately not partitioned by ordering group, so independent lanes are never serialized onto a shared partition slot and their concurrent processing is bounded only by the queue's parallelism; contention within one lane is resolved by the lane claim and the successor wake. Wolverine recovers that request after a crash; AnchorFlow claims its exact semantic lane head, hands the original application envelope to Wolverine, and marks the lane complete only after Wolverine durably accepts the handoff. Blocked requests use Wolverine retry scheduling, so command replay is neither required nor allowed to bypass lane state. When a lane head completes, AnchorFlow computes the directly ready successor requests inside the same completion transaction and immediately hands a Wolverine-parked successor that is not yet due back to the owning Wolverine listener. A successor that is already due, or due within 500 ms by the node or database clock, is left to Wolverine, so one envelope is never taken by both the wake and the poller as long as the clocks of all nodes and of the database differ by less than 500 ms; native durability therefore requires clock-synchronized (for example NTP) hosts. The Wolverine scheduled-retry poller (1 s) together with the park schedule (1 s, 2 s, 5 s, then every 30 s, the step chosen by the time since the envelope was created, so a request first blocked late parks at the last step immediately) remains the crash-safe fallback, so AnchorFlow adds no timer, sweeper or second scheduler. Parking a blocked request never consumes a Wolverine failure attempt, so work that fails after waiting behind a long lane head still receives its remaining finite retry budget. Database-per-tenant message storage resolves the same validated Write descriptor dynamically through the application catalog.

Host shutdown logging: native durability background work uses the host logging providers while it shuts down. Configure logging providers that tolerate concurrent shutdown and disposal. On Windows, Host.CreateDefaultBuilder(), Host.CreateApplicationBuilder() and WebApplication.CreateBuilder() register the EventLog logging provider, whose loggers throw ObjectDisposedException after disposal. Configure only logging providers that remain safe after disposal, for example builder.Logging.ClearProviders(); builder.Logging.AddConsole();, as the AnchorFlow example and test hosts do. This is a logging recommendation only; AddAnchorFlow(...) setup is unchanged.

Schema management is composed into normal startup. SQLite registers a hosted validator that uses ConfigureSchemaManagement(...); the default mode is validate-only and fails startup without creating or repairing schema objects. Explicit migration requires schema.Mode = AnchorFlowSchemaManagementMode.Migrate or a direct IAnchorFlowSchemaManager.ManageConfiguredStoresAsync(AnchorFlowSchemaManagementRequest.Migrate) call. When a provider schema manager is registered, tenant DbContexts require the full published readiness snapshot; registered global DbContexts require their validated Write or Projection role.

SQLite manages local AnchorFlow lifecycle metadata and current command/ordering metadata through provider manifests. PostgreSQL and SQL Server add production lifecycle metadata, schema locks, explicit indexes, database-time reads, and an atomic fencing primitive consumed by ProcessManager ownership leases. Command manifests include AnchorFlow_CommandIdempotency, AnchorFlow_OrderingLanes, AnchorFlow_OrderingMessages, and AnchorFlow_OrderingMessageLanes; ordering lanes are scoped by provider and write store and store opaque lane identifiers, not raw application ordering keys; resource lanes are shared by all tenants of a write store so cross-tenant work on the same resource stays ordered, while each admission row carries the tenant and store attribution. Resource lanes are keyed by provider and store kind of the physical database, so logical write stores that share one database also share their resource lanes: their work on the same resource is serialized in one admission order, and a completed admission wakes its successor regardless of the successor's write store, while retention and backlog signals stay attributed to the write store of each admission. Since command schema version 6 each AnchorFlow_OrderingMessages row records its write store and exact tenant scope (tenant:<id>, global or system), and a retention index on (StoreId, Status, TenantScope, UpdatedAtUtc, MessageId) serves the bounded, ordered retention batch with an index seek and the per-store backlog signals; ordering retention deletes the lane assignments of retired admissions but keeps each message row, without claim owner and lease, as a Retired tombstone that is never purged, so later re-admissions or replays of the same work id stay deduplicated without a time limit and the message table grows by one row per admission. ProcessManager manifests add generic tenant-scoped AnchorFlow_ProcessInstances, AnchorFlow_ProcessExpectations, AnchorFlow_ProcessCommands, AnchorFlow_ProcessStateChunks, AnchorFlow_DurableCollectionEntries, AnchorFlow_DurableCollectionChunks, AnchorFlow_ProcessDeadLetters and AnchorFlow_ProgressOperations tables; route lookup is scoped by the actual bound store descriptor, tenant/system boundary, ProcessId, SagaId, persisted Process-Type and open expectation identity. The normal transition executor writes process state, durable collection changes, progress operations, expected replies and process-owned command rows in the same EF transaction before handing command envelopes and progress projection fan-out work to the configured AnchorFlow message runtime. Each process-owned command crosses that runtime as an AnchorFlow-internal durable dispatch and is then invoked through the ordinary typed ICommandHandler<TCommand, TResult> and existing command Unit of Work; the typed result and command events join the same transaction and outbox fan-out. PostgreSQL and SQL Server provider validation proves that normal and compensation transition commands are stored through Wolverine native outbox storage in the same transaction and roll back with process rows when the enlisted outbox rejects the transaction. Fully routed unexpected replies either close without mutation or write one controlled dead-letter record according to AnchorFlow ProcessManager options; route misses remain NotRouted and never enter the policy, while duplicate or late replies for terminal expectations are suppressed before dead-letter handling. Expected replies preserve UseDefault, positive Explicit and None timeout semantics, with a 30-minute default; default and explicit replies schedule stable ProcessCommandTimedOut feedback through the existing internal Wolverine ProcessManager dispatch wrapper in the owning Work store. Command and expectation completion cancel that exact native Id and Destination in the same transaction; a timeout already captured by recovery remains terminally idempotent. Process-owned scheduled continuations use the same productive dispatch path instead of raw application payload delivery. ProcessManager transitions claim a provider-time, tenant/store-scoped fence before mutation, verify the exact ownership token before saving state, and retry ownership or EF optimistic-concurrency conflicts with fresh dependency-injection scopes. A local child-process crash probe proves a ProcessManager-emitted durable command is retried and handled by a restarted process after a failed pre-ack delivery attempt; the completed provider, broker, 1 -> 3 -> 1 scale and abrupt recovery matrices exercise the same durable continuation, ordering, tenant isolation and fencing guarantees. The coordinator records per-message lane sequences so overlapping resource work waits for every predecessor lane while disjoint lanes can be claimed independently. Provider SQL uses static AnchorFlow-owned identifiers and parameters for tenant, store, owner, token, lease and progress values; connection strings, tenant labels, payloads, and raw provider exceptions are not part of public operations data.

Compatibility Control

US-01 checks in a machine-readable v1 public API baseline under eng/PublicApi/v1, a classification file, and an initially empty approved-breaks file. Retained category-1 and category-2 contracts must remain source-compatible unless a later explicit approved-break entry names the exact symbol.

VirtuRail-specific inventory, migration, source modification, restore, build, test, and report work is deferred by user direction to a separate VirtuRail session. That deferral does not weaken the mandatory generic v1 API baseline, classification, API diff, characterization, or retained-signature compatibility fixture.

The later consumer handover must pin the accepted AnchorFlow commit and package (this tree targets package 0.3.1 and Wolverine 6.51.0), use the Api role for durable API publication, and apply the explicit native schema setup above. VirtuRail acceptance remains Deferred by user to separate VirtuRail session: repeat its existing signed historical-record replay with Mutated=false, without direct DeviceRegistry writes or a Worker role for that API use case. Under the unchanged deadline, verify the current owning source state is fully Applied in the exact global-projection target, including manifest and membership, and then disconnect a real peer. This requires the existing consumer path; it introduces no replay API or additional pumping.

Validation

DataProtection and the existing Microsoft.Extensions dependencies use patch version 10.0.13. DataProtection resolves System.Security.Cryptography.Xml and Pkcs 10.0.13; applications must retain their existing application discriminator and persisted key ring when upgrading so outstanding continuation tokens remain readable. The test environment uses Testcontainers 4.16.0, which resolves SSH.NET 2026.0.0, and explicitly pins Pkcs 10.0.13 for its independent SqlClient dependency graph. These dependency updates preserve the existing container images and normal AddAnchorFlow(...) setup.

The repository provides an invocation-owned test-environment runner for real infrastructure tests. Docker must be installed, running, and available to the current user. Each runner invocation creates a new run id and authenticated loopback host, starts fresh physical containers lazily, labels them with that exact run id, and tears down every invocation-owned container and network before returning. A successful test child is not reported as successful when host teardown fails or labelled resources remain.

The host gives each fixture class a normal client, and every Fact or Theory row explicitly acquires and disposes its own host-issued TestScope. Physical PostgreSQL, SQL Server, and general RabbitMQ services are shared only inside that one runner invocation. Relational scopes receive unique least-privilege logical databases; RabbitMQ scopes receive isolated virtual hosts and topology prefixes. A whole-broker restart uses a separate lazy physical RabbitMQ container and is available only through a RabbitMqRestartFault scope; the host leases that physical restart capability to one active fault scope at a time while normal RabbitMQ scopes remain parallel. Azure Service Bus uses at most two private emulator/SQL stack generations concurrently. Each immutable generation has 50 topics, providing 25 isolated slots with a public topic plus an internal topic with Work/notification filters. One active scope leases each stack; a released slot remains consumed within its generation. After every slot is consumed and its last lease is released, the host removes that generation's exact containers, backend and network before creating a fresh generation with distinct topology and owner identities. Allocation waits use cancellation signals; startup or cleanup failure remains a fixture failure. Generation and slot accounting is recorded by the host. No physical or logical allocation is reused by a later runner invocation.

PostgreSQL, SQL Server, Wolverine, and Reliability provider/broker rows use these explicit per-row scopes and no longer own Testcontainers, physical-resource startup gates, or shared mutable test probes. Reliability child hosts inherit the scope-qualified stores, broker topology, worker identity, signal path, and artifact identity; the parent stops every child before releasing its scopes. The two-stack hosted-worker proof covers simultaneous PostgreSQL and SQL Server 1 -> 3 -> 1 workloads through normal AddAnchorFlow(...) application setup and scoped external ingress; each scope has its own persisted worker identities and raw effect-owner evidence excludes the peer scope. The TestEnvironment is test-only infrastructure and is never configured by an AnchorFlow application.

Run adopted real-infrastructure tests through eng/Invoke-AnchorFlowTests.ps1. The runner requires Docker, restores NuGet packages, builds before testing, invokes dotnet test with the repository-required --no-build --no-restore -m:28 -- RunConfiguration.MaxCpuCount=28 shape, and emits a passed/failed/skipped/total TRX ledger for every selected test project, including zero-test projects. A direct invocation of a test class that adopts TestEnvironmentFixture fails closed instead of starting fallback containers. ANCHORFLOW_TEST_ENVIRONMENT_ENDPOINT, ANCHORFLOW_TEST_ENVIRONMENT_RUN_ID, and ANCHORFLOW_TEST_ENVIRONMENT_TOKEN are internal runner-owned child-process variables; do not set them manually and never log their values.

Run the normal local matrix with:

powershell -NoProfile -ExecutionPolicy Bypass -File eng/Invoke-AnchorFlowTests.ps1 -Configuration Release

For a focused Architecture test command, the runner restores the solution, builds the selected target and host, and forwards the test tail:

powershell -NoProfile -ExecutionPolicy Bypass -File eng/Invoke-AnchorFlowTests.ps1 -Configuration Release -- --filter FullyQualifiedName~MyFocusedTests

Select a different focused project explicitly when needed:

powershell -NoProfile -ExecutionPolicy Bypass -File eng/Invoke-AnchorFlowTests.ps1 -Configuration Release -TestProject tests/AnchorFlow.Wolverine.Tests/AnchorFlow.Wolverine.Tests.csproj -- --filter FullyQualifiedName~MyFocusedTests

Test classes that build hosts through the repository test host attach their xUnit output with AnchorFlowTestOutput.Attach(output); the host then writes its bounded log (at most 2,000 entries of at most 2,000 characters, with connection-string secrets and URI credentials redacted and without stack traces) into that test's result when it is disposed. A direct real-container test invocation that adopts TestEnvironmentFixture fails closed with a diagnostic that names eng/Invoke-AnchorFlowTests.ps1; it never starts a fallback local container. The complete command is unfiltered and covers every solution test project, including AnchorFlow.Examples.Tests. Each project writes a run-id-prefixed TRX file under its own TestResults directory; these ignored diagnostic files can be removed after evidence has been recorded.

The local matrix uses the official Azure Service Bus emulator only for local AMQP, topology, session, retry, and redelivery integration. It does not prove managed Azure availability, SLA, geo-replication, or operational behavior. The live Azure Service Bus profile remains separate: it needs a real namespace, credentials, and its explicit live-category filters, and it is not run or configured by this local emulator runner.

The root README.md is the only README in the repository.

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.3.1 0 10/17/2026
0.3.0 0 10/17/2026
0.2.1 82 10/6/2026
0.2.0 127 8/12/2026