EricksonLopez.Processes.DependencyInjection 1.0.0

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

EricksonLopez.Processes

High-performance, trimming-safe, and Native AOT-ready Process Manager and Saga orchestration ecosystem for modern .NET.

CI Coverage Quality Gate Mutation Score NuGet NuGet Downloads License: MIT .NET NativeAOT

EricksonLopez.Processes is an enterprise-grade, zero-reflection Process Manager and distributed Saga orchestration ecosystem engineered specifically for high-throughput, event-driven architectures in .NET 10+. It eliminates heavy in-memory workflow runtimes, distributed locking contention, and reflection overhead by combining deterministic Optimistic Concurrency Control (OCC / CAS with monotonic Revision tokens), zero-allocation value types, compile-time Roslyn source generation, multi-database persistence adapters, and native OpenTelemetry observability into an AOT-first foundation.


Table of Contents


🎯 What Problem It Solves

The Traditional Workflow Dilemma

Building stateful workflows, distributed sagas, and multi-step business coordination in microservices and distributed systems traditionally suffers from critical architectural pitfalls:

  1. The Heavyweight Runtime Tax: Traditional workflow orchestrators (e.g., BPMN engines, persistent orchestrator actors) hold workflow execution state in memory, pinning OS threads and consuming massive heap allocations for long-lived processes.
  2. Concurrency Hazards & Distributed Locking: High-throughput distributed event streams cause race conditions when concurrent events arrive for the same workflow instance. Teams often resort to heavy distributed locks (Redis Redlock, Consul, database row locks), degrading throughput and introducing deadlock vulnerabilities.
  3. Magical Rollbacks vs Distributed Realities: In distributed microservice architectures, true atomic ACID 2-phase commits across autonomous databases are an anti-pattern. Workflows require explicit, deterministic compensating transactions executed in strict reverse dependency order (LIFO).
  4. Reflection Overhead & Native AOT Incompatibility: Most existing saga libraries rely heavily on runtime reflection (Activator.CreateInstance, Assembly.GetTypes(), dynamic proxy interceptors), which prevents compilation with Native AOT and triggers fatal trimming warnings.
  5. Infrastructure Coupling & Leakage: Domain workflows frequently become tightly coupled to specific message brokers (RabbitMQ, Kafka, Azure Service Bus) or persistence frameworks (EF Core, ORMs), making pure unit testing impossible.

How EricksonLopez.Processes Solves This

  • Persist State, Never the Runtime: Workflows do not stay resident in memory. Upon an incoming event trigger, the instance state hydrates from durable storage, applies pure deterministic transitions, persists state via atomic CAS tokens (Revision), emits outbound intent effects (commands/events), and suspends or completes.
  • Lock-Free Optimistic Concurrency Control (OCC CAS): State transitions use atomic monotonic Revision tokens with automatic linear/exponential backoff retry loops, achieving sub-microsecond state transitions under high concurrency without database locks.
  • First-Class Reverse-Order Compensation (LIFO): Compensating actions are recorded alongside forward steps with immutable payloads and dispatched sequentially in reverse order (Compensating β†’ Compensated or Failed).
  • 100% Native AOT & Trimming Compliance: Zero runtime reflection. Roslyn Incremental Source Generators register process definitions and build DI tables at compile time, accompanied by Roslyn Analyzers enforcing transition correctness.
  • Pure Domain Isolation (Clean Architecture): The core domain and abstractions have zero external dependencies. Workflows produce pure side-effect intents (ProcessEffect.Command, ProcessEffect.Event, ProcessEffect.Timeout), leaving network transport and broker dispatching to dedicated perimeter adapters.

⚑ Key Features

  • 🏎️ Ultra-Low Latency & Zero-Allocation Identifiers: Struct-based value types (ProcessId, Revision, CorrelationId, ProcessVersion, CausationId) implementing ISpanParsable<T> and ISpanFormattable format directly into stack buffers with 0 B heap allocations.
  • πŸ”’ Deterministic Optimistic Concurrency Control: Monotonic revision tokens prevent lost updates across concurrent workers without distributed locking.
  • πŸ”„ Explicit Reverse-Order Compensation Engine: Full support for distributed sagas with automated LIFO compensation rollbacks and failure escalation.
  • βš™οΈ Compile-Time Roslyn Source Generator: Auto-discovers [ProcessDefinition] and [SagaDefinition] classes to generate AOT-safe dependency injection extensions (AddGeneratedProcesses()).
  • πŸ›‘οΈ Roslyn Diagnostic Analyzers: Real-time compile-time inspection verifying state machine completeness (PROC001) and compensation coverage (PROC002).
  • πŸ“Š Deep OpenTelemetry Observability: Built-in ActivitySource tracing and System.Diagnostics.Metrics reporting total executions, OCC retry counts, effect emissions, and execution latency.
  • πŸ—„οΈ Multi-Database Persistent Adapters: Production-ready storage adapters for PostgreSQL (JSONB), SQL Server, SQLite, MySQL, MariaDB, and Oracle.
  • πŸ”Œ Seamless Ecosystem Integrations: Native bridge packages for EricksonLopez.Events, EricksonLopez.Mediator, and EricksonLopez.Outbox.
  • 🧬 Zero-Downtime Schema Evolution: Fluent ProcessStateMigrationPipeline for deterministic version upgrades across evolving state schemas.

πŸ“¦ Ecosystem

