Documentation

Comprehensive guide to building event-sourced applications with SourceFlow.Net, covering core concepts, framework components, persistence, and production patterns.

Event Sourcing

Event Sourcing is an architectural pattern where the state of an application is determined by a sequence of events. Instead of storing the current state directly, the system stores all the events that have occurred, allowing for complete state reconstruction at any point in time.

Key Benefits

  • Complete Audit Trail — Every change is recorded as an immutable event
  • Time Travel — Reconstruct system state at any point in history
  • Debugging — Full visibility into how the system reached its current state
  • Scalability — Events can be replayed to build multiple read models
C#
// Events are immutable records of what happened
public class AccountCreated : Event<BankAccount>
{
    public AccountCreated(BankAccount payload) : base(payload) { }
}

public class MoneyDeposited : Event<BankAccount>
{
    public MoneyDeposited(BankAccount payload) : base(payload) { }
}

Domain-Driven Design (DDD)

DDD focuses on modeling software to match the business domain. SourceFlow.Net provides first-class support for core DDD elements:

  • Entities — Objects with unique identity (IEntity)
  • Aggregates — Coordinate business logic, send commands (Aggregate<T>)
  • Sagas — Orchestrate long-running business processes (Saga<T>)
  • Bounded Contexts — Clear boundaries between domain models

CQRS

Command Query Responsibility Segregation separates read and write operations:

  • Commands — Represent intent to change state (Command<TPayload>)
  • Queries — Read from optimized view models (IViewModel)
  • Projections — Update read models based on events (IProjectOn<TEvent>)

Aggregates

Aggregates encapsulate business logic and coordinate with the command bus. They define the public API of your domain.

C#
public abstract class Aggregate<TEntity> : IAggregate
    where TEntity : class, IEntity
{
    protected ICommandPublisher commandPublisher;
    protected ILogger logger;

    // Send commands to command bus
    protected async Task Send(ICommand command);

    // Subscribe to external events
    public virtual Task On(IEvent @event);
}

Sagas

Sagas handle commands and coordinate business processes across aggregate boundaries. They implement IHandles<TCommand> for each command type they process.

C#
public abstract class Saga<TEntity> : ISaga
    where TEntity : class, IEntity
{
    protected IEntityStoreAdapter repository;
    protected ICommandPublisher commandPublisher;
    protected IEventQueue eventQueue;

    // Publish commands to self or other sagas
    protected async Task Publish<TCommand>(TCommand command);

    // Raise events to notify subscribers
    protected async Task Raise<TEvent>(TEvent @event);
}

Command Bus

The command bus routes commands to appropriate saga handlers and manages command persistence for replay.

C#
public interface ICommandBus
{
    Task Publish<TCommand>(TCommand command) where TCommand : ICommand;
    event EventHandler<ICommand> Dispatchers;
}

Event Queue

The event queue manages event flow and dispatches to subscribers (Aggregates and Views).

C#
public interface IEventQueue
{
    Task Enqueue<TEvent>(TEvent @event) where TEvent : IEvent;
    event EventHandler<IEvent> Dispatchers;
}

Stores (Persistence Layer)

SourceFlow.Net defines three core store interfaces:

StoreInterfacePurpose
Command StoreICommandStorePersists commands for event sourcing and replay
Entity StoreIEntityStorePersists domain entities (aggregates)
ViewModel StoreIViewModelStorePersists read models (projections)

Entity Framework Setup

The SourceFlow.Stores.EntityFramework package provides production-ready persistence with support for SQL Server, PostgreSQL, SQLite, and MySQL.

Terminal
dotnet add package SourceFlow.Stores.EntityFramework

Configuration Options

Single Connection String

C#
services.AddSourceFlowEfStores("Server=localhost;Database=SourceFlow;...");

Separate Connection Strings

C#
services.AddSourceFlowEfStores(
    commandConnectionString: "Server=...;Database=Commands;...",
    entityConnectionString: "Server=...;Database=Entities;...",
    viewModelConnectionString: "Server=...;Database=Views;...");

Custom Database Provider

C#
// PostgreSQL
services.AddSourceFlowEfStoresWithCustomProvider(options =>
    options.UseNpgsql("Host=localhost;Database=sourceflow;..."));

// SQLite
services.AddSourceFlowEfStoresWithCustomProvider(options =>
    options.UseSqlite("Data Source=sourceflow.db"));

Type Registration

Register entity and view model types before building the service provider:

