AnchorFlow.EntityFrameworkCore 0.1.0-alpha.2

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

AnchorFlow

AnchorFlow is a modular .NET framework for tenant-scoped CRUD/application-service systems. It provides command and consumer pipelines, idempotency, durable outbox/inbox storage, provider-backed projection checkpoints, view-notification dispatch, a custom typed process manager, storage providers, Wolverine transport adapters, health/readiness checks, and reusable test harnesses.

This is the only README in the workspace. It is the canonical implementation map for agents and is also packed as the NuGet package readme for every AnchorFlow package. Read it before changing code, then inspect the named files and tests that match the task.

Quick Start

The normal integration path is one direct code-first registration:

services.AddAnchorFlow(options =>
{
  options.UseSqlite("Data Source=app.db");
  options.EnsureWriteTablesCreated();
  options.EnsureProjectionTablesCreated();
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.AddSubscriber<CustomerCreated, CustomerWelcomeSubscriber>();
  options.UseLocalTransport();
  options.RunApiWorkerAndNotificationDispatcher(runtime =>
  {
    runtime.Worker.Id = "local-dev-runtime";
    runtime.Worker.MessagePublishing.MaxConcurrent = 1;
    runtime.Worker.AsyncProcessing.MaxConcurrent = 4;
    runtime.NotificationDispatcher.Id = "local-dev-runtime";
    runtime.NotificationDispatcher.NotificationDelivery.MaxConcurrent = 4;
  });
});

AddAnchorFlow(options => { ... }) is the beginner and normal application path. For SQLite examples and local single-database hosts, UseSqlite(connectionString) selects the provider, shared storage topology, and EF store configuration in one call. Register write-side application DbContexts through AnchorFlow with UseWriteDbContexts(...); do not also register those same DbContexts with services.AddDbContext<TDbContext>(). Register normal event subscribers with AddSubscriber<TEvent,TSubscriber>(). Command handlers may inject app-owned services, repositories, or managed DbContexts directly. AnchorFlow tracks resolved DbContext instances, not repositories, and owns SaveChanges, commit, rollback, idempotency, and outbox staging.

AnchorFlow does not read appsettings.json, IConfiguration, environment variables, command-line arguments, or secret stores. Host applications own configuration loading and may read any source they want; they pass direct strings, values, records, structs, and delegates into AnchorFlow.

Command and projection execution retries use one shared retry-delay profile. Command and projection retry attempts default to MaxAttempts = 3. Retry delays default to Mode = Exponential, BaseDelay = 50 ms, and MaxDelay = 2 s for both optimistic concurrency and classifier-approved transient failures. Exponential retries run the first retry immediately, then grow from BaseDelay up to MaxDelay. Hosts may also provide conservative transient-failure classifiers for non-concurrency DbUpdateException failures that the active provider reports as transient infrastructure faults:

services.AddAnchorFlow(options =>
{
  options.UseSqlite("Data Source=app.db");
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.ConfigureRetryDelays(retries =>
  {
    retries.OptimisticConcurrency.Mode = AnchorFlowRetryDelayMode.Exponential;
    retries.OptimisticConcurrency.BaseDelay = TimeSpan.FromMilliseconds(50);
    retries.OptimisticConcurrency.MaxDelay = TimeSpan.FromSeconds(2);
    retries.TransientFailure.Mode = AnchorFlowRetryDelayMode.Exponential;
    retries.TransientFailure.BaseDelay = TimeSpan.FromMilliseconds(50);
    retries.TransientFailure.MaxDelay = TimeSpan.FromSeconds(2);
  });
  options.ConfigureCommandExecution(command =>
  {
    command.OptimisticConcurrency.MaxAttempts = 3;
    command.Retry.TransientFailureClassifier = context =>
      IsProviderTransient(context.Exception)
        ? AnchorFlowRetryDecision.Retry()
        : AnchorFlowRetryDecision.DoNotRetry();
  });
  options.ConfigureProjectionExecution(projection =>
  {
    projection.OptimisticConcurrency.MaxAttempts = 3;
    projection.Retry.TransientFailureClassifier = context =>
      IsProviderTransient(context.Exception)
        ? AnchorFlowRetryDecision.Retry()
        : AnchorFlowRetryDecision.DoNotRetry();
  });
});

Every command retry reruns the full command attempt in a fresh scope with fresh managed DbContext instances. Every projection retry reruns the projection work item or projection group in a fresh projection execution scope before a failed checkpoint is recorded. Exhausting optimistic-concurrency attempts or a classifier-approved transient retry is a technical failure; for process-owned commands that terminal path is reported through ProcessManager failure feedback rather than as a business result reply.

Runtime role ids and work limits are also host-provided. In Kubernetes, applications commonly read a pod-unique value such as the Downward API metadata.uid through their own configuration layer, then pass that value into RunWorker(...), RunNotificationDispatcher(...), or RunApiWorkerAndNotificationDispatcher(...). AnchorFlow itself does not read Kubernetes metadata, environment variables, machine names, or pod names to choose a role or identity.

Shared database setup can use provider shortcuts when a provider package supplies them:

services.AddScoped<AppCardProjection>();

services.AddAnchorFlow(options =>
{
  options.UseSqlite(writeConnectionString, projectionConnectionString);
  options.EnsureWriteTablesCreated();
  options.EnsureProjectionTablesCreated();
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.UseProjectionDbContexts(projection => projection.Add<AppReadDbContext>());
  options.UseLocalTransport();
  options.RunApiWorkerAndNotificationDispatcher("local-dev-runtime");
});

Database-per-tenant setup uses connection-string templates and a tenant source. The placeholder is host-selected; this example uses [ResortId] because the host wants technical database names based on resort keys, not raw business tenant ids:

services.AddAnchorFlow(options =>
{
  options.UseSqlite();
  options.UseDatabasePerTenant(
    "Data Source=stores/[ResortId]-write.db",
    "Data Source=stores/[ResortId]-projection.db",
    "[ResortId]");
  options.UseTenants([
    new AnchorFlowTenant("0f8fad5b-d9cb-469f-a165-70867728950e", "resort_a"),
    new AnchorFlowTenant("7c9e6679-7425-40de-944b-e07fc1f90ae7", "resort_b")
  ]);
  options.UseWriteStoreEfCore(write =>
  {
    write.UseConnection(
      (serviceProvider, store) => new SqliteConnection(store.ConnectionInfo),
      (serviceProvider, connection, builder) =>
        builder.UseSqlite((SqliteConnection)connection));
  });
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.UseLocalTransport();
  options.RunWorker("resort-worker-01");
});

Use a runtime-refreshable async source when the tenant catalog can change while the process is running. By default, AnchorFlow calls the delegate during store resolution/enumeration so the host can expose catalog changes without restarting:

services.AddAnchorFlow(options =>
{
  options.UsePostgreSql();
  options.UseDatabasePerTenant(
    "Host=db;Database=write_[TenantKey]",
    "Host=db;Database=projection_[TenantKey]");
  options.UseTenants(async cancellationToken =>
    await tenantCatalog.LoadAnchorFlowTenantsAsync(cancellationToken));
  options.UseWriteStoreEfCore(write =>
  {
    write.UseConnection(
      (serviceProvider, store) => new NpgsqlConnection(store.ConnectionInfo),
      (serviceProvider, connection, builder) =>
        builder.UseNpgsql((NpgsqlConnection)connection));
  });
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.UseAzureServiceBus(serviceBusConnectionString);
  options.RunWorker("projection-worker-01");
});

If loading the tenant catalog is expensive and short-lived staleness is acceptable for the host, opt into the built-in catalog cache:

options.UseTenants(
  async cancellationToken => await tenantCatalog.LoadAnchorFlowTenantsAsync(cancellationToken),
  TimeSpan.FromMinutes(1));

The cached overload reuses a successfully loaded tenant catalog for the requested positive duration. It reduces repeated catalog lookups during store enumeration, but it does not change tenant isolation: commands, events, projections, notifications, outbox/inbox records, and process state still run under the tenant captured for each work item. Use the uncached overload when every store resolution must observe the latest catalog immediately.

Providers:

  • UseSqlite(connectionString) uses one shared SQLite database and configures the SQLite provider, topology, write-store EF options, and projection-store EF options.
  • UseSqlite(writeConnectionString, projectionConnectionString) uses separate SQLite write and projection databases with the same provider shortcut.
  • UseSqlite() only selects the SQLite provider; use it with explicit topology/store configuration for advanced cases such as database-per-tenant.
  • UsePostgreSql() uses AnchorFlow.PostgreSql and is the production-like provider with provider contract tests.
  • UseSqlServer() is blocked during startup validation until a real AnchorFlow.SqlServer provider and SQL Server provider contract tests exist.

Transports and roles:

options.UseLocalTransport();                  // local in-process transport
options.UseRabbitMq(rabbitMqConnectionString);
options.UseAzureServiceBus(serviceBusConnectionString);

options.RunApi();                             // API-side command entry points
options.RunWorker("worker-01");               // Worker listeners, sweepers, projections
options.RunApiAndWorker("local-dev-worker");  // combined local/dev API+Worker host
options.RunNotificationDispatcher("dispatcher-01"); // app-owned view notification delivery
options.RunApiWorkerAndNotificationDispatcher("local-dev-runtime"); // all capabilities in one local host

Production pods usually use the same Docker image and select behavior from host configuration:

options.RunApi();

options.RunWorker(worker =>
{
  worker.Id = runtimeId;
  worker.MessagePublishing.MaxConcurrent = 1;
  worker.AsyncProcessing.MaxConcurrent = 4;
});

options.RunNotificationDispatcher(dispatcher =>
{
  dispatcher.Id = runtimeId;
  dispatcher.NotificationDelivery.MaxConcurrent = 4;
});

MessagePublishing, AsyncProcessing, and NotificationDelivery concurrency values are per process/pod. Total effective concurrency scales with replicas: three Worker pods with AsyncProcessing.MaxConcurrent = 4 can run up to twelve asynchronous work items across the deployment, subject to tenant, subscription, storage, and transport limits. A conservative starting point for a 2 vCPU / 2 GB Worker pod is MessagePublishing.MaxConcurrent = 1 and AsyncProcessing.MaxConcurrent = 4; for a 2 vCPU / 2 GB NotificationDispatcher pod, start with NotificationDelivery.MaxConcurrent = 4 and raise it only after observing delivery latency, downstream throttling, and database claim contention.

Projection processing has one additional tuning value on the shared runtime batching options. Advanced hosts that configure the Wolverine adapter directly can set it on AnchorFlowWolverineOptions.RuntimeBatching:

services.AddAnchorFlowWolverine(options =>
{
  options.UseCapabilities(AnchorFlowRuntimeCapabilities.Worker);
  options.WorkerId = runtimeId;
  options.RuntimeBatching.MaxConcurrentUngroupedProjectionWorkItems = 2;
});

MaxConcurrentUngroupedProjectionWorkItems controls how many independent, ungrouped projection work items for one event may run at the same time in one Worker process. The default is 1, which keeps projection processing sequential. Increase it only for projection handlers that do not depend on each other's in-memory side effects and after watching database pressure. Grouped projection batches remain sequential and atomic, projection checkpoints still advance through the runtime, and notification delivery still goes through the notification outbox after projection changes are persisted. Normal AddAnchorFlow(...) role options remain the recommended setup path unless a host already owns low-level Wolverine adapter configuration.

Developer-facing work names:

  • MessagePublishing: publishes committed messages from durable storage to the configured transport.
  • AsyncProcessing: runs durable asynchronous work such as subscribers, projections, and ProcessManagers.
  • NotificationDelivery: delivers staged view notifications through the registered app-owned IAnchorFlowViewNotificationDelivery.

Beginner setup does not require internal table names, UseWolverine, AnchorFlowWolverineOptions, low-level store descriptors, provider SPI types, or direct Wolverine Saga APIs.

Repository-Friendly Write DbContexts

One AddAnchorFlow(options => { ... }) call can register one or more AnchorFlow-managed write DbContexts. Provider shortcuts such as UseSqlite(...) define the current write-store connection and provider options for normal setup. UseWriteDbContexts(...) lists the app DbContext types that may participate in command or consumer invocations without declaring command routes:

services.AddAnchorFlow(options =>
{
  options.UseSqlite(writeConnectionString, projectionConnectionString);

  options.UseWriteDbContexts(write =>
  {
    write.Add<TicketingDbContext>();
    write.Add<TrainManagementDbContext>();
  });

  options.UseLocalTransport();
  options.RunApiAndWorker("operations-local");
});

AnchorFlow-managed write DbContexts map application write entities only. Do not map AnchorFlow technical tables into normal application write DbContexts or app migrations. The command pipeline owns the unit-of-work boundary: command handlers mutate their application DbContext and emit messages through ICommandContext, but they do not call SaveChanges, open or commit transactions, send broker messages, or perform transport delivery directly.

Repository patterns are optional and app-owned:

  • Inject the managed TDbContext directly when the handler or app service is the right place for persistence code.
  • Inject an app repository that itself depends on the managed TDbContext.
  • Inject an app service that coordinates multiple repositories or DbContexts.
  • Inject IAnchorFlowDbContextProvider<TDbContext> for lazy/provider-based repository patterns.
  • Derive a repository from AnchorFlowRepository<TDbContext> when the protected DbContext and Set<TEntity>() helpers reduce boilerplate.