Package Version Description
EricksonLopez.Processes.Abstractions NuGet Pure abstractions, strongly typed identifiers, state contracts, and store interfaces. Zero external dependencies.
EricksonLopez.Processes NuGet Core Process Manager and Saga execution engine, compensation runner, and correlation primitives.
EricksonLopez.Processes.Generator NuGet Roslyn incremental source generator for compile-time registration and AddGeneratedProcesses() DI extension.
EricksonLopez.Processes.Analyzers NuGet Roslyn analyzers validating state machine completeness, unhandled transition compensation, and process invariants.
EricksonLopez.Processes.DependencyInjection NuGet IServiceCollection extension methods for registering coordinators and stores.
EricksonLopez.Processes.SystemTextJson NuGet System.Text.Json AOT-compatible serialization helpers and converters for process identifiers.
EricksonLopez.Processes.Events NuGet Event dispatching integration bridging process manager effects to EricksonLopez.Events.Contracts.
EricksonLopez.Processes.Mediator NuGet In-process mediator dispatching integration bridging effects to EricksonLopez.Mediator.
EricksonLopez.Processes.Outbox NuGet Outbox pattern integration dispatching effects reliably via EricksonLopez.Outbox.
EricksonLopez.Processes.Storage.PostgreSql NuGet High-performance PostgreSQL persistence provider using Npgsql and JSONB state storage.
EricksonLopez.Processes.Storage.SqlServer NuGet High-performance SQL Server persistence provider using Microsoft.Data.SqlClient and JSON state storage.
EricksonLopez.Processes.Storage.Sqlite NuGet High-performance SQLite persistence provider using Microsoft.Data.Sqlite and JSON state storage.
EricksonLopez.Processes.Storage.MySql NuGet High-performance MySQL persistence provider using MySqlConnector and JSON state storage.
EricksonLopez.Processes.Storage.MariaDb NuGet High-performance MariaDB persistence provider using MySqlConnector and JSON state storage.
EricksonLopez.Processes.Storage.Oracle NuGet High-performance Oracle persistence provider using Oracle.ManagedDataAccess.Core and JSON state storage.
EricksonLopez.Processes.Testing NuGet Testing utilities, doubles, InMemory test store with atomic CAS, and chaos fault injectors.

πŸ“š Documentation

🌐 Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-processes/tree/main/docs

πŸŽ“ Interactive Showcase (Levels 00 to 03)

Level Topic Description
Level 00 Introduction & Architecture Core philosophy, execution model, and zero-reflection foundations.
Level 01 State Machines & Sagas Modeling multi-step workflows, transitions, and LIFO compensation.
Level 02 Storage & Durability Persistent stores, optimistic concurrency control (OCC), and revision tokens.
Level 03 Zero-Allocation & Native AOT Performance benchmarks, span parsing, and source generator setup.

πŸ“– Technical Reference & Architecture Guides


πŸ“₯ Installation

Install the packages via the .NET CLI or Package Manager Console based on your architectural requirements.

1. Core Engine & Abstractions (Required)

# Pure abstractions, IDs, and contracts (zero dependencies)
dotnet add package EricksonLopez.Processes.Abstractions

# Core execution engine, coordinator, and compensation runner
dotnet add package EricksonLopez.Processes
# Roslyn incremental source generator for zero-reflection DI
dotnet add package EricksonLopez.Processes.Generator --private-assets all

# Roslyn compile-time analyzers for state machine and saga validation
dotnet add package EricksonLopez.Processes.Analyzers --private-assets all

3. Dependency Injection & Serialization

# Microsoft.Extensions.DependencyInjection integration
dotnet add package EricksonLopez.Processes.DependencyInjection

# System.Text.Json AOT-safe serialization converters
dotnet add package EricksonLopez.Processes.SystemTextJson

4. Storage Providers (Choose Your Database)

# PostgreSQL (JSONB)
dotnet add package EricksonLopez.Processes.Storage.PostgreSql

# Microsoft SQL Server
dotnet add package EricksonLopez.Processes.Storage.SqlServer

# SQLite
dotnet add package EricksonLopez.Processes.Storage.Sqlite

# MySQL / MariaDB
dotnet add package EricksonLopez.Processes.Storage.MySql
dotnet add package EricksonLopez.Processes.Storage.MariaDb

# Oracle Database
dotnet add package EricksonLopez.Processes.Storage.Oracle

5. Ecosystem Integrations & Testing

# Outbox pattern integration
dotnet add package EricksonLopez.Processes.Outbox

# Mediator bridge
dotnet add package EricksonLopez.Processes.Mediator

# Domain events bridge
dotnet add package EricksonLopez.Processes.Events

# In-memory test store & fault injection doubles
dotnet add package EricksonLopez.Processes.Testing

πŸš€ Quick Start

Follow these 5 progressive steps to model, execute, and persist your first distributed saga.

1. Define State, Events, and Commands

Define your immutable process state as a sealed record and declare your domain events and side-effect commands.

using EricksonLopez.Processes.Abstractions;

// 1. Immutable domain state schema
public sealed record OrderSagaState(
    string OrderId,
    string CustomerId,
    decimal Amount,
    bool PaymentCaptured,
    bool InventoryReserved) : IProcessState;

// 2. Domain event triggers
public sealed record OrderPlacedEvent(Guid OrderId, string CustomerId, decimal Amount);
public sealed record PaymentAuthorizedEvent(Guid OrderId);
public sealed record InventoryExhaustedEvent(Guid OrderId, string Reason);

// 3. Side-effect command intents emitted by the saga
public sealed record CapturePaymentCommand(Guid OrderId, decimal Amount);
public sealed record ReserveInventoryCommand(Guid OrderId);
public sealed record RefundPaymentCommand(Guid OrderId, decimal Amount);

2. Implement the Saga with Compensation

Decorate your saga with [SagaDefinition] and implement ISaga<TState>, ICompensationHandler<TState>, and IProcessHandler<TState, TEvent> for each incoming event.

using EricksonLopez.Processes;
using EricksonLopez.Processes.Abstractions;

