AnchorFlow.Projections
0.2.0
dotnet add package AnchorFlow.Projections --version 0.2.0
NuGet\Install-Package AnchorFlow.Projections -Version 0.2.0
<PackageReference Include="AnchorFlow.Projections" Version="0.2.0" />
<PackageVersion Include="AnchorFlow.Projections" Version="0.2.0" />
<PackageReference Include="AnchorFlow.Projections" />
paket add AnchorFlow.Projections --version 0.2.0
#r "nuget: AnchorFlow.Projections, 0.2.0"
#:package AnchorFlow.Projections@0.2.0
#addin nuget:?package=AnchorFlow.Projections&version=0.2.0
#tool nuget:?package=AnchorFlow.Projections&version=0.2.0
AnchorFlow v2
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.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. When RabbitMQ topology is externally provisioned with AutoProvision = false, configure ManagementEndpoint so AnchorFlow can validate exchange, queue and binding readiness without declaring or repairing broker topology.
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 permits application/API request handling without background intake, Publisher permits durable publication, Worker starts command/subscriber/projection/ProcessManager consumers, and NotificationDispatcher releases projection notification candidates. The default Combined role keeps the single-host setup path. Durable roles require UseNativeDurability(...); API-only hosts do not register transport startup readiness or worker listeners. Role selection is immutable after AddAnchorFlow(...) builds the host service graph.
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 so 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; 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. 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 exact tenant/write-store scope, a bounded batch size and only AnchorFlow-owned semantic cleanup scopes: command idempotency, completed ProcessManager graphs and resolved dead letters. 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.
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.Abstractionscontains stable application-facing contracts only, including provider-neutral storage, operations and retention result contracts.AnchorFlow.Coredepends only onAnchorFlow.Abstractionsand contains provider-neutral runtime role, bounded work-admission, redacted telemetry and operations facade contracts.AnchorFlow.ViewNotificationscontains app-owned view notification delivery contracts, view attributes, descriptor caching and notification payload contracts.AnchorFlow.Projectionscontains projection handler contracts, stable manifest construction, projection notification staging and all-projections barrier primitives.AnchorFlow.ProcessManagercontains 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.Progresscontains ProcessManager-bound progress reporters, committed progress-change events, bounded snapshot readers, localization helpers and progress view-notification payloads.AnchorFlow.Testingcontains deterministic harnesses, instance-scoped fakes and public bounded message-pump contracts for application tests.AnchorFlow.AspNetCorecontains ASP.NET Core tenant middleware and health-check adapters over AnchorFlow-owned contracts.AnchorFlow.EntityFrameworkCorecontains 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.Sqliteprovides the local SQLite EF provider and schema lifecycle implementation.AnchorFlow.PostgreSqlprovides the PostgreSQL EF provider, schema lifecycle implementation, database-time clock, and fencing primitive.AnchorFlow.SqlServerprovides the SQL Server EF provider, schema lifecycle implementation, database-time clock, and fencing primitive.AnchorFlow.Wolverineprovides 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.AnchorFlowis 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, stages at most one follow-up command with an explicit or v1 fallback idempotency key, 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. Delivery is at least once: if application delivery fails after the app-visible effect but before the released mark is persisted, redelivery uses the same NotificationId. The completed production topology, scale and abrupt child-process fault matrices cover this projection and notification-barrier behavior.
ProcessManager state may include DurableList<T>, DurableDictionary<TKey,TValue> and DurableBlob<T> properties. AnchorFlow binds those helpers to the current tenant/system 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, tenant/system 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, 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 tenant/system-scoped snapshots from committed operations, 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. 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. 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>. The framework constructs a managed DbContext only after TenantStoreGuard has established the exact tenant and Write or Projection store binding for the current scope; unrequested registered contexts are not created.
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 when they are resolved in the same scope for the same provider, physical database, tenant and store. The EF Core integration reuses one scoped provider connection for that compatible descriptor and ManagedDbContextTransactionCoordinator enlists the resolved contexts in that single relational transaction. Store/provider/database/tenant 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. 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. Database-per-tenant message storage resolves the same validated Write descriptor dynamically through the application catalog.
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. Managed DbContexts require published schema readiness when a provider schema manager is registered.
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 provider/store/tenant scoped and store opaque lane identifiers, not raw application ordering keys. 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, and 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.
Validation
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 two lazy private emulator/SQL stacks with 50 immutable topic slots each; a scope receives one topic with session, fan-out-session, and non-session subscriptions, one active scope leases each stack, released slots remain consumed for the run, and the 101st allocation fails closed with a bounded diagnostic. 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
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 | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AnchorFlow.Abstractions (>= 0.2.0)
- AnchorFlow.Core (>= 0.2.0)
- AnchorFlow.ViewNotifications (>= 0.2.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on AnchorFlow.Projections:
| Package | Downloads |
|---|---|
|
AnchorFlow.EntityFrameworkCore
Entity Framework Core command, projection, subscriber, ProcessManager, and operations persistence for AnchorFlow. |
|
|
AnchorFlow.Testing
Deterministic testing harnesses and fixtures for AnchorFlow applications. |
|
|
AnchorFlow
Application-facing dependency injection and composition package for AnchorFlow. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.0 | 43 | 8/12/2026 |