AnchorFlow tracks DbContexts resolved in the invocation scope. It does not track repository instances, and repositories should not call SaveChangesAsync in the normal managed command path.

CRUD Bulk Chunks

CRUD bulk commands should process a chunk as one technical commit unit. A command may update, delete, or insert many application entities in one AnchorFlow-managed transaction, but the handler still follows the normal command pipeline rules: mutate the managed DbContext, emit facts through ICommandContext, and let AnchorFlow own SaveChanges, commit, rollback, idempotency, and outbox staging. Normal tracked EF updates are not hidden n-of-m best-effort operations: the command transaction commits as a whole or rolls back as a whole.

Bulk handlers should prefer set-based writes and database constraints. Use EF Core fluent operations such as ExecuteUpdateAsync and ExecuteDeleteAsync when they express the change cleanly. Their affected-row counts are database facts about that set-based statement, not automatic AnchorFlow business success/failure semantics. Do not introduce per-item ExistsAsync or AnyAsync loops to guess conflicts before each item write; rely on set filters, unique constraints, foreign keys, optimistic concurrency, and database conflict behavior, then let application code decide the business meaning in its typed result.

The chunk is the smallest reliable technical result. If the chunk transaction rolls back, AnchorFlow cannot infer per-item success or partial commit state from normal tracked updates. The default generic failure mapping is AnchorFlowBulkChunkFailurePolicy.MarkWholeChunkFailed; SplitAndRetry and StopBulkAction are vocabulary for future coordinators and guidance, not automatic retry-splitting behavior in this rollout.

Provider conflict behavior is described with AnchorFlowBulkConflictCapability. The enum is technical capability vocabulary only: for example, a provider may be able to ignore, report, or update on conflicts, but AnchorFlow does not decide whether an ignored row means already exists, no-op, rejected, merged, skipped, or updated. Application code owns that interpretation.

Bulk item terminal outcomes reuse AnchorFlowOperationOutcome. AnchorFlow intentionally does not define a separate AnchorFlowBulkItemOutcome enum for the same state set.

Generic CRUD helper APIs such as IAnchorFlowBulkWriter<TEntity,TKey,TItem>, AnchorFlowBulkWriteResult<TKey>, and AnchorFlowBulkWriteItemResult<TKey> are deferred. They should be designed only after provider SPI shape, capability reporting, chunk rollback semantics, and result semantics are proven clean without app-specific tables or business-semantics shortcuts.

Operation Results And Progress

Expected business outcomes should be result values or replies, not technical exceptions. Use IAnchorFlowOperationResult or an app-defined result record with AnchorFlowOperationOutcome, an optional app-owned AnchorFlowOperationResultCode, and a sanitized message when generic outcome vocabulary helps the caller or a ProcessManager. A process-owned command's business failure result arrives through the normal expected reply and is handled by the matching When(TReply, ...) transition. Technical terminal failure arrives through ProcessCommandFailed, and missing expected replies arrive through ProcessCommandTimedOut. Reserve exceptions for unexpected technical faults so retry, dead-letter, failure-feedback, and timeout paths remain meaningful.

Long-running workflows should report UI state through generic progress primitives. ProcessContext.Progress stages language-neutral operations in the same transition as process state, durable collection changes, outbox rows, expectations, and inbox completion. Progress uses app-defined AnchorFlowProgressKind, AnchorFlowProgressSectionKey, AnchorFlowProgressTextKey, AnchorFlowProgressIssueCode, and optional AnchorFlowProgressItemKey values. Store text tokens and stable argument names, not translated strings.

Applications can read progress with IAnchorFlowProgressReader and materialize AnchorFlowProgressSnapshot instances. Localization remains app-owned: implement IAnchorFlowProgressLocalizer or localize snapshots client-side from the stored text keys and arguments. The optional AnchorFlowProgressSnapshotLocalizer is a helper over app-provided localization rules; AnchorFlow does not choose culture, authorization, HTTP shape, or DTO shape.

Progress view notifications are generic view notifications. Projection code can stage language-neutral progress snapshots through AddProgressNotification(...); dispatch still goes through the notification outbox and app-owned IAnchorFlowViewNotificationDelivery. Do not send progress directly from command handlers, ProcessManagers, projections, or SignalR hubs, and do not create app-specific framework notification types for progress.

Imports are an application-owned composition pattern, not a framework feature. Model an import as application state such as:

ImportSession -> TypeRuns -> Chunks -> Items

Use ProcessManager state for execution, DurableList<T>/DurableDictionary<TKey,TValue>/DurableBlob<T> for large process-owned data, app commands and replies for type runs or chunks, AnchorFlowOperationOutcome for expected business results, and generic progress sections for user-visible status. For grouped imports, count every expected reply, ProcessCommandFailed, and ProcessCommandTimedOut as terminal for its application-level item key, then compensate only already committed successful steps when the workflow needs all-or-none business behavior. The framework storage remains generic and tenant-scoped; do not add import-specific framework tables, endpoints, controllers, hubs, or special progress concepts.

DDD Aggregate Roots

Use AggregateRoot<TAggregateRoot,TId> when an application entity is a DDD aggregate root: it owns invariants, changes child entities through root methods, and emits domain facts that should be committed with the same command or consumer transaction as the aggregate state. Aggregate roots should not save changes, publish to brokers, stage view notifications, call repositories, or run long workflows. Command handlers load the aggregate through an AnchorFlow-managed write DbContext or repository, call aggregate methods, and return the command result:

public async ValueTask<OrderResult> HandleAsync(
  SubmitOrderCommand command,
  ICommandContext context,
  CancellationToken cancellationToken = default)
{
  var order = await this.orders.GetAsync(command.OrderId, cancellationToken);
  order.Submit(command.SubmittedAtUtc);
  return OrderResult.From(order);
}

No manual context.Emit(...) call is needed for aggregate domain events. AnchorFlow collects uncommitted aggregate events from tracked aggregate roots in AnchorFlow-managed write DbContexts and stages them in the outbox as part of the same transaction.

Aggregate Events

Aggregate events derive from AggregateEvent<TAggregateRoot,TId>. The primary resource id is always the aggregate root id, so subscribers, projections, and process managers see the changed aggregate as the affected resource:

public readonly record struct OrderId(string Value);

public sealed record OrderSubmitted(
  OrderId PrimaryResourceId,
  DateTimeOffset SubmittedAtUtc) : AggregateEvent<Order, OrderId>(PrimaryResourceId);

public sealed class Order : AggregateRoot<Order, OrderId>,
  IEmit<Order, OrderId, OrderSubmitted>
{
  private bool submitted;

  public override OrderId Id { get; }

  public void Submit(DateTimeOffset submittedAtUtc)
  {
    if (this.submitted)
    {
      throw new InvalidOperationException("The order has already been submitted.");
    }

    this.Emit(new OrderSubmitted(this.Id, submittedAtUtc));
  }

  public void Apply(OrderSubmitted @event)
  {
    this.submitted = true;
  }
}

Emit(...) is the aggregate API name because it records a domain fact from aggregate behavior. There is no aggregate Raise(...) API. Emit(...) validates the event belongs to the current aggregate id, verifies the aggregate implements the matching IEmit<TAggregateRoot,TId,TEvent>, calls the public Apply(TEvent) method, and then records the event as uncommitted. If Apply(...) throws, the event is not recorded.

IEmit<TAggregateRoot,TId,TEvent> is intentionally explicit in the aggregate type list. It states which event types the aggregate can apply and keeps each Apply(...) method strongly typed for future replay support. Apply(...) must be a public instance method; explicit-interface-only implementations are rejected. Calling Apply(...) directly is replay semantics: it mutates aggregate state but does not record a new uncommitted event.

Child Entity Changes

Child entities do not emit independent AnchorFlow aggregate events. Route child changes through the aggregate root and model the event as a root-scoped event with child ids in the payload:

public sealed record OrderLineQuantityChanged(
  OrderId PrimaryResourceId,
  OrderLineId LineId,
  int Quantity) : AggregateEvent<Order, OrderId>(PrimaryResourceId);

Do not model the same fact as AggregateEvent<OrderLine,OrderLineId> from inside Order. The aggregate root is the transactional consistency boundary, and AnchorFlow collection follows tracked aggregate roots.

Automatic Outbox Collection

Automatic aggregate event collection works for aggregate roots tracked by AnchorFlow-managed write DbContexts registered through UseWriteDbContexts(...). It does not apply to unmanaged services.AddDbContext<TDbContext>() registrations. Register application write DbContexts through AnchorFlow when aggregate events should be collected:

services.AddAnchorFlow(options =>
{
  options.UseSqlite(writeConnectionString, projectionConnectionString);
  options.UseWriteDbContexts(write => write.Add<OrdersDbContext>());
  options.UseLocalTransport();
  options.RunApiAndWorker("orders-local");
});

During command and consumer invocation, AnchorFlow tracks aggregate candidates when EF tracks entities, deduplicates aggregate root instances by reference, preserves aggregate-local emission order, stages uncommitted events before outbox records are persisted, and clears those events only after the transaction commits. It does not promise global ordering across multiple aggregate roots changed by one command. The collector is designed for the hot path: it does not scan ChangeTracker.Entries() or call DetectChanges() at commit time and does not query the database to discover events.

Event Sourcing Boundary

DDD aggregate support in v1 is not event sourcing. AnchorFlow stores current aggregate state through EF and writes emitted aggregate events to the transactional outbox for subscribers, projections, process managers, and notifications. There are no event streams, snapshots, expected stream versions, historical replay APIs, historical projection rebuilds, historical event schema migration handlers, or dedicated event database integration. Public Apply(...) methods keep a future replay path possible, but current outbox rows are not a canonical event log.

Aggregate domain events are not integration events by default. Use aggregate events for facts inside the application boundary. Map or emit cross-boundary integration contracts outside aggregates, for example from a command handler/application service with EmitIntegration(...) when the integration event belongs to the command use case, or from subscribers/process managers when it belongs to asynchronous workflow.

Legacy Command Routing

The route-based command API still exists for existing hosts and focused tests. UseWriteDbContext<TDbContext>(...) routes commands explicitly. Use Handles<TCommand>() for individual command types, or HandleCommandsFromAssembly(...) when one DbContext owns all command types in an assembly. A command type may be routed only once, and an explicit command route must not overlap an assembly route for the same command assembly.

New applications should use provider shortcuts plus route-free UseWriteDbContexts(...). When API-side command execution is enabled and no managed write DbContext is registered, AnchorFlow requires at least one explicit command route. Runtime-only hosts that do not accept commands should call DisableCommandExecution().

Projection DbContexts

EF-backed projections can register projection DbContexts through the same options delegate:

services.AddScoped<CustomerCardsProjection>();

services.AddAnchorFlow(options =>
{
  options.UseSqlite(writeConnectionString, projectionConnectionString);
  options.DisableCommandExecution();
  options.UseProjectionDbContexts(projection => projection.Add<CustomerCardsDbContext>());
  options.UseLocalTransport();
  options.RunWorker("projection-worker-01");
});

Register projection services before AddAnchorFlow(...); AnchorFlow discovers concrete IProjection<TEvent> implementations from the service collection during configuration. Projection DbContexts used for AnchorFlow-managed projection writes must be registered through AnchorFlow with UseProjectionDbContexts(...), not only with services.AddDbContext<TDbContext>().

AnchorFlow-managed projection DbContexts map read-model entities only. Projection checkpoints and notification outbox rows live in AnchorFlow-owned projection technical tables, not in normal application projection DbContext migrations.

AnchorFlow Technical Schema Creation

Application EF migrations are responsible for application write tables and read-model tables. AnchorFlow technical tables are created through the configured provider.

For local development, demos, tests, or simple hosted startup paths, opt in explicitly after selecting the provider and storage topology:

services.AddAnchorFlow(options =>
{
  options.UseSqlite(writeConnectionString, projectionConnectionString);
  options.EnsureWriteTablesCreated();
  options.EnsureProjectionTablesCreated();
  options.DisableCommandExecution();
  options.UseLocalTransport();
  options.RunWorker("projection-worker-01");
});

EnsureWriteTablesCreated() creates write-store technical tables such as idempotency, outbox, process state, locks, and dead letters during startup. EnsureProjectionTablesCreated() creates projection-store technical tables such as projection checkpoints, notification outbox, projection locks, and projection dead letters during startup. Use only the targets a host actually needs. A normal host that opts into these startup methods should not also resolve IAnchorFlowSchemaManager and call EnsureConfiguredStoresAsync(...) in the same startup path.

Production release or migration scripts can call the schema manager explicitly instead of creating schemas during every application startup:

await serviceProvider.GetRequiredService<IAnchorFlowSchemaManager>()
  .EnsureConfiguredStoresAsync(AnchorFlowSchemaTarget.Write | AnchorFlowSchemaTarget.Projection);

Low-level APIs such as modelBuilder.ApplyAnchorFlowFrameworkStorage() remain available for focused tests and existing prerelease paths, but they are not part of the normal application DbContext guidance.

Projection Transaction Boundary

For managed EF projections, AnchorFlow resolves projection services and any AnchorFlow-managed projection DbContexts from the projection execution scope after the current ProjectionStore is initialized. Projection handlers update read models and stage notifications through ProjectionContext; they do not call SaveChanges in the managed path.