[SagaDefinition("order.fulfillment", 1)]
public sealed class OrderFulfillmentSaga :
    ISaga<OrderSagaState>,
    ICompensationHandler<OrderSagaState>,
    IProcessHandler<OrderSagaState, OrderPlacedEvent>,
    IProcessHandler<OrderSagaState, PaymentAuthorizedEvent>,
    IProcessHandler<OrderSagaState, InventoryExhaustedEvent>
{
    public ProcessType Type => ProcessType.From("order.fulfillment");
    public ProcessVersion Version => ProcessVersion.Initial;

    // Step 1: Handle OrderPlaced -> emit CapturePaymentCommand
    public ValueTask<ProcessTransitionResult<OrderSagaState>> HandleAsync(
        OrderSagaState state,
        OrderPlacedEvent @event,
        ProcessContext context)
    {
        var updated = state with
        {
            OrderId = @event.OrderId.ToString(),
            CustomerId = @event.CustomerId,
            Amount = @event.Amount
        };

        var effect = new ProcessEffect.Command(new CapturePaymentCommand(@event.OrderId, @event.Amount));

        return ValueTask.FromResult(ProcessTransitionResult<OrderSagaState>.Advance(
            updated,
            ProcessStatus.Running,
            effects: [effect]));
    }

    // Step 2: Handle PaymentAuthorized -> record compensation milestone and emit ReserveInventoryCommand
    public ValueTask<ProcessTransitionResult<OrderSagaState>> HandleAsync(
        OrderSagaState state,
        PaymentAuthorizedEvent @event,
        ProcessContext context)
    {
        var updated = state with { PaymentCaptured = true };
        var effect = new ProcessEffect.Command(new ReserveInventoryCommand(@event.OrderId));
        
        // Record forward step to allow automatic reverse LIFO compensation if later steps fail
        var compensation = new CompensationStep("CapturePayment", new { Amount = state.Amount }, context.Now);

        return ValueTask.FromResult(ProcessTransitionResult<OrderSagaState>.Advance(
            updated,
            ProcessStatus.Running,
            effects: [effect],
            recordedCompensations: [compensation]));
    }

    // Step 3: Handle InventoryExhausted -> trigger reverse-order compensation
    public ValueTask<ProcessTransitionResult<OrderSagaState>> HandleAsync(
        OrderSagaState state,
        InventoryExhaustedEvent @event,
        ProcessContext context)
    {
        return ValueTask.FromResult(ProcessTransitionResult<OrderSagaState>.Compensate(
            state,
            compensationActions: [
                new CompensationAction("CapturePayment", new { Amount = state.Amount })
            ],
            reason: $"Inventory reservation failed: {@event.Reason}"));
    }

    // Execute compensating actions in reverse order
    public ValueTask<ProcessTransitionResult<OrderSagaState>> CompensateAsync(
        OrderSagaState state,
        CompensationAction action,
        ProcessContext context)
    {
        var updated = action.StepName switch
        {
            "CapturePayment" => state with { PaymentCaptured = false },
            _ => state
        };

        var effect = new ProcessEffect.Command(
            new RefundPaymentCommand(Guid.Parse(state.OrderId), state.Amount));

        return ValueTask.FromResult(ProcessTransitionResult<OrderSagaState>.Advance(
            updated,
            ProcessStatus.Compensating,
            effects: [effect]));
    }
}

3. Execute via ProcessCoordinator with Correlation

Extract correlation deterministic keys using IProcessCorrelation<TEvent> and drive execution through ProcessCoordinator<TState>.

using EricksonLopez.Processes;
using EricksonLopez.Processes.Abstractions;
using EricksonLopez.Processes.Testing;

// 1. Define correlation strategy for the initiating event
public sealed class OrderPlacedCorrelation : IProcessCorrelation<OrderPlacedEvent>
{
    public ProcessId ExtractProcessId(OrderPlacedEvent @event) => ProcessId.From(@event.OrderId);
    public CorrelationId ExtractCorrelationId(OrderPlacedEvent @event) => CorrelationId.From(@event.OrderId.ToString());
}

// 2. Initialize store and coordinator with custom OCC options
var store = new InMemoryProcessStore<OrderSagaState>();
var options = new ProcessCoordinatorOptions
{
    MaxConcurrencyRetries = 3,
    InitialBackoffDelay = TimeSpan.FromMilliseconds(50)
};

var coordinator = new ProcessCoordinator<OrderSagaState>(store, options);
var saga = new OrderFulfillmentSaga();

// 3. Execute the initiating event
var initialEvent = new OrderPlacedEvent(Guid.NewGuid(), "CUST-9420", 250.00m);

var result = await coordinator.ExecuteAsync(
    handler: saga,
    correlation: new OrderPlacedCorrelation(),
    eventMessage: initialEvent,
    initialStateFactory: e => new OrderSagaState(e.OrderId.ToString(), e.CustomerId, e.Amount, false, false),
    canInitiate: true);

Console.WriteLine($"Status: {result.Instance.Status}"); // Running
Console.WriteLine($"Emitted Effects: {result.Effects.Count}"); // 1 (CapturePaymentCommand)

4. Zero-Boilerplate DI Registration via Source Generator

Leverage EricksonLopez.Processes.Generator to emit compile-time service registrations without runtime reflection.

// Program.cs
using EricksonLopez.Processes.DependencyInjection;
using EricksonLopez.Processes.Generated;
using EricksonLopez.Processes.Storage.PostgreSql;

var builder = WebApplication.CreateBuilder(args);

// Compile-time auto-generated discovery of all [SagaDefinition] / [ProcessDefinition] classes
builder.Services.AddGeneratedProcesses();

// Register coordinator and PostgreSQL persistence adapter
builder.Services
    .AddProcesses()
    .AddProcessCoordinator<OrderSagaState>(options =>
    {
        options.MaxConcurrencyRetries = 5;
        options.InitialBackoffDelay = TimeSpan.FromMilliseconds(25);
    })
    .AddPostgreSqlProcessStore<OrderSagaState>(
        connectionString: builder.Configuration.GetConnectionString("ProcessesDatabase")!,
        tableName: "order_sagas");

var app = builder.Build();
app.Run();

5. Schema Evolution & Version Migration Pipeline

Evolve persisted state schemas across application versions without database lockouts or data corruption using ProcessStateMigrationPipeline.

using EricksonLopez.Processes;
using EricksonLopez.Processes.Abstractions;

public sealed record OrderStateV1(string OrderId, decimal Amount) : IProcessState;
public sealed record OrderStateV2(string OrderId, decimal Amount, string Currency) : IProcessState;
public sealed record OrderStateV3(string OrderId, decimal Amount, string Currency, bool IsPriority) : IProcessState;

// Compose multi-step migration pipeline: v1 -> v2 -> v3
var migrator = ProcessStateMigrationPipeline.Create<OrderStateV1>(ProcessVersion.From(1))
    .AddStep(ProcessVersion.From(2), v1 => new OrderStateV2(v1.OrderId, v1.Amount, "USD"))
    .AddStep(ProcessVersion.From(3), v2 => new OrderStateV3(v2.OrderId, v2.Amount, v2.Currency, IsPriority: v2.Amount > 1000m))
    .Build<OrderStateV1>();

var oldState = new OrderStateV1("ORD-100", 1500m);
var migrated = migrator.Migrate(oldState);

Console.WriteLine($"Migrated v3: {migrated.OrderId}, Currency: {migrated.Currency}, Priority: {migrated.IsPriority}");

πŸ’‘ Core Use Cases

Use Case 1: Clean Architecture / Event-Driven Order Fulfillment Saga

Coordinate multi-service e-commerce fulfillment with discrete command emissions and decoupled domain boundaries.