C#
// Option 1: Assembly scanning (recommended)
EntityDbContext.RegisterAssembly(typeof(BankAccount).Assembly);
ViewModelDbContext.RegisterAssembly(typeof(AccountViewModel).Assembly);

// Option 2: Explicit registration
EntityDbContext.RegisterEntityType<BankAccount>();
ViewModelDbContext.RegisterViewModelType<AccountViewModel>();
⚠️ Important

Types must be registered before calling BuildServiceProvider() and ApplyMigrations().

Migrations

The ApplyMigrations() method creates tables for dynamically registered types using the DbContextMigrationHelper:

C#
var entityContext = sp.GetRequiredService<EntityDbContext>();
await entityContext.Database.EnsureCreatedAsync();  // Create database
entityContext.ApplyMigrations();                    // Create dynamic tables

Table naming uses the T prefix by default: BankAccountTBankAccount. Customise with EntityTableNaming options.

Event Replay

Replay all commands for an aggregate to rebuild state:

C#
var aggregate = sp.GetRequiredService<IAccountAggregate>();
await aggregate.ReplayHistory(accountId);

// The framework automatically:
// 1. Loads commands from store
// 2. Marks commands as replay (IsReplay = true)
// 3. Re-executes command handlers
// 4. Updates projections

Observability

Built-in OpenTelemetry support with <1ms latency overhead:

C#
services.AddSourceFlowTelemetry(options =>
{
    options.Enabled = true;
    options.ServiceName = "MyService";
    options.ServiceVersion = "1.0.0";
});

services.AddOpenTelemetry()
    .AddSourceFlowOtlpExporter("http://localhost:4317");

Instrumented Operations

CategoryTraces
Commandssourceflow.commandbus.dispatch, sourceflow.commanddispatcher.send
Eventssourceflow.eventqueue.enqueue, sourceflow.eventdispatcher.dispatch
Storessourceflow.entitystore.persist, sourceflow.viewmodelstore.persist

Resilience with Polly

Production-grade fault tolerance for Entity Framework operations:

C#
services.AddSourceFlowEfStores(options =>
{
    options.DefaultConnectionString = connectionString;
    options.Resilience.Enabled = true;
    options.Resilience.Retry.MaxRetryAttempts = 3;
    options.Resilience.Retry.UseExponentialBackoff = true;
    options.Resilience.CircuitBreaker.Enabled = true;
    options.Resilience.CircuitBreaker.FailureThreshold = 5;
    options.Resilience.Timeout.Enabled = true;
    options.Resilience.Timeout.TimeoutMs = 30000;
});

Performance

ArrayPool-based optimization for extreme throughput scenarios:

MetricBeforeAfterImprovement
Memory Allocation~40MB / 10K cmds<1MB / 10K cmds40× reduction
Gen 0 GCBaseline↓70%
Command ThroughputBaseline+25-40%
Event DispatchingBaseline+30-50%

These optimizations are zero-configuration — they're applied automatically.

Best Practices

  • Commands must have parameterless constructors — Required for deserialization from the command store
  • Use fine-grained eventsAccountCreated, MoneyDeposited (not AccountChanged)
  • Single responsibility sagas — One saga per aggregate lifecycle
  • Register types early — Before BuildServiceProvider()
  • Enable resilience in production — Retry + circuit breaker + timeout
  • Enable observability — <1ms overhead, invaluable for debugging
  • Use separate databases — Split command, entity, and view model stores for performance
  • Enable SQL idempotency for multi-instance — In-memory is insufficient for distributed deployments

FAQ

How does SourceFlow.Net handle persistence?

SourceFlow.Net uses a store abstraction pattern with in-memory stores (testing), Entity Framework stores (production), or custom implementations.

Can I use different databases for each store?

Yes! Use AddSourceFlowEfStoresWithCustomProviders() to specify different providers per store.

Why do commands need parameterless constructors?

The CommandStoreAdapter uses reflection to deserialize commands. Without it, replay fails with MissingMethodException.

What database providers are supported?

Any EF Core provider: SQL Server (default), PostgreSQL, SQLite, MySQL, In-Memory, and more.

Should I enable observability in production?

Yes! With <1ms latency and <2% CPU overhead, it provides invaluable distributed tracing and metrics. Use 10% sampling for production.

How much does ArrayPool improve performance?

In high-throughput scenarios (>1000 cmds/sec): 40× memory reduction, 70% fewer Gen0 GC collections, 25-40% throughput improvement. Zero configuration required.