The EF projection transaction commits resolved managed Projection DbContexts, projection checkpoint updates, and notification outbox records together. If projection execution fails before commit, those read model changes, completed checkpoint updates, and staged notifications roll back together. Projection handlers that mutate the same target state should share a projection service/ordering group; all AnchorFlow-managed projection DbContexts resolved during projection execution participate in the same managed transaction boundary. Repository-friendly command invocation does not broaden projection execution to projection DbContext participation.

When several projections handle the same event in the same scope, view notifications staged by any one projection are not delivery-ready just because that projection committed. AnchorFlow persists projection-event barrier metadata with the notification outbox row and releases the row only after every expected projection for that event and scope reaches a terminal successful checkpoint state. Terminal successful states are applied, duplicate/already-applied, and old-version/skipped. Running or failed projections keep the notification blocked until retry processing records a terminal successful state.

Workspace Map

Path Purpose
AnchorFlow.sln Main product and test solution.
examples/AnchorFlow.Examples.sln Runnable example applications. Examples must remain runnable without PostgreSQL and must not add example test projects.
src/AnchorFlow.Abstractions Stable public contracts: messaging, tenancy, runtime capability flags, and storage SPIs. It must not depend on EF Core, Wolverine, ASP.NET Core, SignalR, SQLite, PostgreSQL, or provider packages.
src/AnchorFlow.Core Framework-independent runtime foundations: envelopes, tenant context, invocation context, subscription fan-out, runtime shutdown gating, payload/batching guards, retention, dead-letter operations, health contracts, and configured store resolution.
src/AnchorFlow.EntityFrameworkCore EF Core command/consumer pipelines and generic framework-owned EF storage records/model mapping.
src/AnchorFlow.EntityFrameworkCore.Sqlite SQLite EF Core shortcut package for UseSqlite(connectionString) and UseSqlite(writeConnectionString, projectionConnectionString), binding provider selection, shared topology, and managed EF store defaults.
src/AnchorFlow.Sqlite SQLite implementation of the provider SPI for tests, runnable examples, demos, integration tests, and lightweight single-node deployments.
src/AnchorFlow.PostgreSql PostgreSQL implementation of the provider SPI for production-like and realistic local/dev technical storage, leases, SKIP LOCKED claiming, schema creation, and indexes.
src/AnchorFlow.Wolverine Optional Wolverine adapter for local transport, explicit API/Worker/NotificationDispatcher capability plans, dispatch, fan-out, outbox sweepers, worker lifecycle, notification dispatcher and retention workers, startup validation, runtime limits, and health.
src/AnchorFlow.Wolverine.RabbitMq Optional RabbitMQ adapter package on top of AnchorFlow.Wolverine.
src/AnchorFlow.Wolverine.AzureServiceBus Optional Azure Service Bus adapter package on top of AnchorFlow.Wolverine.
src/AnchorFlow.AspNetCore ASP.NET Core tenant resolution plus liveness/API readiness/Worker readiness/NotificationDispatcher readiness health check registration.
src/AnchorFlow.ProcessManager Custom typed process manager runtime, state persistence, expected replies, scheduled messages, and durable collections. It does not use Wolverine Saga APIs.
src/AnchorFlow.Projections Projection registration, work item creation, idempotent checkpointing, projection groups, transaction hooks, and notification staging.
src/AnchorFlow.ViewNotifications View-notification model, projection extension methods, notification outbox adapter, dispatcher, and app-owned delivery contracts.
src/AnchorFlow.Testing Fakes, clocks, deterministic IDs, command/consumer/projection/process harnesses, and provider contract tests.
tests/* Package-local xUnit test projects. Provider behavior is validated through shared provider contract tests.
examples/* Compile-tested and runnable sample applications for core workflows.
AgentPlans/AnchorFlow Baseline architecture contract, validation contract, story state, and guard evidence for planned architecture work.
AgentPlans/TenantStoreConfiguration Tenant-store configuration feature contract, story status, guard findings, validation ledger, and final audit evidence. Read it before changing tenant registry, store resolver, current-store, provider naming, provisioning, or related docs.
AgentPlans/* Feature-specific Goal/Guard plans. Treat the relevant plan package as the operating contract when work targets a planned feature.

Package Boundaries

Keep package references narrow. Applications should install only the packages needed at their boundary.

Package Main public surface
AnchorFlow.Abstractions ICommandBus, Command<TResult>, ICommand<TResult>, ICommandHandler<TCommand,TResult>, IQueryProcessor, IQuery<TResult>, IQueryHandler<TQuery,TResult>, IMessageHandler<TMessage>, AnchorFlowEvent<TEntity,TId>, AnchorFlowManyEvent<TEntity,TId>, AggregateRoot<TAggregateRoot,TId>, AggregateEvent<TAggregateRoot,TId>, IEmit<TAggregateRoot,TId,TEvent>, IAnchorFlowIntegrationEvent, IntegrationOrderingKey, MessageEnvelope, identifier value types, ITenantResolver, ITenantContextAccessor, AnchorFlowRuntimeCapabilities, TenantKey, IAnchorFlowTenantRegistry, IAnchorFlowStoreResolver, IAnchorFlowStorageProvider, provider store interfaces, table name/topology contracts, tenant provisioning contracts.
AnchorFlow.Core CommandBus, QueryProcessor, MessageEnvelopeFactory, CommandContext, ConsumerContext, core ProjectionContext/ProcessContext, AsyncLocalTenantContextAccessor, TenantContext, TenantGuard, ConfiguredAnchorFlowTenantRegistry, ConfiguredAnchorFlowStoreResolver, IAnchorFlowCurrentStore, AnchorFlowTenantProvisioner, AnchorFlowSubscriptionRegistry, AnchorFlowSubscriptionFanOutRouter, AnchorFlowSubscriptionWorkItemDispatcher, IAnchorFlowRuntimeGate, AnchorFlowRuntimeGate, AnchorFlowRuntimeGateApplicationLifetimeRegistration, AnchorFlowRetentionCleanupService, AnchorFlowDeadLetterOperations, AnchorFlowInvocationContextAccessor, payload and batching options.
AnchorFlow.EntityFrameworkCore AddAnchorFlow, AnchorFlowOptions, UseWriteStoreEfCore(...), UseWriteDbContexts(...), UseProjectionDbContexts(...), IAnchorFlowDbContextProvider<TDbContext>, AnchorFlowRepository<TDbContext>, AnchorFlowTenant, AnchorFlowTenantSource, route-based UseWriteDbContext<TDbContext>(...), existing-host AddAnchorFlow<TDbContext>, low-level AddAnchorFlowEntityFrameworkCommandBus<TDbContext>, EntityFrameworkCommandPipeline<TDbContext>, EntityFrameworkConsumerPipeline<TDbContext>, EntityFrameworkCommandInvoker<TDbContext>, EntityFrameworkConsumerInvoker<TDbContext>, AnchorFlowModelBuilderExtensions, EF storage record types.
AnchorFlow.EntityFrameworkCore.Sqlite UseSqlite(connectionString), UseSqlite(writeConnectionString, projectionConnectionString).
AnchorFlow.Sqlite AnchorFlowSqliteStorageProvider, AnchorFlowSqliteOptions, AnchorFlowSqliteTableNameResolver, AnchorFlowSqliteSchema.
AnchorFlow.PostgreSql AnchorFlowPostgreSqlStorageProvider, AnchorFlowPostgreSqlOptions, AnchorFlowPostgreSqlTableNameResolver, AnchorFlowPostgreSqlSchema, PostgreSQL lease support.
AnchorFlow.Wolverine AddAnchorFlowWolverine, AnchorFlowWolverineOptions, AnchorFlowWolverineRuntimePlan, AnchorFlowRuntimeLimiter, AnchorFlowWolverineOutboxDispatcher, AnchorFlowWolverineImmediateOutboxFlusher, AnchorFlowWolverineSubscriptionFanOutDispatcher, AnchorFlowWolverineWorkerRuntime, AnchorFlowWolverineWorkerServiceCatalog, AnchorFlowWolverineWorkerStartupValidator, AnchorFlowWolverineNotificationWorker, AnchorFlowWolverineRetentionWorker, AnchorFlowWorkerLifecycleAdapter, WolverineMessageBusTransport.
AnchorFlow.Wolverine.RabbitMq UseRabbitMq(...), AnchorFlowRabbitMqTransportAdapter, RabbitMQ topology descriptors.
AnchorFlow.Wolverine.AzureServiceBus UseAzureServiceBus(...), AnchorFlowAzureServiceBusTransportAdapter, Azure Service Bus topology descriptors.
AnchorFlow.AspNetCore ASP.NET Core tenant resolver and AnchorFlow health check registration.
AnchorFlow.ProcessManager ProcessManager<TState,TStartEvent>, ProcessManagerRegistry, ProcessManagerRuntime, ProcessStateStore<TState>, ProcessManagerOperations, Transition, ProcessContext, ProcessSendBuilder, DurableList<T>, DurableDictionary<TKey,TValue>, DurableBlob<T>.
AnchorFlow.Projections IProjection<TEvent>, discovered projection definitions, ProjectionProcessor, ProjectionWorkItemFactory, ProviderProjectionCheckpointStore, ProviderProjectionTransactionFactory, IProjectionTransactionFactory, IProjectionNotificationOutbox, projection result and checkpoint types.
AnchorFlow.ViewNotifications ViewNotification, ProjectionViewNotificationExtensions, AnchorFlowViewNotificationOutbox, AnchorFlowViewNotificationDispatcher, AnchorFlowViewNotificationDispatcherOptions, IAnchorFlowViewNotificationDelivery.
AnchorFlow.Testing CommandTestHarness, ConsumerTestHarness, ProjectionTestHarness, ProcessManagerTestHarness, fake tenant/user contexts, fake notification delivery, TestMessageClock, TestScheduler, AnchorFlowStorageProviderContractTests.

Core Runtime Model

AnchorFlow assumes writes enter through commands, async work moves through durable outbox/inbox records, projections maintain query/view state, view notifications are delivered only after projection changes are persisted, and process managers orchestrate long-running workflows through typed state plus expected replies.

Runtime shutdown and scale-in safety is centralized in IAnchorFlowRuntimeGate / AnchorFlowRuntimeGate. AnchorFlowRuntimeGateApplicationLifetimeRegistration connects host ApplicationStopping to the gate. Internal workers and durable claim paths must check the gate before creating new work claims, run with the gate stopping token, and leave shutdown cancellation retryable or reclaimable instead of recording it as terminal business failure.

Do not introduce event-database storage, historical event schema migration handlers, event-log-based projection rebuilds, framework-managed external HTTP orchestration, direct Wolverine saga usage, or app/process/import-specific framework tables unless the architecture is explicitly changed.

Tenant And Message Metadata

Tenant isolation is a hard invariant. Commands, events, process state, durable collections, projections, notifications, idempotency, outbox, inbox, retention, and dead letters are tenant-scoped unless explicitly system-level.

Important types:

  • TenantId, MessageId, CorrelationId, CausationId, ProcessId, SagaId, and IdempotencyKey are value contracts in AnchorFlow.Abstractions.Messaging.
  • MessageEnvelope carries kind, message type, tenant/system scope, correlation/causation, process/saga ids, idempotency key, timestamp, and headers.
  • MessageEnvelopeFactory creates command/event/reply envelopes and preserves causation metadata.
  • TenantContext, AsyncLocalTenantContextAccessor, and TenantGuard enforce ambient tenant behavior.
  • AnchorFlowInvocationContextAccessor exposes the active command, subscriber, projection, process manager, or notification invocation to downstream code.

When adding a pipeline path, always initialize or preserve envelope metadata and tenant context before invoking user code.

Ambient Tenant Context

AnchorFlow separates the application tenant value from command payloads. A command may carry its own TenantId property, but it does not have to when the application has already established an ambient tenant context for the current operation.

The ambient tenant is exposed through ITenantContextAccessor.Current. The default implementation, AsyncLocalTenantContextAccessor, is intentionally registered as a singleton by ASP.NET Core integration and stores the current tenant in AsyncLocal. The singleton instance is shared, but the Current value is scoped to the current asynchronous execution flow. Parallel ASP.NET Core requests can therefore use the same accessor instance and still see different tenant contexts.

Use BeginScope(...) when setting the tenant for a request or explicit operation so the previous value is restored:

using var tenantScope = tenantAccessor.BeginScope(
  TenantContext.ForTenant(new TenantId(applicationTenantId), "jwt"));

await next();

For applications that use Guid tenant ids, convert the application-specific id to the framework TenantId string value consistently:

var tenantId = new TenantId(applicationTenantGuid.ToString("D"));

Use one format consistently for the whole application, for example "D" for the usual dashed GUID text or "N" for compact technical ids.

ASP.NET Core applications normally resolve the application-specific tenant before publishing commands, for example in middleware that reads a JWT claim, validates it according to application rules, converts it to AnchorFlow.Abstractions.Messaging.TenantId, and sets ITenantContextAccessor.Current for the rest of the request. This middleware belongs to the host application because the framework cannot know the application's JWT schema, tenant claim names, authorization model, or tenant lookup rules.

When ICommandBus.PublishAsync(...) executes a command, the command tenant is resolved from CommandExecutionOptions.TenantId, a public command TenantId property, or the ambient ITenantContextAccessor.Current.TenantId. If both the command/options and the ambient context specify tenant values, they must match. If none is available, the command is treated as explicit system-level work.

Worker paths do not rely on an HTTP request tenant. Command execution persists tenant metadata on the command envelope and outbox rows. Worker-side outbox, consumer, subscription, projection, notification, and process-manager execution reconstruct the tenant from the persisted/brokered envelope metadata and set the ambient tenant context for the duration of that unit of work before user code runs. This is the reason command handlers and event subscribers can use the same tenant-aware services in API and Worker roles.

Tenant Store Configuration

Tenant store configuration separates the business tenant id from technical store routing. The runtime path is:

TenantId -> TenantRegistry -> StoreResolver -> WriteStore / ProjectionStore -> CurrentStoreContext -> DbContext/provider setup

Core contracts:

  • TenantKey is the validated technical key for store names, templates, provisioning, and tenant partitions. It is not the same thing as the business TenantId.
  • AnchorFlowTenantRegistryEntry registers one tenant with one WriteStore and one ProjectionStore.
  • AnchorFlowTenantStoreRegistration records store id, AnchorFlowStoreKind, topology, provider, optional database name, and connection source.
  • ConfiguredAnchorFlowStoreResolver resolves AnchorFlowStoreDescriptor instances for command/consumer/process WriteStore work and projection/notification ProjectionStore work.
  • IAnchorFlowCurrentStore is a scoped Microsoft DI accessor initialized by AnchorFlow invocation scopes before scoped DbContexts or runtime services are resolved.

Store descriptor semantics:

  • AnchorFlowStoreDescriptor.ConnectionInfo is the resolved provider-specific value used by DbContext/provider setup. Treat it as sensitive and do not print it in diagnostics.
  • AnchorFlowStoreDescriptor.TenantId identifies the business tenant for tenant-bound work. Shared worker enumeration may return an unbound shared descriptor with TenantId == null.
  • AnchorFlowStoreDescriptor.TenantPartitionKey is the value used for tenant-owned rows in a shared physical store. Registry-backed tenant descriptors use TenantKey.Value; legacy direct shared-store fallback uses the tenant id as the partition key.
  • EnumerateWriteStoresAsync and EnumerateProjectionStoresAsync are worker-facing APIs. SharedDatabase enumeration deduplicates shared physical stores; DatabasePerTenant enumeration emits per-tenant descriptors.

Supported topologies:

  • SharedDatabase: all tenants resolve to the same WriteStore and ProjectionStore descriptors. Tenant rows still carry tenant metadata.
  • DatabasePerTenant: each tenant resolves to its own WriteStore and ProjectionStore descriptors.

Connection-string strategies:

var resolver = new ConfiguredAnchorFlowStoreResolver(
  new AnchorFlowStoreResolverOptions()
    .AddConnectionString("TenantA:Write", "Data Source=tenant-a-write.db")
    .AddConnectionString("TenantA:Projection", "Data Source=tenant-a-projection.db")
    .AddTenantRegistration(
      new AnchorFlowTenantRegistryEntry(
        "tenant-a",
        new TenantKey("tenant_a"),
        new AnchorFlowTenantStoreRegistration(
          "tenant-a-write",
          AnchorFlowStoreKind.Write,
          AnchorFlowTenancyMode.DatabasePerTenant,
          "sqlite",
          new AnchorFlowStoreConnectionSource(
            AnchorFlowStoreConnectionSourceKind.ConnectionStringName,
            "TenantA:Write"),
          "tenant_a_write"),
        new AnchorFlowTenantStoreRegistration(
          "tenant-a-projection",
          AnchorFlowStoreKind.Projection,
          AnchorFlowTenancyMode.DatabasePerTenant,
          "sqlite",
          new AnchorFlowStoreConnectionSource(
            AnchorFlowStoreConnectionSourceKind.ConnectionStringName,
            "TenantA:Projection"),
          "tenant_a_projection"))));

Templates are also supported for controlled technical values:

new AnchorFlowStoreConnectionSource(
  AnchorFlowStoreConnectionSourceKind.ConnectionStringTemplate,
  "Data Source=stores/{WriteDatabaseName}.db");

Allowed placeholders are {TenantKey}, {WriteDatabaseName}, and {ProjectionDatabaseName}. Raw {TenantId} is rejected because it is business input and may not be safe for technical store names. Do not log resolved connection strings when they may contain secrets.

AnchorFlow-managed write DbContext setup is current-store-based and owned by AnchorFlow:

services.AddAnchorFlow(options =>
{
  options.UseSqlite(writeConnectionString, projectionConnectionString);
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.UseLocalTransport();
  options.RunApiAndWorker("local-dev-worker");
});

Handlers, projections, app-owned notification delivery services, and process managers must not receive connection strings directly. They should rely on the scoped DbContext or provider services resolved after AnchorFlow initializes the current store.

Runtime store binding:

  • EF command and consumer pipelines resolve and bind the tenant WriteStore before scoped handlers and DbContexts run.
  • Projection processing resolves and binds the tenant ProjectionStore before projection dependencies run.
  • View-notification dispatch enumerates ProjectionStores and validates tenant/store ownership before app-owned delivery code runs.
  • Wolverine subscription work and scoped outbox sweep work bind WriteStore context before scoped storage or transport services execute.
  • ProcessManager scoped transitions bind tenant context and WriteStore context before process managers, process state, durable collections, and process storage execute.
  • Store-kind or tenant mismatches fail before user handler, projection, notification, or process-manager code is invoked.

Provisioning is explicit. IAnchorFlowTenantProvisioner coordinates:

  1. Register or create WriteStore.
  2. Register or create ProjectionStore.
  3. Apply application WriteStore migrations.
  4. Apply application ProjectionStore migrations.
  5. Apply AnchorFlow WriteStore schema.
  6. Apply AnchorFlow ProjectionStore schema.
  7. Update the TenantRegistry.

Provisioning is not automatic on every request. Hosts can implement IAnchorFlowTenantStoreProvisioner, IAnchorFlowStoreMigrator, and IAnchorFlowTenantRegistryUpdater to integrate their own database creation, migration, and registry persistence.

The Core provisioner reports ordered step results and sanitized failure metadata. It does not create production databases, store secrets, or implement provider-specific provisioning engines by itself.

Testing guidance:

  • Use fake or configured IAnchorFlowTenantRegistry entries to verify SharedDatabase and DatabasePerTenant routing.
  • Test named connection-string and template resolution without exposing real secrets.
  • Use AddAnchorFlowInvocationContext() plus AnchorFlowScopedInvocationRunner to verify IAnchorFlowCurrentStore based DbContext configuration.
  • Use SQLite relational tests or examples for local store routing. Do not treat EF Core InMemory tests as proof of relational provider behavior.

Query Handling

Application read entry points can execute typed IQuery<TResult> requests through scoped IQueryProcessor.ProcessAsync(...). The processor is not a bus and does not create messages. It only initializes the current projection store when needed, resolves the typed IQueryHandler<TQuery,TResult> from DI, and calls HandleAsync(...).

Query handling has no command, outbox, inbox, idempotency, retry, broker, notification, envelope, transaction, or SaveChanges semantics. Query handlers are app-owned read services. They may inject projection DbContexts, read repositories, or other read-side services and should use normal read-only EF patterns such as AsNoTracking() where appropriate.

The public processor API only needs the query argument:

public sealed record GetCustomerDetails(string CustomerId) : IQuery<CustomerDetails>;

public sealed class GetCustomerDetailsHandler(CustomerReadDbContext dbContext)
  : IQueryHandler<GetCustomerDetails, CustomerDetails>
{
  public async ValueTask<CustomerDetails> HandleAsync(
    GetCustomerDetails query,
    CancellationToken cancellationToken = default)
  {
    return await dbContext.Customers
      .AsNoTracking()
      .Where(customer => customer.Id == query.CustomerId)
      .Select(customer => new CustomerDetails(customer.Id, customer.Name))
      .SingleAsync(cancellationToken);
  }
}
var details = await queryProcessor.ProcessAsync(new GetCustomerDetails(customerId), cancellationToken);

The processor caches the concrete query-type dispatch plan after first use. Reflection is not used per query execution, and query results, handlers, DbContexts, stores, and application data are not cached.

For AnchorFlow-owned projection DbContexts, the processor resolves and initializes the projection store before resolving the handler. In shared-database and database-per-tenant setups, the ambient ITenantContextAccessor.Current must contain a tenant unless the current scope already has an initialized projection store.

Command Pipeline

Application entry points should publish Command<TResult> instances through scoped ICommandBus.PublishAsync(...). The bus delegates to the configured command executor, and EF-backed applications normally register one or more AnchorFlow-managed write DbContexts through provider shortcuts plus UseWriteDbContexts(...). The existing-host AddAnchorFlow<TDbContext>(...) overload and the lower-level AddAnchorFlowEntityFrameworkCommandBus<TDbContext>() still live in src/AnchorFlow.EntityFrameworkCore/Messaging for runtime internals, custom setup, and focused tests; they are not the beginner path.

Expected command flow:

  1. Resolve ambient tenant from ITenantContextAccessor.
  2. Resolve command tenant from CommandExecutionOptions.TenantId, a command TenantId property, or system-level scope.
  3. Resolve idempotency key from CommandExecutionOptions.IdempotencyKey, command IdempotencyKey, or command CommandId.
  4. Create a MessageEnvelope through MessageEnvelopeFactory.
  5. Verify tenant consistency through TenantGuard.
  6. Resolve the write store through IAnchorFlowStoreResolver.
  7. Check existing AnchorFlowIdempotencyRecord by (IdempotencyPartition, IdempotencyKey).
  8. Try to insert a started idempotency record; a duplicate start race is re-read and mapped to public duplicate semantics.
  9. Begin the EF transaction owned by the pipeline.
  10. Invoke ICommandHandler<TCommand,TResult> with CommandContext.
  11. Save participating application DbContext changes.
  12. Convert context.Emit(...) and context.EmitIntegration(...) messages into AnchorFlowOutboxMessageRecord rows.
  13. Persist completed idempotency result.
  14. Commit.
  15. Run bounded immediate outbox flush through IAnchorFlowImmediateOutboxFlusher if registered.

Command retry:

  • DbUpdateConcurrencyException raised during AnchorFlow-owned command saving is retried according to ConfigureCommandExecution(...).OptimisticConcurrency.
  • Non-concurrency DbUpdateException failures are retried only when ConfigureCommandExecution(...).Retry.TransientFailureClassifier returns AnchorFlowRetryDecision.Retry().
  • Command retry delays use the shared ConfigureRetryDelays(...) profile; exponential mode retries once immediately, then grows from BaseDelay to MaxDelay.
  • The failed attempt's scope, DbContext instances, transaction, idempotency work, and staged outbox work are discarded; the next attempt creates a fresh scope and reruns HandleAsync.
  • HandleAsync can run more than once for one published command. Handlers must be retry-safe and must not perform irreversible external side effects before the command commit.
  • Only the successful attempt commits idempotency and outbox rows. AnchorFlow outbox and idempotency records commit once for the successful attempt, but handler code itself may have been invoked more than once.
  • Deterministic programming, mapping, schema, cancellation, and business-result failures are not command-retried. Exhausted retry attempts rethrow the final EF Core update exception.
  • The route-free command bus and route-based command-bus paths create fresh retry scopes. Direct caller-owned EntityFrameworkCommandPipeline<TDbContext>.ExecuteAsync(command, handler, ...) invocations run one attempt because the caller owns the handler and DbContext instances.

Handler rules:

  • A command handler is the unit-of-work boundary.
  • Handler code may use EF Core directly, app repositories, or app services.
  • Handler code must not open/commit/rollback transactions.
  • Handler code must not call SaveChanges, brokers, transport delivery, SignalR delivery, or outbox tables directly.
  • Handler code emits events only through ICommandContext.Emit(...) or EmitIntegration(...).
  • Actor/user metadata from CommandExecutionOptions.ActorId and CommandExecutionOptions.UserId is copied into command headers and preserved through outbox/consumer paths.

Duplicate command behavior:

  • Same tenant/system partition, same idempotency key, same command type, and same request hash returns the cached result.
  • Same idempotency key with a different request hash throws CommandIdempotencyConflictException.
  • An incomplete matching record, including a concurrent duplicate start while the first request is still running, throws CommandAlreadyInProgressException.

Consumer Pipeline

The EF consumer pipeline lives in src/AnchorFlow.EntityFrameworkCore/Messaging/EntityFrameworkConsumerPipeline.cs.

Expected consumer flow:

  1. Receive an AnchorFlowConsumerMessage from transport or framework dispatch.
  2. Recreate a MessageEnvelope from the persisted/brokered metadata.
  3. Validate or establish ambient tenant context.
  4. Check inbox receipt by (MessageId, ConsumerName).
  5. Begin the EF transaction owned by the pipeline.
  6. Try-start or restart a failed inbox receipt.
  7. Deserialize payload after payload guard checks.
  8. Invoke IMessageHandler<TMessage> with ConsumerContext.
  9. Save application changes.
  10. Stage outgoing outbox records from context.Emit(...).
  11. Complete inbox receipt.
  12. Commit.
  13. Flush committed outbox records if immediate flusher is registered.

Failure behavior:

  • Retry decisions come from IAnchorFlowConsumerRetryPolicy; default is MaxAttemptConsumerRetryPolicy(3).
  • Retryable failures persist failed receipt metadata and return RetryScheduled without ack.
  • Permanent failures create an AnchorFlowDeadLetterRecord, mark the inbox receipt dead-lettered, and return an ackable result.

Events And Affected Resources

Business events must derive from AnchorFlowEvent<TEntity,TId> for one primary resource or AnchorFlowManyEvent<TEntity,TId> for several primary resources. Application events should not implement IAnchorFlowEvent directly, and plain records are not valid business events. ICommandContext.Emit(...) and IConsumerContext.Emit(...) accept only strict business event contracts.

Use the event base constructors for the primary affected resource or resources. Add secondary resources by passing explicit affected-resource callbacks to Emit(...); those resources are additive and do not replace the primary resource metadata from the event base class. Do not depend on property-name conventions such as CustomerId or OrderId to make a plain record routable.

Integration events are separate from business events. They implement IAnchorFlowIntegrationEvent, are emitted with EmitIntegration(...), and provide a deterministic IntegrationOrderingKey through the OrderingKey property. Integration ordering is per key, not global. AnchorFlow persists and forwards the key through outbox, provider, subscription-work-item, and Wolverine metadata; RabbitMQ and Azure Service Bus broker-level ordering/session enforcement is not added by AnchorFlow.

Legacy Publish(...) wrappers remain for existing prerelease callers and are not the target API. When migrating prerelease application code, change emitted business-event records to derive from AnchorFlowEvent<TEntity,TId> or AnchorFlowManyEvent<TEntity,TId>, move cross-boundary notifications to IAnchorFlowIntegrationEvent, and choose stable ordering keys such as customer:{id} or order:{id}.

Affected-resource metadata is persisted on outbox rows and travels with committed outbox batches, broker messages, subscription work items, projections, and notifications. Business events are persisted with OutboxSequence, the technical write-store ordering value also exposed as StoreEventSequence on subscriber work. EntityVersion, ResourceVersion, and AffectedResourceVersions are optional domain/projection-safety values and are not required for event ordering.

Business-event source ordering is per affected-resource lane within the tenant or system scope, not global. Later outbox messages that overlap an earlier undispatched affected-resource lane are not claimable until the earlier source message is dispatched. Independent resources can still dispatch in the same wave, so CRUD and DDD command handlers may emit several events without forcing global serialization across unrelated entities.

When a source business event has subscribers, AnchorFlow creates durable subscription work items before marking that source outbox message dispatched. Subscriber work item lanes then serialize execution by OrderingSubscriptionId + AffectedResourceKey + StoreEventSequence after the work item rows exist. This means a subscriber command chain for a later same-resource event cannot start by bypassing the earlier event's durable fan-out barrier.

Startup DbContext.Model/IModel analysis may be used only for entity/resource name normalization, key-type recognition, convention validation, and early developer-error detection. It must not query application rows or infer affected resources from current database state.

Event payloads are serialized with System.Text.Json using web defaults. Keep event contracts DTO-like: public immutable records or classes, stable property names, no behavior-dependent serialization, and no framework/runtime-only objects in payloads. Plain scalar properties, nested concrete records/classes, List<T>, IReadOnlyList<T>, arrays, and lists such as List<string> do not need custom converters when T is concrete and serializable.

Polymorphic event members need an explicit contract. If an event contains a property such as IReadOnlyList<BaseChange> where items may be different derived types, register the known derived types with System.Text.Json polymorphism metadata, for example [JsonPolymorphic(TypeDiscriminatorPropertyName = "$kind")] plus [JsonDerivedType(typeof(AddressChanged), "address-changed")] on the base type. Use a custom JsonConverter only when the hierarchy is open/plugin-based, the discriminator must match legacy JSON, the base type cannot be annotated, or type selection depends on existing property combinations. Do not rely on Newtonsoft.Json-style automatic type-name handling.

Custom converters are also required for event members that System.Text.Json cannot represent by default: value objects without a supported constructor or converter, non-string dictionary keys that are not built in, external/third-party types with private state only, interface-typed properties without polymorphism metadata, and unsupported runtime types such as System.Type, delegates, Span<T>/ref struct values, DataTable, or DataSet. Prefer changing the event contract to a small explicit DTO before adding a converter.

Do not emit very large affected-resource lists. If an operation exceeds MaxAffectedResourceKeysPerEvent, model it as an explicit LargeBatch flow: use a ProcessManager, durable collection, database table, blob/object storage, or several bounded commands/events instead of one huge payload.

Storage SPI And Topologies

IAnchorFlowStorageProvider coordinates all provider-owned technical stores:

  • Idempotency
  • Outbox
  • Inbox
  • Processes
  • DurableCollections
  • Notifications
  • ProjectionCheckpoints
  • DeadLetters
  • Retention
  • UnitOfWork

AnchorFlowStoreKind separates Write and Projection stores. ConfiguredAnchorFlowStoreResolver resolves stores from AnchorFlowStoreDescriptor values:

  • SharedDatabase: all tenants share one write store and one projection store. Rows still carry tenant metadata.
  • DatabasePerTenant: each tenant has separate write/projection stores. Rows still carry tenant metadata.

Write store tables contain command idempotency, outbox, inbox, process instances/state/expectations/messages, durable collections, process dead letters/locks, and write-side retention markers.

Projection store tables contain projection checkpoints, notification outbox, projection dead letters/locks, and projection-side retention markers. Projection stores intentionally exclude write/process/durable tables.

Provider table naming:

  • SQLite uses fixed physical names like AnchorFlow_Outbox, AnchorFlow_Inbox, AnchorFlow_Idempotency, AnchorFlow_NotificationOutbox, and AnchorFlow_ProjectionCheckpoints.
  • PostgreSQL uses the "AnchorFlow" schema with physical names like "AnchorFlow"."Outbox", "AnchorFlow"."Inbox", "AnchorFlow"."Idempotency", "AnchorFlow"."NotificationOutbox", and "AnchorFlow"."ProjectionCheckpoints".
  • Older prerelease names such as __framework_*, OutboxMessages, InboxMessages, NotificationOutboxMessages, and CommandIdempotency are obsolete. Existing prerelease databases need explicit migration or recreation.
  • Prerelease databases created before resource-aware command/event emission need migration or recreation for the outbox OutboxSequence, AffectedResourcesJson, and AffectedResourceVersionsJson columns and their provider indexes. The sequence is local to the physical write store; database-per-tenant deployments have one sequence per tenant write database.
  • OutboxSequence is generated by the database (ValueGeneratedOnAdd, SQLite autoincrement, or PostgreSQL identity). Providers must not synthesize write ordering with MAX() + 1.
  • Business-event source ordering uses the framework-owned outbox lane table keyed by tenant/system scope, affected-resource lane, and OutboxSequence. Provider claims use that table for set-based blockers instead of parsing affected-resource JSON on the hot claim path.
  • Durable subscription work items carry status, worker ownership, lease expiry, attempt counts, retry scheduling, last error text, timestamps, the original event payload needed for recovery, and a concurrency token. Worker runtimes recover committed pending rows and due retry-scheduled rows directly from provider storage without depending on source-event or transport-work-item redelivery. running work may be reclaimed after LockedUntilUtc; retry-scheduled work is claimable only after NextAttemptAtUtc; dead-lettered work is final for its own item and intentionally releases ordering blockers after max retries so later overlapping work may proceed. Dashboard/API management for replay, requeue, ignore, or manual resolution is deferred.
  • Prerelease databases created before subscription work item recovery metadata was persisted can contain pending or retry-scheduled rows without lane, payload, headers, correlation, causation, process/saga, idempotency, affected-resource, and ordering metadata. Those rows cannot be reconstructed by Worker recovery claims. Drain them through the old transport path, replay or recreate the source events, or run an explicit prerelease migration that repopulates the metadata before relying on subscription work item recovery.
  • Prerelease databases created before tenant-scoped projection checkpoint identity need migration or recreation for ProjectionCheckpoints. The checkpoint primary key is now ScopeKey + ProjectionName + EventId, where ScopeKey represents tenant scope or system scope. Existing technical tables keyed only by ProjectionName + EventId must be rebuilt or explicitly migrated before sharing one projection store across tenants with colliding projection/event ids.

Provider table map:

Store Logical table SQLite physical table PostgreSQL physical table
Write Idempotency AnchorFlow_Idempotency "AnchorFlow"."Idempotency"
Write Outbox AnchorFlow_Outbox "AnchorFlow"."Outbox"
Write OutboxMessageLanes AnchorFlow_OutboxMessageLanes "AnchorFlow"."OutboxMessageLanes"
Write Inbox AnchorFlow_Inbox "AnchorFlow"."Inbox"
Write ProcessInstances AnchorFlow_ProcessInstances "AnchorFlow"."ProcessInstances"
Write ProcessStateChunks AnchorFlow_ProcessStateChunks "AnchorFlow"."ProcessStateChunks"
Write ProcessMessages AnchorFlow_ProcessMessages "AnchorFlow"."ProcessMessages"
Write ProcessExpectations AnchorFlow_ProcessExpectations "AnchorFlow"."ProcessExpectations"
Write ProcessLocks AnchorFlow_ProcessLocks "AnchorFlow"."ProcessLocks"
Write SubscriptionWorkItems AnchorFlow_SubscriptionWorkItems "AnchorFlow"."SubscriptionWorkItems"
Write SubscriptionWorkItemLanes AnchorFlow_SubscriptionWorkItemLanes "AnchorFlow"."SubscriptionWorkItemLanes"
Write ProcessDeadLetters AnchorFlow_ProcessDeadLetters "AnchorFlow"."ProcessDeadLetters"
Write DurableStateCollections AnchorFlow_DurableStateCollections "AnchorFlow"."DurableStateCollections"
Write DurableStateCollectionChunks AnchorFlow_DurableStateCollectionChunks "AnchorFlow"."DurableStateCollectionChunks"
Write RetentionMarkers AnchorFlow_RetentionMarkers "AnchorFlow"."RetentionMarkers"
Projection NotificationOutbox AnchorFlow_NotificationOutbox "AnchorFlow"."NotificationOutbox"
Projection ProjectionCheckpoints AnchorFlow_ProjectionCheckpoints "AnchorFlow"."ProjectionCheckpoints"
Projection ProjectionLocks AnchorFlow_ProjectionLocks "AnchorFlow"."ProjectionLocks"
Projection ProjectionDeadLetters AnchorFlow_ProjectionDeadLetters "AnchorFlow"."ProjectionDeadLetters"
Projection ProjectionRetentionMarkers AnchorFlow_ProjectionRetentionMarkers "AnchorFlow"."ProjectionRetentionMarkers"

Provider implementation notes:

  • SQLite is for tests, runnable examples, demos, and lightweight single-node scenarios. It uses optimistic row-token claiming and SQLite writer serialization; it is not the high-concurrency production target and should not be treated as the default for realistic local product development.
  • PostgreSQL is the production-like provider and the preferred provider for realistic local/dev database work. It uses PostgreSQL 15+ assumptions, FOR UPDATE SKIP LOCKED queue claiming, timestamptz, schema-level table creation, provider-specific indexes, and row-lock based leases.
  • Provider SQL must be parameterized. Static table names from validated provider metadata are acceptable.
  • Provider behavior must pass AnchorFlowStorageProviderContractTests.
  • Outbox, notification, projection, subscription-work, and process/projection lease claims must include worker identity, lock duration, and concurrency-token checks. Provider lease storage uses the write-side ProcessLocks table or projection-side ProjectionLocks table. A crash after publish/send but before mark-dispatched can redeliver; downstream handlers and domain constraints must remain idempotent.

Subscriber Ordering

One emitted event may fan out into multiple subscription work items. Dedupe identity is SubscriptionId + EventId within the tenant/system/store partition. Ordering identity is OrderingSubscriptionId + AffectedResourceKey + StoreEventSequence.

  • Different subscribers may run in parallel, even for the same event.
  • The same subscriber and same affected resource run serially by sequence.
  • The same subscriber and non-overlapping affected resources may run in parallel.
  • Work with several affected resources cannot start while earlier pending/running work for the same ordering subscription overlaps any of those resources.
  • Source business events are also claimed by affected-resource lane before fan-out, so later overlapping source events do not create subscriber work before earlier source events have completed durable fan-out and source dispatch marking.
  • Claimable durable states are pending, due retry-scheduled, and expired running. completed and dead-lettered are final for their item; earlier pending, retry-scheduled, or running overlapping work keeps later ordered work blocked, while dead-lettered earlier work is treated as terminal and non-blocking.
  • SubscriptionGroup and ProjectionGroup are the opt-in way to make multiple handlers that mutate the same target state share one ordering identity. Independent handlers should keep independent subscription ids.

Projections

Projection code lives in src/AnchorFlow.Projections.

Developer-facing pieces:

  • Implement IProjection<TEvent>.
  • Register projection service types with DI before AddAnchorFlow(...); AnchorFlow auto-discovers concrete IProjection<TEvent> implementations.
  • Keep projections that mutate the same target state in one projection service when they must share ordering. AnchorFlow discovers each concrete projection service as its own ordering group.
  • Register EF read-model DbContexts with UseProjectionDbContexts(projection => projection.Add<TDbContext>()) when projection handlers write through EF Core.
  • Use ProjectionProcessor to process one event envelope into one or more projection work items.
  • Use ProjectionContext to access envelope metadata, projection name, ordering key, entity id/version, prior progress, transaction hook, and staged notifications.
  • Mark EF read models with [AnchorFlowView] metadata when normal projection changes should automatically stage view notifications.
  • Use ProjectionViewNotificationExtensions.AddViewNotification(...) only for explicit advanced or low-level notifications that are not backed by a marked read-model change.

Processing model:

  1. ProjectionWorkItemFactory creates work items from event identity, discovered projection definitions, grouping, tenant, and ordering metadata.
  2. ProjectionProcessor asks IProjectionCheckpointStore.TryBeginAsync(...) whether a work item is started/running, duplicate, or old-version.
  3. Started items run inside an IProjectionTransaction from IProjectionTransactionFactory.
  4. Projection handlers mutate projection state and stage notifications. They must not call SaveChanges.
  5. DbUpdateConcurrencyException failures raised while committing managed EF projections are retried according to ConfigureProjectionExecution(...).OptimisticConcurrency.
  6. Non-concurrency DbUpdateException projection failures are retried only when ConfigureProjectionExecution(...).Retry.TransientFailureClassifier returns AnchorFlowRetryDecision.Retry().
  7. Projection retry delays use the same shared ConfigureRetryDelays(...) profile as command retries.
  8. Checkpoint completion and notification enqueue happen before transaction commit.
  9. Duplicate, already-running, or old-version events are skipped idempotently.
  10. Failures are recorded through checkpoint failure handling.

Provider-backed projection infrastructure:

  • ProviderProjectionCheckpointStore adapts IAnchorFlowProjectionCheckpointStore to IProjectionCheckpointStore and requires an explicit nonblank WorkerId plus a positive lease duration.
  • Provider-backed checkpoints persist worker ownership, LockedUntilUtc, attempt/retry metadata, and a concurrency token. Completion and failure recording require the matching claim.
  • Projection checkpoint identity is scoped by tenant/system scope in shared stores, so the same projection and event id can be processed independently for tenant A, tenant B, and system-level work.
  • ProviderProjectionTransactionFactory lets projection data, checkpoint completion, and notification outbox work share provider transaction boundaries where the application supplies a real shared transaction.
  • Strict atomicity between application projection tables and framework checkpoint/notification tables requires the supplied transaction factory to use the same database transaction.
  • UseProjectionDbContexts(...) installs EF-backed checkpoint, notification outbox, and transaction adapters and enlists resolved managed Projection DbContexts in the projection transaction.
  • EF projection DbContexts map read-model entities only in the normal path. Projection checkpoints and notification outbox rows are AnchorFlow-owned technical tables created by the provider schema manager.

Removed prerelease projection APIs:

  • Applications no longer register projections with AddProjections(...) or direct ProjectionRegistry mutation.
  • Applications no longer route projection DbContexts with UseProjectionDbContext<TDbContext>(...) plus HandlesProjection(...).
  • Application code registers projection services before AddAnchorFlow(...) and registers EF read-model DbContexts with route-free UseProjectionDbContexts(...).

View Notifications

View-notification dispatch lives in src/AnchorFlow.ViewNotifications.

Contract:

  • API/UI initial state is queried from projections.
  • EF projection handlers normally update marked read models only; AnchorFlow stages view notifications from those read-model changes.
  • Manual AddViewNotification(...) remains available for explicit advanced or low-level notifications that are not represented by a marked EF read model.
  • Notifications are persisted to IAnchorFlowNotificationOutboxStore through AnchorFlowViewNotificationOutbox.
  • Projection-staged notifications use provider-backed notification outbox readiness. There is no UI wait, UI polling, client-side delay, or SignalR delivery workaround in the readiness guarantee.
  • When multiple projections handle the causing event, notifications staged by any projection remain in the notification outbox until all expected projections for the same event and scope are terminal successful: applied, duplicate/already-applied, or old-version/skipped. Running or failed projection checkpoints block delivery until retry/success records one of those terminal successful states.
  • AnchorFlowViewNotificationDispatcher claims only barrier-ready notification outbox rows, invokes the registered IAnchorFlowViewNotificationDelivery, and marks rows dispatched.
  • Wolverine passes its configured runtime WorkerId to view-notification dispatch as the provider claim owner. Claims use worker ownership, LockedUntilUtc, attempt counts, retry/dead-letter state, and concurrency tokens.
  • A delivered view notification means all AnchorFlow-expected projection writes for the causing event and scope have been persisted and the notification outbox row was provider-ready for dispatch, subject to normal at-least-once duplicate delivery semantics.
  • View-notification delivery is at-least-once. If app-owned delivery succeeds but marking the outbox row dispatched fails or the process stops, the same durable notification can be delivered again.
  • App-owned IAnchorFlowViewNotificationDelivery implementations must make external effects idempotent and duplicate-safe. Use AnchorFlowViewNotificationDeliveryContext.NotificationId as the primary durable dedupe key, or derive an app key from the context's RecipientKey, tenant/system scope, store id, and ViewNotification event/view/resource/operation/sequence fields.
  • Failed notification dispatches are retried until MaxDeliveryAttempts, then provider storage records the deterministic notification:<NotificationId> dead letter and terminal notification mark together so a retry after a partial failure can finish the terminal mark without duplicate-key poisoning.
  • Handlers and projections must not invoke app transport delivery directly.

Automatic read-model view notifications:

[AnchorFlowView("trains")]
public sealed class TrainReadModel
{
  [AnchorFlowViewKey]
  public string TrainId { get; set; } = "";

  [AnchorFlowViewSequence]
  public long Version { get; set; }

  public string Name { get; set; } = "";

  public int VehicleCount { get; set; }

  [AnchorFlowViewIgnore]
  public string? InternalComment { get; set; }
}

public sealed class TrainProjection(TrainReadDbContext db)
  : IProjection<VehicleRemovedFromTrain>
{
  public async ValueTask ProjectAsync(
    VehicleRemovedFromTrain @event,
    ProjectionContext context,
    CancellationToken cancellationToken = default)
  {
    var train = await db.Trains.SingleAsync(
      item => item.TrainId == @event.TrainId,
      cancellationToken);

    train.Version = @event.Version;
    train.VehicleCount--;
  }
}

For marked read models, added rows stage an upsert, modified rows stage a patch, and deleted rows stage a remove. Payloads come from changed read-model entries in EF Core's ChangeTracker before projection commit; AnchorFlow does not reload the database in the dispatcher and does not serialize the original domain event as the standard UI payload. By default, mapped scalar public properties are included, [AnchorFlowViewIgnore] properties are excluded, and navigations are not included.

Use a read-model-specific mapper when one view needs a special UI-safe shape:

public sealed class TrainViewModelMapper
  : IAnchorFlowViewModelMapper<TrainReadModel>
{
  public object Map(TrainReadModel readModel, AnchorFlowViewModelMappingContext context)
  {
    return new
    {
      Id = readModel.TrainId,
      readModel.Name,
      readModel.VehicleCount,
      context.Sequence
    };
  }
}

Register the mapper as IAnchorFlowViewModelMapper<TrainReadModel>. Mappers are keyed by read-model type, not by event type, and they receive event metadata through AnchorFlowViewModelMappingContext only for dedupe, audit, and shaping decisions. They are not event-specific switch points.

Applications own delivery targets. IAnchorFlowViewNotificationDelivery receives a ready ViewNotification; the application resolves SignalR groups, users, connection ids, Redis channels, or gateway topics from tenant id, view, scope, user permissions, and application subscription state:

public sealed class AppViewNotificationDelivery : IAnchorFlowViewNotificationDelivery
{
  public Task DeliverAsync(
    AnchorFlowViewNotificationDeliveryContext context,
    CancellationToken cancellationToken = default)
  {
    var group = ResolveGroup(
      context.Notification.TenantId,
      context.Notification.View,
      context.Notification.Scope);

    return SendToGroupAsync(group, context.Notification, cancellationToken);
  }
}

Important classes:

  • ViewNotification describes view, scope, operation, sequence, event metadata, and payload.
  • ViewNotificationOperations defines operation names such as insert/update/upsert/remove/replace/refresh.
  • AnchorFlowViewNotificationConventions defines the channel and payload type expected by the dispatcher.
  • AnchorFlowViewAttribute, AnchorFlowViewKeyAttribute, AnchorFlowViewSequenceAttribute, and AnchorFlowViewIgnoreAttribute opt EF read models into automatic notification payloads.
  • IAnchorFlowViewModelMapper<TReadModel> maps one marked read-model type to a custom UI payload.
  • Applications own transport-specific delivery, including any SignalR, Redis, Azure or gateway integration.
  • RecordingViewNotificationDelivery in examples is a test/demo delivery service.

Process Manager

Process manager code lives in src/AnchorFlow.ProcessManager.

Developer model:

public sealed class ImportProcess
  : ProcessManager<ImportProcessState, ImportStarted>
{
  public override ProcessId GetProcessId(ImportStarted startEvent)
  {
    return new ProcessId(startEvent.ImportId);
  }

  public ValueTask<Transition> When(
    ImportStarted startEvent,
    ImportProcessState state,
    ProcessContext ctx)
  {
    state.Status = "running";
    state.ProgressId = startEvent.ProgressId;

    foreach (var typeRun in startEvent.TypeRuns)
    {
      ctx.PublishCommand(new ImportTypeRunCommand(
          startEvent.ImportId,
          startEvent.Tenant,
          typeRun.TypeRunKey.Value,
          typeRun.TypeRunKey.Value + "-chunk-001",
          typeRun.ItemCount))
        .WithExpectationKey(new AnchorFlowProcessExpectationKey(typeRun.TypeRunKey.Value))
        .Expect<ImportTypeRunResult>();
    }

    return ValueTask.FromResult(ctx.Transition);
  }

  public ValueTask<Transition> When(
    ImportTypeRunResult reply,
    ImportProcessState state,
    ProcessContext ctx)
  {
    state.SucceededCount += reply.SucceededCount;
    state.RejectedCount += reply.RejectedCount;

    ctx.Progress.Report(
      new ProgressId(state.ProgressId),
      new AnchorFlowProgressSectionKey(reply.TypeRunKey),
      reply.SucceededCount,
      reply.RejectedCount,
      processed: reply.ProcessedCount);

    return ValueTask.FromResult(ctx.Transition);
  }
}

Rules:

  • A process starts from a dedicated start event. The start event does not need a marker interface, but it must not be a command.
  • GetProcessId(TStartEvent) must be deterministic.
  • When(...) methods mutate typed state and schedule commands/timeouts. They do not do business work directly.
  • ctx.PublishCommand(command).Expect<TReply>() records one process-owned command and one expected reply using the configured default expected-reply timeout. Use it for workflow-relevant process-owned commands whose business result should advance process state. This is separate from normal application command publishing through ICommandBus.PublishAsync(...).
  • .WithTimeout(...) overrides the default timeout for one expected reply. .WithoutTimeout() intentionally opts that one command out of timeout feedback.
  • A transition may publish several process-owned commands. Give each command a distinct ProcessCommandId and, when the app needs item/chunk correlation, an app-owned AnchorFlowProcessExpectationKey through WithExpectationKey(...).
  • Business success or business failure comes from the expected typed reply/result. Technical failure feedback arrives as ProcessCommandFailed; timeout feedback arrives as ProcessCommandTimedOut.
  • Expect<TReply>() plus WithExpectationKey(...) is best practice for grouped or parallel process-owned commands that affect workflow state. It is not mandatory framework enforcement: FireAndForget() remains appropriate when command completion does not affect the process.
  • Multiple process-owned commands are not one database transaction. Model all-or-none business workflows as sagas: track terminal state per command in process state, verify unknown commit outcomes when necessary, and publish compensation commands only for steps that committed successfully.
  • Progress reported through ctx.Progress is committed atomically with process state, durable collections, process commands, expectations, outbox rows, and inbox completion.
  • ctx.Send.Command(command) and Send.Command(command) remain legacy wrappers for the process-owned publish-command API.
  • Replies route only when ProcessId, SagaId, process headers, message type, and expected step match.
  • Process transitions are sequential per process instance.
  • Duplicate and unexpected messages are handled through inbox/process records and ProcessUnexpectedMessagePolicy.

Runtime model:

  • ProcessManagerRegistry validates registrations, start event ownership, start When(...), reply When(...), and expected reply methods.
  • ProcessManagerRuntime routes starts/replies/timeouts, acquires process-local concurrency protection, acquires provider leases when enabled, starts inbox records, loads typed state, invokes When(...), writes state chunks, persists outbox messages and expectations, completes inbox records, and commits provider unit of work.
  • ProcessManagerRuntimeOptions.DefaultExpectedReplyTimeout defaults to 30 minutes and must be positive. Normal AddAnchorFlow(...) worker setup exposes the same value at worker.ProcessManagers.DefaultExpectedReplyTimeout.
  • ProcessManagerRuntimeOptions.UseLeases defaults to true. Worker runtimes must provide a stable WorkerId; no-lease execution is only for explicit local/test single-worker setups that also allow running without leases.
  • ProcessStateStore<TState> can execute manual state transitions with typed state plus durable collection bindings; it persists manual expectations without scheduling delayed timeout feedback.
  • ProcessManagerOperations exposes operational state changes such as MarkRunningAsync; ContinueAsync is status-only.

ProcessManager worker timeout defaults can be configured with the normal role options:

options.RunWorker(worker =>
{
  worker.Id = workerId;
  worker.ProcessManagers.DefaultExpectedReplyTimeout = TimeSpan.FromMinutes(10);
});

Durable state:

  • One typed state exists per process instance.
  • State is persisted as generic JSON chunks in framework-owned process state storage.
  • Large data should live in DurableList<T>, DurableDictionary<TKey,TValue>, or DurableBlob<T>, backed by generic durable collection tables.
  • Do not create process/import-specific framework tables.

Runtime Roles And Transport

The normal runtime and transport setup is configured through AddAnchorFlow(...) options:

services.AddAnchorFlow(options =>
{
  options.UsePostgreSql();
  options.UseSharedDatabase(writeConnectionString, projectionConnectionString);
  options.UseWriteStoreEfCore(write =>
  {
    write.UseConnection(
      (serviceProvider, store) => new NpgsqlConnection(store.ConnectionInfo),
      (serviceProvider, connection, builder) =>
        builder.UseNpgsql((NpgsqlConnection)connection));
  });
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.UseRabbitMq(rabbitMqConnectionString);
  options.RunWorker("orders-worker-01");
});

Local developer hosts may run API, Worker, and NotificationDispatcher behavior in one process while still using the same real store and tenant configuration that separated roles use:

services.AddAnchorFlow(options =>
{
  options.UseSqlite(writeConnectionString, projectionConnectionString);
  options.UseWriteDbContexts(write => write.Add<AppDbContext>());
  options.UseLocalTransport();
  options.RunApiWorkerAndNotificationDispatcher(runtime =>
  {
    runtime.Worker.Id = "orders-local-dev";
    runtime.Worker.MessagePublishing.MaxConcurrent = 1;
    runtime.Worker.AsyncProcessing.MaxConcurrent = 4;
    runtime.NotificationDispatcher.Id = "orders-local-dev";
    runtime.NotificationDispatcher.NotificationDelivery.MaxConcurrent = 4;
  });
});

For realistic local development, bind application DbContexts and AnchorFlow providers to real local/dev databases, preferably PostgreSQL when validating production-like leases, claims, checkpoints, and transaction behavior. UseLocalTransport() only means the local host does not need RabbitMQ or Azure Service Bus; it does not imply SQLite storage.

The combined local modes do not change command transactions, outbox/inbox behavior, provider leases, projection checkpointing, notification dispatch, or ProcessManager semantics. Worker and NotificationDispatcher roles require stable nonblank ids, and each id must be unique for concurrently running instances. For production-like role testing, run separate processes explicitly: one configured with RunApi(), one with RunWorker("..."), and one with RunNotificationDispatcher("...").

The same application binary or Docker image can be used for all production roles. The host application's configuration layer decides whether to call RunApi(), RunWorker(...), or RunNotificationDispatcher(...); AnchorFlow does not inspect Kubernetes labels, pod metadata, environment variables, or machine names. A common Kubernetes setup exposes metadata.uid through the Downward API, reads it through normal application configuration, and passes that value as the Worker or NotificationDispatcher id.

Runtime roles:

  • RunApi(): API host accepts commands, commits write-store changes and outbox rows, and may perform bounded immediate post-commit flush. It does not start queue listeners, fallback sweepers, projections, notification dispatch, process manager workers, or retention loops.
  • RunWorker("worker-id"): Worker host starts transport listeners and owns fallback outbox sweeps, subscription work, projections, process manager work, scheduled work, and retention. It does not dispatch view notifications.
  • RunNotificationDispatcher("dispatcher-id"): Notification Dispatcher host claims provider-ready notification outbox rows after projection transaction commit and projection-event barrier release, then invokes the registered app-owned IAnchorFlowViewNotificationDelivery.
  • RunApiAndWorker("worker-id"): local/dev host that combines both roles with an explicit worker id.
  • RunApiWorkerAndNotificationDispatcher("runtime-id"): local/dev host that combines API, Worker, and NotificationDispatcher capabilities for runnable examples and single-process local validation.

Role option overloads:

options.RunWorker(worker =>
{
  worker.Id = workerId;
  worker.MessagePublishing.MaxConcurrent = 1;
  worker.MessagePublishing.ReclaimAfter = TimeSpan.FromMinutes(2);
  worker.MessagePublishing.RetryDelay = TimeSpan.FromSeconds(30);
  worker.AsyncProcessing.MaxConcurrent = 4;
  worker.AsyncProcessing.MaxConcurrentPerTenant = 2;
  worker.AsyncProcessing.MaxConcurrentPerSubscription = 1;
  worker.AsyncProcessing.ReclaimAfter = TimeSpan.FromMinutes(2);
});

options.RunNotificationDispatcher(dispatcher =>
{
  dispatcher.Id = dispatcherId;
  dispatcher.NotificationDelivery.MaxConcurrent = 4;
  dispatcher.NotificationDelivery.ReclaimAfter = TimeSpan.FromSeconds(30);
});

These settings are per process/pod. Kubernetes replicas multiply total capacity, so initial values should leave CPU, memory, database connections, broker credits, and downstream notification capacity headroom. For 2 vCPU / 2 GB pods, start Worker pods with MessagePublishing.MaxConcurrent = 1 and AsyncProcessing.MaxConcurrent = 4, and start NotificationDispatcher pods with NotificationDelivery.MaxConcurrent = 4.

For MessagePublishing, ReclaimAfter and RetryDelay are separate operational knobs. ReclaimAfter is the claim lease duration: if a process crashes or cannot actively release a claim, another Worker can reclaim the row after the lease expires. RetryDelay is the scheduled wait after a non-cancellation publish failure before the next attempt is due. Shortening retry delay does not shorten crash lease expiry, and shortening reclaim time does not make failed rows immediately due for retry.

Transport modes:

  • UseLocalTransport() for examples and local single-process execution without RabbitMQ or Azure Service Bus.
  • UseRabbitMq(connectionString) for RabbitMQ.
  • UseAzureServiceBus(connectionString) for Azure Service Bus.

Low-Level And Existing-Host APIs:

  • UseWriteDbContext<TDbContext>(...), Handles<TCommand>(), and HandleCommandsFromAssembly(...) are the existing route-based command APIs. New beginner docs and examples should use provider shortcuts with UseWriteDbContexts(...).
  • AddAnchorFlow<TDbContext>(...) remains available for existing single-DbContext hosts.
  • AddAnchorFlowEntityFrameworkCommandBus<TDbContext>() remains available for focused tests, custom registration, and runtime internals. It binds one EF command DbContext and does not model multi-DbContext command routing.
  • AddAnchorFlowWolverine(...), AnchorFlowWolverineOptions, and direct UseMessaging(...) remain available for advanced tests and low-level adapter work.
  • UseLocalWolverineDefaults(...), Wolverine RabbitMQ options, and Wolverine Azure Service Bus options are implementation-level APIs. They are not required in beginner setup.
  • Existing low-level store descriptors and provider SPI types remain available for custom registry/provisioning work, but normal applications should use provider shortcuts where available, or UseSharedDatabase(...), UseDatabasePerTenant(...), and UseTenants(...) for custom topology setup.

Operational details:

  • Durable outbox is always the correctness boundary. Brokers are performance/distribution infrastructure, not a replacement for the outbox.
  • Immediate flush is a latency fast path after commit. If it fails without FailRequest, the command remains successful and worker fallback recovers pending rows.
  • MessagePublishing claims and publishes pending committed messages for Worker hosts.
  • AsyncProcessing handles durable asynchronous work for normal subscribers, projections, and ProcessManagers.
  • NotificationDelivery claims committed view notifications and invokes the registered app-owned delivery implementation.
  • An EventSubscriber may publish one follow-up command through ICommandBus.PublishAsync(...). If that command has no explicit idempotency key, AnchorFlow derives one from SubscriptionId, event id, and command type; multiple follow-up commands, waiting, timeouts, expected replies, or orchestration steps belong in a ProcessManager.
  • Default topology uses bounded logical lanes: main, projection, notification, and process. Tenant id is metadata, not part of default queue names.
  • Friendly work names map to AnchorFlow durable infrastructure internally, including outbox rows, async work items, projection checkpoints, notification outbox rows, and transport backpressure. Normal application configuration does not require internal table names or Wolverine internals.
  • AnchorFlowWorkerLifecycleAdapter closes the central runtime gate on host shutdown. During graceful shutdown or scale-in, Worker and NotificationDispatcher paths stop creating new claims and actively release unfinished runtime claims for outbox messages, durable async work, projection checkpoints, and notification outbox rows when durable completion was not recorded. Active release lets another runtime instance reclaim the work before the original lease would expire. If the process crashes or release cannot complete, passive lease expiry through ReclaimAfter remains the recovery fallback.
  • Outbox and notification delivery are at-least-once. If a Worker or NotificationDispatcher releases work after an external broker send or app-owned notification delivery succeeds but before the durable dispatched mark is recorded, a later runtime may deliver the same durable item again. Downstream consumers, delivery targets, and domain constraints must stay idempotent.
  • AnchorFlowWolverineWorkerStartupValidator verifies that enabled worker services have the required runtime plan and DI registrations before the worker host starts processing. Worker event fan-out with registered subscriptions requires an IAnchorFlowStorageProvider that can also persist IAnchorFlowSubscriptionWorkItemStore pending rows; low-level Wolverine configurations that cannot provide that durable subscription work-item persistence fail before subscription work-item messages are published.
  • Health checks separate liveness, API readiness, and Worker readiness. Worker readiness is stricter and normally requires stores plus configured transport readiness.

ASP.NET Core Integration

ASP.NET Core support lives in src/AnchorFlow.AspNetCore.

  • Tenant resolution uses AspNetCoreTenantResolver and AspNetCoreTenantResolverOptions.
  • Health registration maps IAnchorFlowRuntimeHealthMonitor to liveness, API readiness, and Worker readiness checks.
  • API readiness requires write-store resolution for the configured readiness tenant.
  • Worker readiness requires configured work stores and transport readiness unless explicitly relaxed.

Request tenant setup is host-application responsibility. In a typical ASP.NET Core API, application middleware reads and validates the tenant from JWT claims, headers, route values, or an application tenant service, then sets the AnchorFlow ambient tenant for the rest of the request:

app.Use(async (httpContext, next) =>
{
  var tenantAccessor = httpContext.RequestServices.GetRequiredService<ITenantContextAccessor>();
  var applicationTenantGuid = ResolveTenantGuidFromJwt(httpContext);
  using var tenantScope = tenantAccessor.BeginScope(
    TenantContext.ForTenant(new TenantId(applicationTenantGuid.ToString("D")), "jwt"));

  await next();
});

After this point, application code can publish commands without putting the tenant id into every command constructor, as long as the command does not intentionally target a different explicit tenant. AnchorFlow command execution will use the ambient tenant and will reject mismatches between an explicit command/options tenant and the current ambient tenant.

Worker execution is different: Worker hosts do not have an HTTP request or JWT tenant. The tenant id has already been captured in the command envelope and outbox records by the API-side command transaction. When the Worker claims outbox, subscription, projection, notification, consumer, or process-manager work, AnchorFlow rehydrates the ambient tenant from the persisted message metadata for that work item before invoking user handlers.

Examples

Examples are part of the implementation contract. Keep them compile-tested and runnable without PostgreSQL, RabbitMQ, Azure Service Bus, Redis, Azure SignalR Service, or external SignalR servers. Example application code must use public AnchorFlow APIs through AddAnchorFlow(...), ICommandBus, events, subscribers, projections, notifications, and ProcessManagers; do not copy provider outbox/inbox/envelope internals into examples.

All retained examples are ASP.NET Core hosts. Each example owns appsettings.json and Options binding, then maps those host-owned values into AnchorFlow options such as SQLite file names, local transport, tenants, and runtime role ids. AnchorFlow still does not read IConfiguration or appsettings.json directly. The examples use local SQLite and local transport, accept the X-AnchorFlow-Tenant header where multiple tenants are configured, and expose expected business outcomes as HTTP result values instead of exception-driven business flow.

The run commands below start long-running web hosts. For validation after dotnet build examples/AnchorFlow.Examples.sln -c Release --no-restore, add --no-build before -- --urls ....

Example Demonstrates Run host Smoke endpoints
examples/BasicCrud Beginner ASP.NET Core AddAnchorFlow(...) setup with managed CustomersDbContext, host-owned Options, SQLite/local transport, ICommandBus.PublishAsync(...), strict business events, command results, and subscriber-visible audit output. dotnet run --project examples/BasicCrud/AnchorFlow.Examples.BasicCrud.csproj -c Release -- --urls http://127.0.0.1:5102 GET /health/live, GET /api/basic-crud/status, POST /api/basic-crud/customers, GET /api/basic-crud/customers/{customerId}/welcome-notification
examples/BulkOperations ASP.NET Core bulk command API over a managed InventoryDbContext, strict many-resource events, subscriber audit output, result responses, and SQLite/local configuration. dotnet run --project examples/BulkOperations/AnchorFlow.Examples.BulkOperations.csproj -c Release -- --urls http://127.0.0.1:5103 GET /health/live, GET /api/bulk-operations/status, POST /api/bulk-operations/archive, GET /api/bulk-operations/audits/{auditId}
examples/AnchorFlow.Examples.DddOrders DDD aggregate HTTP API with root-scoped aggregate events, automatic aggregate event collection from a managed OrdersDbContext, subscriber-visible order audit output, result responses, and grouped bulk-order saga compensation. dotnet run --project examples/AnchorFlow.Examples.DddOrders/AnchorFlow.Examples.DddOrders.csproj -c Release -- --urls http://127.0.0.1:5108 GET /health/live, GET /api/ddd-orders/status, POST /api/ddd-orders/orders, GET /api/ddd-orders/bulk-submissions/{bulkId}/progress
examples/MissionsProperties ASP.NET Core Mission/Property command API with entity-owned evaluation, public events, subscriber audit rows, tenant-scoped reads, and result-pattern responses. dotnet run --project examples/MissionsProperties/AnchorFlow.Examples.MissionsProperties.csproj -c Release -- --urls http://127.0.0.1:5105 GET /health/live, GET /api/missions-properties/status, POST /api/missions-properties/missions/{missionId}/properties/{propertyId}/status, GET /api/missions-properties/audits
examples/MissionsImportProcessManager ProcessManager-driven Mission import API using application commands, expected replies, SQLite/local services, single-tenant local API+Worker execution, and process-visible batch progress. dotnet run --project examples/MissionsImportProcessManager/AnchorFlow.Examples.MissionsImportProcessManager.csproj -c Release -- --urls http://127.0.0.1:5106 GET /health/live, GET /api/missions-import-process-manager/status, POST /api/missions-import/imports, GET /api/missions-import/imports/{importId}/state
examples/MultiTenantCrud Tenant-scoped ASP.NET Core command API with shared and database-per-tenant SQLite routing, tenant-isolated projections, notifications, and application-visible order cards. dotnet run --project examples/MultiTenantCrud/AnchorFlow.Examples.MultiTenantCrud.csproj -c Release -- --urls http://127.0.0.1:5192 GET /health/live, GET /api/multi-tenant-crud/status, GET /api/multi-tenant-crud/topology, POST /api/multi-tenant-crud/orders with X-AnchorFlow-Tenant
examples/ProjectionsAndNotifications ASP.NET Core ticket API with command-published events, projection read models, staged view notifications, recording app-owned delivery output, and SQLite/local API+Worker+NotificationDispatcher execution. dotnet run --project examples/ProjectionsAndNotifications/AnchorFlow.Examples.ProjectionsAndNotifications.csproj -c Release -- --urls http://127.0.0.1:5191 GET /health/live, GET /api/projections-notifications/status, POST /api/projections-notifications/tickets, GET /api/projections-notifications/notifications
examples/ProcessManagerImport ASP.NET Core multi-type import API using ProcessManager, DurableList<T>, Customers/Vehicles/Missions/MissionProperties sections, app-owned typed keys, generic progress snapshots, expected replies, operation results, and saga compensation. dotnet run --project examples/ProcessManagerImport/AnchorFlow.Examples.ProcessManagerImport.csproj -c Release -- --urls http://127.0.0.1:5107 GET /health/live, GET /api/process-manager-import/status, POST /api/process-manager-import/imports, GET /api/process-manager-import/imports/{importId}/progress

Tests And Validation

Baseline product validation:

dotnet restore AnchorFlow.sln
dotnet build AnchorFlow.sln -c Release --no-restore
dotnet test AnchorFlow.sln -c Release --no-build -m:28 -- RunConfiguration.MaxCpuCount=28 NUnit.NumberOfTestWorkers=28

Examples validation:

dotnet restore examples/AnchorFlow.Examples.sln
dotnet build examples/AnchorFlow.Examples.sln -c Release --no-restore
dotnet test examples/AnchorFlow.Examples.sln -c Release --no-build -m:28 -- RunConfiguration.MaxCpuCount=28 NUnit.NumberOfTestWorkers=28

# Start each host with a bounded automation wrapper, wait for /health/live, run the listed HTTP smoke endpoints,
# then stop the host. Example host command shape after the Release build:
dotnet run --project examples/BasicCrud/AnchorFlow.Examples.BasicCrud.csproj -c Release --no-build -- --urls http://127.0.0.1:5102
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5102/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5102/api/basic-crud/status

dotnet run --project examples/BulkOperations/AnchorFlow.Examples.BulkOperations.csproj -c Release --no-build -- --urls http://127.0.0.1:5103
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5103/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5103/api/bulk-operations/status

dotnet run --project examples/AnchorFlow.Examples.DddOrders/AnchorFlow.Examples.DddOrders.csproj -c Release --no-build -- --urls http://127.0.0.1:5108
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5108/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5108/api/ddd-orders/status

dotnet run --project examples/MissionsProperties/AnchorFlow.Examples.MissionsProperties.csproj -c Release --no-build -- --urls http://127.0.0.1:5105
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5105/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5105/api/missions-properties/status

dotnet run --project examples/MissionsImportProcessManager/AnchorFlow.Examples.MissionsImportProcessManager.csproj -c Release --no-build -- --urls http://127.0.0.1:5106
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5106/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5106/api/missions-import-process-manager/status

dotnet run --project examples/MultiTenantCrud/AnchorFlow.Examples.MultiTenantCrud.csproj -c Release --no-build -- --urls http://127.0.0.1:5192
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5192/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5192/api/multi-tenant-crud/status

dotnet run --project examples/ProjectionsAndNotifications/AnchorFlow.Examples.ProjectionsAndNotifications.csproj -c Release --no-build -- --urls http://127.0.0.1:5191
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5191/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5191/api/projections-notifications/status

dotnet run --project examples/ProcessManagerImport/AnchorFlow.Examples.ProcessManagerImport.csproj -c Release --no-build -- --urls http://127.0.0.1:5107
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5107/health/live
Invoke-RestMethod -Method Get -Uri http://127.0.0.1:5107/api/process-manager-import/status

Package validation:

dotnet pack AnchorFlow.sln -c Release --no-build

PostgreSQL tests configure their own Testcontainers-backed PostgreSQL instance:

dotnet test AnchorFlow.sln -c Release --no-build -m:28 -- RunConfiguration.MaxCpuCount=28 NUnit.NumberOfTestWorkers=28
dotnet test tests/AnchorFlow.PostgreSql.Tests/AnchorFlow.PostgreSql.Tests.csproj -c Release --no-build -m:28 -- RunConfiguration.MaxCpuCount=28 NUnit.NumberOfTestWorkers=28

PostgreSQL provider tests start a postgres:16-alpine Testcontainers instance from the test fixture. Docker/Testcontainers must be available; otherwise the PostgreSQL tests fail explicitly instead of silently skipping provider coverage. Tests must not read database connection strings from environment variables.

Test selection guidance:

  • Command, consumer, outbox, inbox, notification, retention, dead-letter, process, projection, provider, or public API changes need targeted tests close to the changed package plus broader solution validation when feasible.
  • Example API changes need an examples solution build and runnable checks for touched examples when feasible.
  • Provider behavior needs the relevant provider contract tests.
  • EF query changes that rely on SQL translation need relational/provider-backed validation. Do not treat EF Core InMemory as proof of SQL behavior.
  • Runtime reliability changes should keep the static audits green: no framework runtime role selection from Environment.GetEnvironmentVariable, APP_ROLE, or Environment.MachineName, and no provider ordering based on MAX() + 1.
  • Keep tests deterministic. Prefer controllable clocks, deterministic IDs, and test schedulers over sleeps.

Build, Packaging, And Versioning

Central settings:

  • Directory.Build.props targets net10.0, sets package metadata, maps package descriptions per project name, and packs the root README.md as the NuGet package readme for every package.
  • Directory.Packages.props owns package versions.
  • Packages are written to artifacts/packages/Release by default.

Version defaults:

  • VersionPrefix=0.1.0
  • VersionSuffix=alpha.2

Override package version at pack time:

dotnet pack AnchorFlow.sln -c Release -p:VersionPrefix=0.1.0 -p:VersionSuffix=alpha.2
dotnet pack AnchorFlow.sln -c Release -p:VersionPrefix=0.1.0 -p:VersionSuffix=

Release script:

.\eng\release.ps1 -Configuration Release -VersionPrefix 0.1.0 -VersionSuffix alpha.2
.\eng\release.ps1 -Configuration Release -VersionPrefix 0.1.0 -VersionSuffix "" -Publish -NuGetSource $env:NUGET_SOURCE -ApiKey $env:NUGET_API_KEY

Implementation Rules For Agents

Before editing:

  • Run git status --porcelain=v1 --untracked-files=all.
  • If the workspace is dirty, do not overwrite user changes. Keep edits scoped and report only files you changed.
  • For planned architecture work, read the relevant files under AgentPlans/<FeatureName>, especially 00_Master_Goal_Orchestration.md, 01_Shared_Architecture_Contract.md, 02_Shared_Validation_Contract.md, story contracts, state files, guard findings, validation ledgers, and final audit files.

Coding rules:

  • Preserve package boundaries.
  • Keep public APIs small, documented, and consistent.
  • Product-code public APIs need XML documentation. Implementing members should use <inheritdoc /> when directly implementing documented contracts.
  • Keep cancellation tokens in async APIs and pass them through.
  • Preserve ConfigureAwait(false) patterns in library code.
  • Follow current style: block-scoped namespaces are common, explicit this. is common, sealed classes are preferred where inheritance is not intended, and one type per file is required.
  • Do not return tuples from public/protected APIs. Use a named result type.
  • Remove unused usings, dead code, and warnings introduced by the change.

Architecture rules:

  • Commands and consumers own transaction boundaries through the pipeline, not through handlers.
  • Do not call brokers or view-notification transport delivery directly from command handlers, consumers, or projections.
  • Outbox/inbox behavior must remain transactional and idempotent.
  • Projection handlers stage notifications and let the runtime commit. They do not call SaveChanges.
  • View notifications must go through the notification outbox.
  • Process managers orchestrate state and commands/replies; they do not execute business work directly.
  • Framework storage must stay generic and tenant-scoped.
  • Retention, dead-letter, leases, claiming, idempotency, and checkpoints must preserve tenant/system partitioning.
  • AnchorFlow framework runtime code must not choose API/Worker/NotificationDispatcher role or production runtime identity from environment variables. Host apps and examples may read configuration or environment and map it into simplified direct role options such as RunApi(), RunWorker(...), RunNotificationDispatcher(...), RunApiAndWorker(...), and RunApiWorkerAndNotificationDispatcher(...).

EF and SQL rules:

  • In EF-backed packages/examples/tests, prefer EF Core LINQ in fluent style unless provider-specific SQL is the purpose of the package.
  • Before changing non-trivial EF queries, inspect entity CLR types, store types, conversions, owned/JSON mapping, and translation risks.
  • Be careful with typed IDs and converted properties in queries. Equality may translate; ordering/range comparisons and .Value, .ToString(), CompareTo, Guid helpers, and StringComparison are fragile unless verified.
  • Provider SQL must be parameterized.

Useful implementation anchors:

  • Command workflow: EntityFrameworkCommandPipeline<TDbContext> and tests in tests/AnchorFlow.EntityFrameworkCore.Tests/Messaging/EntityFrameworkCommandPipelineTests.cs.
  • Consumer workflow: EntityFrameworkConsumerPipeline<TDbContext> and tests in tests/AnchorFlow.EntityFrameworkCore.Tests/Messaging/EntityFrameworkConsumerPipelineTests.cs.
  • Provider contract: AnchorFlowStorageProviderContractTests plus SQLite/PostgreSQL wrappers.
  • Projection workflow: ProjectionProcessor and tests/AnchorFlow.Projections.Tests/ProjectionProcessorTests.cs.
  • View-notification dispatch: AnchorFlowViewNotificationDispatcher and tests/AnchorFlow.ViewNotifications.Tests.
  • Process manager runtime: ProcessManagerRuntime, ProcessStateStore<TState>, and tests/AnchorFlow.ProcessManager.Tests.
  • Wolverine dispatch/runtime: AnchorFlowWolverine* classes and tests/AnchorFlow.Wolverine.Tests.
  • Storage topology: TenantKey, AnchorFlowTenantRegistryEntry, AnchorFlowTenantStoreRegistration, ConfiguredAnchorFlowTenantRegistry, ConfiguredAnchorFlowStoreResolver, IAnchorFlowCurrentStore, AnchorFlowTenantProvisioner, tests/AnchorFlow.Core.Tests/Storage/ConfiguredAnchorFlowStoreResolverTests.cs, tests/AnchorFlow.Core.Tests/Storage/AnchorFlowTenantProvisionerTests.cs, and tests/AnchorFlow.EntityFrameworkCore.Tests/Storage/CurrentStoreDbContextRoutingTests.cs.
  • Runnable behavior references: examples listed above.
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 (1)

Showing the top 1 NuGet packages that depend on AnchorFlow.EntityFrameworkCore:

Package Downloads
AnchorFlow.EntityFrameworkCore.Sqlite

SQLite EF Core shortcuts for configuring AnchorFlow storage provider, topology and managed DbContext defaults with connection strings.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0-alpha.2 41 7/7/2026
0.1.0-alpha.1 60 7/4/2026

Initial prerelease packaging contract for AnchorFlow 0.1.0 alpha packages.