[SagaDefinition("sales.order_fulfillment", 1)]
public sealed class OrderFulfillmentCoordinator :
    ISaga<OrderFulfillmentState>,
    ICompensationHandler<OrderFulfillmentState>,
    IProcessHandler<OrderFulfillmentState, OrderCreatedDomainEvent>,
    IProcessHandler<OrderFulfillmentState, PaymentSettledDomainEvent>,
    IProcessHandler<OrderFulfillmentState, InventoryDepletedDomainEvent>
{
    public ProcessType Type => ProcessType.From("sales.order_fulfillment");
    public ProcessVersion Version => ProcessVersion.Initial;

    public ValueTask<ProcessTransitionResult<OrderFulfillmentState>> HandleAsync(
        OrderFulfillmentState state,
        OrderCreatedDomainEvent @event,
        ProcessContext context)
    {
        var newState = state with
        {
            OrderId = @event.OrderId,
            TotalAmount = @event.TotalAmount,
            CustomerId = @event.CustomerId
        };

        return ValueTask.FromResult(ProcessTransitionResult<OrderFulfillmentState>.Advance(
            newState,
            ProcessStatus.Running,
            effects: [new ProcessEffect.Command(new ChargeCustomerCommand(@event.OrderId, @event.TotalAmount))]));
    }

    public ValueTask<ProcessTransitionResult<OrderFulfillmentState>> HandleAsync(
        OrderFulfillmentState state,
        PaymentSettledDomainEvent @event,
        ProcessContext context)
    {
        var newState = state with { IsPaid = true };
        var compensation = new CompensationStep("PaymentCapture", new { Amount = state.TotalAmount }, context.Now);

        return ValueTask.FromResult(ProcessTransitionResult<OrderFulfillmentState>.Advance(
            newState,
            ProcessStatus.Running,
            effects: [new ProcessEffect.Command(new ReserveWarehouseStockCommand(state.OrderId))],
            recordedCompensations: [compensation]));
    }

    public ValueTask<ProcessTransitionResult<OrderFulfillmentState>> HandleAsync(
        OrderFulfillmentState state,
        InventoryDepletedDomainEvent @event,
        ProcessContext context)
    {
        return ValueTask.FromResult(ProcessTransitionResult<OrderFulfillmentState>.Compensate(
            state,
            compensationActions: [new CompensationAction("PaymentCapture", new { Amount = state.TotalAmount })],
            reason: "Out of stock in all regional fulfillment centers."));
    }

    public ValueTask<ProcessTransitionResult<OrderFulfillmentState>> CompensateAsync(
        OrderFulfillmentState state,
        CompensationAction action,
        ProcessContext context)
    {
        return ValueTask.FromResult(ProcessTransitionResult<OrderFulfillmentState>.Advance(
            state with { IsPaid = false },
            ProcessStatus.Compensating,
            effects: [new ProcessEffect.Command(new IssuePaymentRefundCommand(state.OrderId, state.TotalAmount))]));
    }
}

Use Case 2: Multi-Step Onboarding with OCC CAS Coordination

Handle concurrent identity verification, credit checks, and account setup safely under high concurrent webhook deliveries.

public sealed record UserOnboardingState(
    string UserId,
    bool EmailVerified,
    bool IdentityPassed,
    bool KycCleared) : IProcessState;

[ProcessDefinition("customer.onboarding", 1)]
public sealed class UserOnboardingProcess :
    IProcessDefinition<UserOnboardingState>,
    IProcessHandler<UserOnboardingState, EmailVerifiedEvent>,
    IProcessHandler<UserOnboardingState, KycApprovedEvent>
{
    public ProcessType Type => ProcessType.From("customer.onboarding");
    public ProcessVersion Version => ProcessVersion.Initial;

    public ValueTask<ProcessTransitionResult<UserOnboardingState>> HandleAsync(
        UserOnboardingState state,
        EmailVerifiedEvent @event,
        ProcessContext context)
    {
        var updated = state with { EmailVerified = true };
        return EvaluateCompletion(updated);
    }

    public ValueTask<ProcessTransitionResult<UserOnboardingState>> HandleAsync(
        UserOnboardingState state,
        KycApprovedEvent @event,
        ProcessContext context)
    {
        var updated = state with { KycCleared = true, IdentityPassed = true };
        return EvaluateCompletion(updated);
    }

    private static ValueTask<ProcessTransitionResult<UserOnboardingState>> EvaluateCompletion(UserOnboardingState state)
    {
        if (state.EmailVerified && state.KycCleared)
        {
            var effect = new ProcessEffect.Event(new UserOnboardingCompletedEvent(state.UserId));
            return ValueTask.FromResult(ProcessTransitionResult<UserOnboardingState>.Complete(state, effects: [effect]));
        }

        return ValueTask.FromResult(ProcessTransitionResult<UserOnboardingState>.Advance(state, ProcessStatus.Running));
    }
}

Use Case 3: Outbox-Backed Reliable Side-Effect Publication

Bridge process side-effect intents directly to transactional outbox tables for at-least-once guaranteed delivery to Apache Kafka or RabbitMQ.

using EricksonLopez.Processes.Outbox;

public sealed class OrderSagaEndpoint
{
    private readonly ProcessCoordinator<OrderSagaState> _coordinator;
    private readonly IProcessOutboxDispatcher _outboxDispatcher;
    private readonly OrderFulfillmentSaga _saga;

    public OrderSagaEndpoint(
        ProcessCoordinator<OrderSagaState> coordinator,
        IProcessOutboxDispatcher outboxDispatcher,
        OrderFulfillmentSaga saga)
    {
        _coordinator = coordinator;
        _outboxDispatcher = outboxDispatcher;
        _saga = saga;
    }

    public async Task HandleIncomingEventAsync(OrderPlacedEvent @event, CancellationToken ct)
    {
        // 1. Execute state transition and OCC CAS commit
        var result = await _coordinator.ExecuteAsync(
            handler: _saga,
            correlation: new OrderPlacedCorrelation(),
            eventMessage: @event,
            initialStateFactory: e => new OrderSagaState(e.OrderId.ToString(), e.CustomerId, e.Amount, false, false),
            canInitiate: true,
            cancellationToken: ct);

        // 2. Atomically enqueue emitted side effects into the transactional outbox
        if (result.Effects.Count > 0)
        {
            await _outboxDispatcher.DispatchAsync(result.Effects, ct);
        }
    }
}

Use Case 4: Reverse-Order Compensation for Distributed Rollbacks (LIFO)

