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
// 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.
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.
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.
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).
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:
| Store | Interface | Purpose |
|---|---|---|
| Command Store | ICommandStore | Persists commands for event sourcing and replay |
| Entity Store | IEntityStore | Persists domain entities (aggregates) |
| ViewModel Store | IViewModelStore | Persists read models (projections) |
Entity Framework Setup
The SourceFlow.Stores.EntityFramework package provides production-ready persistence with support for SQL Server, PostgreSQL, SQLite, and MySQL.
dotnet add package SourceFlow.Stores.EntityFramework
Configuration Options
Single Connection String
services.AddSourceFlowEfStores("Server=localhost;Database=SourceFlow;...");
Separate Connection Strings
services.AddSourceFlowEfStores(
commandConnectionString: "Server=...;Database=Commands;...",
entityConnectionString: "Server=...;Database=Entities;...",
viewModelConnectionString: "Server=...;Database=Views;...");
Custom Database Provider
// 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:
// 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>();
Types must be registered before calling BuildServiceProvider() and ApplyMigrations().
Migrations
The ApplyMigrations() method creates tables for dynamically registered types using the DbContextMigrationHelper:
var entityContext = sp.GetRequiredService<EntityDbContext>();
await entityContext.Database.EnsureCreatedAsync(); // Create database
entityContext.ApplyMigrations(); // Create dynamic tables
Table naming uses the T prefix by default: BankAccount → TBankAccount. Customise with EntityTableNaming options.
Event Replay
Replay all commands for an aggregate to rebuild state:
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:
services.AddSourceFlowTelemetry(options =>
{
options.Enabled = true;
options.ServiceName = "MyService";
options.ServiceVersion = "1.0.0";
});
services.AddOpenTelemetry()
.AddSourceFlowOtlpExporter("http://localhost:4317");
Instrumented Operations
| Category | Traces |
|---|---|
| Commands | sourceflow.commandbus.dispatch, sourceflow.commanddispatcher.send |
| Events | sourceflow.eventqueue.enqueue, sourceflow.eventdispatcher.dispatch |
| Stores | sourceflow.entitystore.persist, sourceflow.viewmodelstore.persist |
Resilience with Polly
Production-grade fault tolerance for Entity Framework operations:
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:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Memory Allocation | ~40MB / 10K cmds | <1MB / 10K cmds | 40× reduction |
| Gen 0 GC | Baseline | — | ↓70% |
| Command Throughput | Baseline | — | +25-40% |
| Event Dispatching | Baseline | — | +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 events —
AccountCreated,MoneyDeposited(notAccountChanged) - 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.