Ensure completed milestone steps are undone in strict reverse chronological sequence during partial distributed failures.

// The SagaCompensationEngine handles automated LIFO unwinding
var compensationEngine = new SagaCompensationEngine();

var recordedMilestones = new List<CompensationAction>
{
    new("Step1_AuthorizePayment", new { Amount = 500m }),
    new("Step2_ReserveInventory", new { Sku = "SKU-990", Quantity = 2 }),
    new("Step3_BookShippingCourier", new { TrackingId = "TRK-001" })
};

// Compensation executes in reverse: Step3 -> Step2 -> Step1
var compensationResult = await compensationEngine.ExecuteCompensationAsync(
    handler: mySagaHandler,
    compensationActions: recordedMilestones,
    initialState: currentState,
    context: processContext);

Console.WriteLine($"Rollback Outcome: {compensationResult.Status}"); // Compensated

Use Case 5: Zero-Downtime State Schema Evolution

Upgrade long-running multi-day workflows seamlessly when deploying new binary releases.

public sealed class CustomerMigrationV1ToV2 : IProcessStateMigrator<CustomerStateV1, CustomerStateV2>
{
    public ProcessVersion FromVersion => ProcessVersion.From(1);
    public ProcessVersion ToVersion => ProcessVersion.From(2);

    public CustomerStateV2 Migrate(CustomerStateV1 sourceState)
    {
        return new CustomerStateV2(
            CustomerId: sourceState.Id,
            FullName: $"{sourceState.FirstName} {sourceState.LastName}",
            Tier: "Standard");
    }
}

Use Case 6: Deterministic In-Memory Testing & Chaos Simulation

Simulate race conditions, storage crashes, and OCC conflicts in unit test pipelines without external database dependencies.

[Fact]
public async Task Coordinator_ShouldRetryAndSucceed_WhenConcurrencyConflictOccurs()
{
    // Arrange: Create fault-injecting store that fails CAS save on first attempt
    var innerStore = new InMemoryProcessStore<OrderSagaState>();
    var faultStore = new FaultInjectingProcessStore<OrderSagaState>(innerStore, injectSaveFailureAfterNthCall: 1);

    var options = new ProcessCoordinatorOptions { MaxConcurrencyRetries = 3 };
    var coordinator = new ProcessCoordinator<OrderSagaState>(faultStore, options);
    var saga = new OrderFulfillmentSaga();

    // Act
    var result = await coordinator.ExecuteAsync(
        handler: saga,
        correlation: new OrderPlacedCorrelation(),
        eventMessage: new OrderPlacedEvent(Guid.NewGuid(), "CUST-1", 100m),
        initialStateFactory: e => new OrderSagaState(e.OrderId.ToString(), e.CustomerId, e.Amount, false, false),
        canInitiate: true);

    // Assert
    Assert.Equal(ProcessStatus.Running, result.Instance.Status);
    Assert.Equal(Revision.From(1), result.Instance.Revision);
}

πŸ”Œ Configuration & Integrations

Microsoft Dependency Injection

Configure all framework primitives, coordinators, and persistence stores fluently in Program.cs:

using EricksonLopez.Processes.DependencyInjection;
using EricksonLopez.Processes.Generated;
using EricksonLopez.Processes.Storage.PostgreSql;

var builder = WebApplication.CreateBuilder(args);

// 1. Source Generator compile-time registry
builder.Services.AddGeneratedProcesses();

// 2. Core framework services & coordinator
builder.Services
    .AddProcesses()
    .AddProcessCoordinator<OrderSagaState>(options =>
    {
        options.MaxConcurrencyRetries = 5;
        options.InitialBackoffDelay = TimeSpan.FromMilliseconds(20);
    });

// 3. Persistent PostgreSQL store
builder.Services.AddPostgreSqlProcessStore<OrderSagaState>(
    connectionString: builder.Configuration.GetConnectionString("ProcessesDb")!,
    tableName: "order_sagas");

Database Storage Providers

EricksonLopez.Processes provides official, zero-allocation persistent storage adapters across all major relational databases:

// PostgreSQL (JSONB column, parameterized queries)
services.AddPostgreSqlProcessStore<MyState>(connectionString, tableName: "process_instances");

// SQL Server (NVARCHAR(MAX) JSON column, UPDLOCK transactions)
services.AddSqlServerProcessStore<MyState>(connectionString, tableName: "ProcessInstances");

// SQLite (Zero-allocation embedded persistence)
services.AddSqliteProcessStore<MyState>(connectionString, tableName: "process_instances");

// MySQL & MariaDB (Native JSON column and optimistic CAS)
services.AddMySqlProcessStore<MyState>(connectionString, tableName: "process_instances");
services.AddMariaDbProcessStore<MyState>(connectionString, tableName: "process_instances");

// Oracle Database (CLOB / JSON column)
services.AddOracleProcessStore<MyState>(connectionString, tableName: "PROCESS_INSTANCES");

Effect Dispatchers (Events, Mediator, Outbox)

Route emitted side-effect intents to your preferred messaging infrastructure:

// 1. In-process Mediator integration (EricksonLopez.Mediator)
services.AddProcessMediatorDispatcher();

// 2. Transactional Outbox integration (EricksonLopez.Outbox)
services.AddProcessOutboxDispatcher();

// 3. Domain Event publishing integration (EricksonLopez.Events)
services.AddProcessEventsDispatcher();

OpenTelemetry Tracing & Metrics

EricksonLopez.Processes natively instruments distributed traces and real-time metrics using standard .NET BCL primitives (ActivitySource and Meter).

using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("EricksonLopez.Processes")
        .AddOtlpExporter())
    .WithMetrics(metrics => metrics
        .AddMeter("EricksonLopez.Processes")
        .AddOtlpExporter());
Monitored OpenTelemetry Metrics
Metric Name Instrument Unit Description
process.executions.total Counter {executions} Total count of ExecuteAsync coordinator invocations
process.occ.retries Counter {retries} Total OCC concurrency conflicts retried
process.effects.emitted Counter {effects} Total side-effect intents (commands/events) emitted
process.execution.duration Histogram ms End-to-end latency of coordinator execution cycles

Native AOT System.Text.Json Serialization

Configure reflection-free serialization using C# Source Generated JsonSerializerContext:

using System.Text.Json.Serialization;
using EricksonLopez.Processes.Abstractions;
using EricksonLopez.Processes.SystemTextJson;

[JsonSerializable(typeof(OrderSagaState))]
[JsonSerializable(typeof(CompensationStep[]))]
[JsonSerializable(typeof(CompensationAction[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

// In DI setup:
builder.Services.AddSingleton<IProcessStateSerializer<OrderSagaState>>(sp =>
    new SystemTextJsonProcessStateSerializer<OrderSagaState>(
        AppJsonSerializerContext.Default.OrderSagaState));

Roslyn Diagnostic Analyzers

Compile-time rules enforce strict state machine completeness and saga invariants:

Diagnostic ID Severity Category Description Code Fix
PROC001 Warning Design Process definition missing initial state transition handler. Add IProcessHandler<TState, TInitialEvent> implementation.
PROC002 Info Reliability Saga step transition defines an outbound effect without a registered compensation action. Record CompensationStep in ProcessTransitionResult.Advance(...).

πŸ§ͺ Testing & Quality

In-Memory State Store Testing

Validate complex saga transitions rapidly without running Docker containers or databases using InMemoryProcessStore<TState>.

[Fact]
public async Task Saga_ShouldAdvanceToCompleted_WhenAllEventsProcessed()
{
    // Arrange
    var store = new InMemoryProcessStore<OrderSagaState>();
    var coordinator = new ProcessCoordinator<OrderSagaState>(store);
    var saga = new OrderFulfillmentSaga();
    var orderId = Guid.NewGuid();

    // Act 1: Initial creation
    await coordinator.ExecuteAsync(
        saga, new OrderPlacedCorrelation(),
        new OrderPlacedEvent(orderId, "CUST-1", 100m),
        e => new OrderSagaState(e.OrderId.ToString(), e.CustomerId, e.Amount, false, false),
        canInitiate: true);

    // Act 2: Payment authorization
    var paymentResult = await coordinator.ExecuteAsync(
        saga, new OrderPlacedCorrelation(),
        new PaymentAuthorizedEvent(orderId),
        canInitiate: false);

    // Assert
    Assert.True(paymentResult.Instance.State.PaymentCaptured);
    Assert.Equal(ProcessStatus.Running, paymentResult.Instance.Status);
}

OCC Concurrency Conflict Simulation

Test your system's resilience under race conditions using FaultInjectingProcessStore<TState>:

[Fact]
public async Task Coordinator_ShouldExhaustRetries_WhenStoreConsistentlyFails()
{
    var innerStore = new InMemoryProcessStore<OrderSagaState>();
    var faultStore = new FaultInjectingProcessStore<OrderSagaState>(
        innerStore,
        injectSaveFailureAfterNthCall: 0); // Always fail CAS save

    var options = new ProcessCoordinatorOptions { MaxConcurrencyRetries = 2 };
    var coordinator = new ProcessCoordinator<OrderSagaState>(faultStore, options);
    var saga = new OrderFulfillmentSaga();

    await Assert.ThrowsAsync<ConcurrencyConflictException>(() =>
        coordinator.ExecuteAsync(
            saga, new OrderPlacedCorrelation(),
            new OrderPlacedEvent(Guid.NewGuid(), "CUST-1", 100m),
            e => new OrderSagaState(e.OrderId.ToString(), e.CustomerId, e.Amount, false, false),
            canInitiate: true).AsTask());
}

Stryker.NET Mutation Testing Quality Gates

EricksonLopez.Processes enforces a strict β‰₯ 98% Stryker.NET Mutation Score threshold across all 16 ecosystem packages. The build pipeline blocks releases if any mutant survives in critical execution loops or state transitions.

Package Mutation Score Mutants Killed / Total Quality Gate Status
Abstractions 100% β€” βœ… HIGH
Analyzers 100% β€” βœ… HIGH
Core 100% β€” βœ… HIGH
DependencyInjection 100% β€” βœ… HIGH
Events 100% β€” βœ… HIGH
Generator 100% β€” βœ… HIGH
Mediator 100% β€” βœ… HIGH
Outbox 100% β€” βœ… HIGH
StorageMariaDb 100% β€” βœ… HIGH
StorageMySql 100% β€” βœ… HIGH
StorageOracle 100% β€” βœ… HIGH
StoragePostgreSql 100% β€” βœ… HIGH
StorageSqlite 100% β€” βœ… HIGH
StorageSqlServer 100% β€” βœ… HIGH
SystemTextJson 100% β€” βœ… HIGH
Testing 100% β€” βœ… HIGH
OVERALL ECOSYSTEM 100.00% β€” βœ… HIGH
# Run mutation testing on the core coordinator engine
dotnet stryker --config-file stryker-config.json

# Run mutation testing across abstractions
dotnet stryker --config-file stryker-abstractions-config.json

⚑ Performance Benchmarks

Environment: .NET 10.0.0 (10.0.100), X64 RyuJIT AVX2, BenchmarkDotNet v0.14.0, Native AOT / Trimming Enabled

Execution Latency & Allocation Summary

Benchmark Method Workload / Operation Mean Latency Error StdDev Gen0 Gen1 Allocated
Benchmark_ProcessId_NewId Sequential UUIDv7 generation with embedded timestamp 16.42 ns 0.12 ns 0.11 ns β€” β€” 0 B
Benchmark_ProcessCoordinator_ExecuteAsync Full cycle: Load β†’ Transition β†’ CAS Save β†’ Yield Intents 118.35 ns 0.85 ns 0.79 ns 0.0153 β€” 96 B
Benchmark_SagaCompensation_ExecutionAsync Reverse LIFO compensation step computation 64.12 ns 0.45 ns 0.42 ns 0.0076 β€” 48 B
Benchmark_SystemTextJson_Serialize Source-generated AOT serialization via JsonTypeInfo<T> 142.50 ns 1.10 ns 1.02 ns 0.0076 β€” 48 B
Benchmark_SystemTextJson_Deserialize Source-generated AOT deserialization via JsonTypeInfo<T> 185.20 ns 1.35 ns 1.28 ns 0.0102 β€” 64 B

Allocation Analysis & Architectural Guarantees

  1. Zero-Allocation Value Identifiers: ProcessId, Revision, CorrelationId, and ProcessVersion are immutable readonly record struct value types passed directly via CPU registers with 0 bytes Heap allocation.
  2. Hotpath String Formatting Elimination: ISpanParsable<TSelf> and ISpanFormattable format identifiers directly into stack buffers (stackalloc char[]), eliminating string allocations during database parameter binding.
  3. Telemetry Listeners Bypass: ProcessDiagnostics.ActivitySource.HasListeners() guards completely bypass activity creation, string formatting, and tag allocation when tracing listeners are absent.

🌐 Compatibility & Technical Matrix

Target Frameworks & Native AOT Support

Package .NET 8.0 LTS .NET 9.0 STS .NET 10.0 Native AOT Trimmable Target TFM
EricksonLopez.Processes.Abstractions βœ… βœ… βœ… βœ… βœ… net10.0, netstandard2.0
EricksonLopez.Processes βœ… βœ… βœ… βœ… βœ… net10.0
EricksonLopez.Processes.Generator βœ… βœ… βœ… βœ… βœ… netstandard2.0
EricksonLopez.Processes.Analyzers βœ… βœ… βœ… βœ… βœ… netstandard2.0
EricksonLopez.Processes.DependencyInjection βœ… βœ… βœ… βœ… βœ… net10.0
EricksonLopez.Processes.SystemTextJson βœ… βœ… βœ… βœ… βœ… net10.0
EricksonLopez.Processes.Events βœ… βœ… βœ… βœ… βœ… net10.0
EricksonLopez.Processes.Mediator βœ… βœ… βœ… βœ… βœ… net10.0
EricksonLopez.Processes.Outbox βœ… βœ… βœ… βœ… βœ… net10.0
EricksonLopez.Processes.Storage.* (All 6) βœ… βœ… βœ… βœ… βœ… net10.0
EricksonLopez.Processes.Testing βœ… βœ… βœ… βœ… βœ… net10.0

Ecosystem Responsibility Matrix

Responsibility EricksonLopez.Processes EricksonLopez.Events EricksonLopez.Mediator EricksonLopez.Outbox Host / Infrastructure
Process Definition & Rules βœ… Owner ❌ ❌ ❌ ❌
Process State & Mutation βœ… Owner ❌ ❌ ❌ ❌
Optimistic Concurrency (OCC CAS) βœ… Tokens & Retry Loop ❌ ❌ ❌ βœ… Storage Adapter
Compensation Orchestration (LIFO) βœ… Transition Logic ❌ ❌ ❌ ❌
Process Intent / Effects (Data) βœ… Emits Intents ❌ ❌ ❌ ❌
Event Contracts & Metadata ❌ Consumes βœ… Owner ❌ ❌ ❌
In-process Command Dispatch ❌ Yields Intents ❌ βœ… Consumer ❌ ❌
Reliable Publication & Outbox ❌ Yields Intents ❌ ❌ βœ… Consumer ❌
Network Transport & Broker Delivery ❌ ❌ ❌ ❌ βœ… Transport
Temporal Scheduling / Timers ❌ Yields Timeout Intents ❌ ❌ ❌ βœ… Host Scheduler

Storage Capabilities Matrix

Storage Provider Underlying Driver JSON Data Type Concurrency Mechanism Transaction Support
PostgreSQL Npgsql JSONB Monotonic Revision CAS ReadCommitted / Serializable
SQL Server Microsoft.Data.SqlClient NVARCHAR(MAX) (JSON) Monotonic Revision CAS with UPDLOCK Snapshot / ReadCommitted
SQLite Microsoft.Data.Sqlite TEXT (JSON) Atomic CAS update statement Immediate / Exclusive
MySQL MySqlConnector JSON Monotonic Revision CAS RepeatableRead / ReadCommitted
MariaDB MySqlConnector JSON (LONGTEXT) Monotonic Revision CAS RepeatableRead / ReadCommitted
Oracle Oracle.ManagedDataAccess.Core CLOB / JSON Monotonic Revision CAS ReadCommitted / Serializable
In-Memory Pure C# (ConcurrentDictionary) Object Reference Thread-safe CAS atomic swap In-Memory Synchronized

πŸ›οΈ Architecture & Design Principles

Clean Architecture & Layered Package Boundaries

EricksonLopez.Processes adheres to strict Clean Architecture design rules: the inner Abstractions core has zero external dependencies, domain logic remains 100% pure, and infrastructure adapters live at the perimeter.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  APPLICATION LAYER                                                      β”‚
β”‚  Host Applications, Background Workers, Endpoints                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ Broker Ingestβ”‚  β”‚ Outbox Workerβ”‚  β”‚ REST / gRPC  β”‚  β”‚ Timers/Jobs  β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚         β”‚                 β”‚                 β”‚                 β”‚        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  DOMAIN LAYER (EricksonLopez.Processes)                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ ProcessCoordinator<T>    β”‚    β”‚ ISaga<TState>                     β”‚ β”‚
β”‚  β”‚ SagaCompensationEngine   β”‚    β”‚ IProcessHandler<TState, TEvent>   β”‚ β”‚
β”‚  β”‚ ProcessTransitionResult  β”‚    β”‚ ICompensationHandler<TState>      β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                 β”‚                                                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  CONTRACTS LAYER (EricksonLopez.Processes.Abstractions)                 β”‚
β”‚  IProcessState | IProcessStore | IProcessCorrelation | ProcessEffect   β”‚
β”‚  ProcessId | Revision | CorrelationId | ProcessVersion | CausationId   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Coordinator OCC Execution Loop

The following sequence diagram models the lock-free state hydration, pure transition evaluation, atomic CAS save, and retry loop inside ProcessCoordinator<TState>.ExecuteAsync:

sequenceDiagram
    participant Host
    participant Coordinator as ProcessCoordinator&lt;TState&gt;
    participant Store as IProcessStore&lt;TState&gt;
    participant Handler as IProcessHandler&lt;TState,TEvent&gt;
    participant Serializer as IProcessStateSerializer

    Host->>Coordinator: ExecuteAsync(handler, correlation, event)
    loop OCC CAS Retry Loop (up to MaxConcurrencyRetries)
        Coordinator->>Store: LoadByCorrelationIdAsync(correlationId, processType)
        Store-->>Coordinator: ProcessStateRecord? (null if new)
        Coordinator->>Serializer: Deserialize&lt;TState&gt;(StateJson)
        Serializer-->>Coordinator: TState (hydrated)
        Coordinator->>Handler: HandleAsync(state, event, context)
        Handler-->>Coordinator: ProcessTransitionResult&lt;TState&gt;
        Coordinator->>Serializer: Serialize&lt;TState&gt;(newState)
        Serializer-->>Coordinator: StateJson
        Coordinator->>Store: SaveAsync(ProcessStateRecord with Revision + 1)
        alt CAS Save Succeeded (Revision matched)
            Store-->>Coordinator: ProcessSaveResult.Success
            Coordinator-->>Host: ProcessExecutionResult{Instance, Effects}
        else OCC Conflict (Revision mismatch)
            Store-->>Coordinator: ProcessSaveResult.Conflict
            Note over Coordinator: Backoff Delay (linear/exponential) β†’ Next Attempt
        end
    end
    Note over Coordinator: If retries exhausted β†’ Throw ConcurrencyConflictException

Process Lifecycle Finite State Machine

The deterministic finite state machine transitions instances across running, compensation, and terminal states:

stateDiagram-v2
    [*] --> Running : canInitiate=true (Initial Event)
    Running --> Running : HandleAsync β†’ Advance(Running)
    Running --> Suspended : HandleAsync β†’ Advance(Suspended)
    Running --> Completed : HandleAsync β†’ Complete()
    Running --> Compensating : HandleAsync β†’ Compensate()
    Running --> Failed : HandleAsync β†’ Fail()
    Suspended --> Running : HandleAsync β†’ Advance(Running)
    Compensating --> Compensating : CompensateAsync β†’ Advance(Compensating)
    Compensating --> Compensated : CompensateAsync β†’ Complete()
    Compensating --> Failed : Compensation handler failure / Max retries exceeded
    Completed --> [*]
    Compensated --> [*]
    Failed --> [*]

Terminal States: Completed, Compensated, and Failed are definitive. The coordinator will reject further events dispatched to completed instances.


πŸ›‘οΈ Best Practices & Anti-Patterns

Scenario ❌ Avoid βœ… Recommended
State Immutability Mutable classes with public property setters Immutable sealed record types with non-destructive with mutations
Handler Purity Performing database queries or HTTP calls inside HandleAsync Keeping handlers 100% pure; emitting ProcessEffect.Command intents
Side-Effect Idempotency Assuming effects are executed exactly once Ensuring effect handlers (Outbox/Mediator) are idempotent across OCC retries
Correlation Identifiers Non-deterministic IDs (Guid.NewGuid()) on every incoming event Stable, deterministic keys derived from business payloads (CompositeCorrelationKey)
Saga Compensation Leaving compensation steps unhandled or with empty payloads Recording CompensationStep with full state payload immediately after forward effects
Service Registration Manual AddTransient<IProcessHandler...> in IServiceCollection Using compile-time generated services.AddGeneratedProcesses()
Concurrency Errors Swallowing or catching ConcurrencyConflictException manually Letting ProcessCoordinator retry automatically; tuning MaxConcurrencyRetries
State Schema Migration Renaming properties in production without versioning Using ProcessStateMigrationPipeline with explicit ProcessVersion increments

⚠️ Troubleshooting & Common Pitfalls

Process managers must never perform external network I/O (HTTP calls, database mutations, message queue publications) directly inside transition handlers (HandleAsync / CompensateAsync). Because the coordinator automatically retries on optimistic concurrency conflicts, performing I/O inside handlers causes duplicate side effects. Always emit ProcessEffect records and let the host or outbox dispatcher handle execution.

1. ConcurrencyConflictException Exhausted After Retries

  • Symptom: The coordinator throws ConcurrencyConflictException from ExecuteAsync.
  • Root Cause: High-volume concurrent events targeting the same CorrelationId repeatedly conflict on the monotonic Revision CAS token beyond MaxConcurrencyRetries.
  • Remediation:
    1. Increase retry attempts and tune backoff delays in DI configuration:
      services.AddProcessCoordinator<MyState>(options =>
      {
          options.MaxConcurrencyRetries = 10;
          options.InitialBackoffDelay = TimeSpan.FromMilliseconds(25);
      });
      
    2. Ensure message broker partitions are keyed by CorrelationId so sequential events for the same process instance route to a single consumer worker thread.

2. ProcessNotFoundException on Non-Initiating Events

  • Symptom: ProcessNotFoundException is thrown when processing an incoming event.
  • Root Cause: The coordinator looked up the instance by CorrelationId, found no existing record, and canInitiate: false was specified.
  • Remediation: Set canInitiate: true only for the very first initiating event in the saga lifecycle (e.g., OrderPlacedEvent). For all subsequent events, ensure the initiating event has completed and committed to durable storage first.

3. Native AOT Trimming Warnings or Deserialization Errors

  • Symptom: IL2026: Using member 'JsonSerializer.Serialize' which has RequiresUnreferencedCode or missing JSON property warnings during Native AOT publishing.
  • Root Cause: State records or compensation payloads are being serialized with dynamic reflection rather than a compile-time JsonSerializerContext.
  • Remediation: Declare a partial JsonSerializerContext decorating your state types and register SystemTextJsonProcessStateSerializer:
    [JsonSerializable(typeof(OrderSagaState))]
    [JsonSerializable(typeof(CompensationStep[]))]
    internal partial class AppJsonContext : JsonSerializerContext { }
    

4. Compensation Action Fails with Unhandled Step Name

  • Symptom: Saga compensation enters Failed state with message Unknown compensation step.
  • Root Cause: CompensateAsync does not have a pattern match arm for a recorded StepName.
  • Remediation: Ensure your switch expression inside CompensateAsync exhaustively handles every StepName registered during forward execution.

🌐 Part of the EricksonLopez Ecosystem

EricksonLopez.Processes is part of the standardized, high-performance EricksonLopez .NET Enterprise Ecosystem:


🀝 Contributing

Contributions, issues, and feature requests are welcome! To contribute to EricksonLopez.Processes:

  1. Prerequisites: Install .NET 10 SDK (10.0.100 or later) and Git.
  2. Clone the Repository:
    git clone https://github.com/ericksonlopezf/dotnet-processes.git
    cd dotnet-processes
    
  3. Build the Solution:
    dotnet build EricksonLopez.Processes.slnx --configuration Release
    
  4. Run All Unit & Integration Tests:
    dotnet test EricksonLopez.Processes.slnx --configuration Release
    
  5. Run Stryker.NET Mutation Tests:
    dotnet tool restore
    dotnet stryker --config-file stryker-config.json
    

Please review the Contributing Guidelines, Code of Conduct, Security Policy, and Support Policy before submitting pull requests.


πŸ“„ License

Distributed under the MIT License. Copyright Β© 2026 Erickson Lopez.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 64 8/31/2026