Quickstart Guide
Get your first event-sourced application running in minutes. This guide walks you through installing SourceFlow.Net, setting up persistence, and building a complete banking domain example.
Prerequisites
- .NET 9.0 SDK or later installed
- SQL Server, PostgreSQL, SQLite, or any EF Core-compatible database
- A code editor (Visual Studio, VS Code, or Rider)
Installation
Install the core package and your preferred persistence provider:
# Core framework
dotnet add package SourceFlow.Net
# Entity Framework persistence
dotnet add package SourceFlow.Stores.EntityFramework
# AWS cloud messaging (optional)
dotnet add package SourceFlow.Cloud.AWS
Basic Setup
Configure SourceFlow with Entity Framework stores in your Program.cs:
using SourceFlow;
using SourceFlow.Stores.EntityFramework;
using SourceFlow.Stores.EntityFramework.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var services = new ServiceCollection();
// Add logging
services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
// Register entity and view model types BEFORE building service provider
EntityDbContext.RegisterAssembly(typeof(Program).Assembly);
ViewModelDbContext.RegisterAssembly(typeof(Program).Assembly);
// Configure SourceFlow with automatic discovery
services.UseSourceFlow(typeof(Program).Assembly);
// Add Entity Framework stores with SQL Server
services.AddSourceFlowEfStores(
"Server=localhost;Database=SourceFlow;Integrated Security=true;TrustServerCertificate=true;");
var serviceProvider = services.BuildServiceProvider();
// Initialize databases
var commandContext = serviceProvider.GetRequiredService<CommandDbContext>();
await commandContext.Database.EnsureCreatedAsync();
var entityContext = serviceProvider.GetRequiredService<EntityDbContext>();
await entityContext.Database.EnsureCreatedAsync();
entityContext.ApplyMigrations();
var viewModelContext = serviceProvider.GetRequiredService<ViewModelDbContext>();
await viewModelContext.Database.EnsureCreatedAsync();
viewModelContext.ApplyMigrations();
For PostgreSQL, use AddSourceFlowEfStoresWithCustomProvider(options => options.UseNpgsql(...)). For SQLite, use options.UseSqlite(...).
Define Your Domain
Create your domain entity implementing IEntity:
public class BankAccount : IEntity
{
public int Id { get; set; }
public string AccountName { get; set; }
public decimal Balance { get; set; }
public bool IsClosed { get; set; }
public DateTime CreatedOn { get; set; }
}
Create Commands
Commands represent intent to change state. Always include a parameterless constructor for serialization:
public class CreateAccount : Command<CreateAccountPayload>
{
public CreateAccount() : base() { }
public CreateAccount(CreateAccountPayload payload) : base(payload) { }
}
public class DepositMoney : Command<TransactionPayload>
{
public DepositMoney() : base() { }
public DepositMoney(TransactionPayload payload) : base(payload) { }
}
public class WithdrawMoney : Command<TransactionPayload>
{
public WithdrawMoney() : base() { }
public WithdrawMoney(TransactionPayload payload) : base(payload) { }
}
Define Events
Events record what happened — they're immutable facts:
public class AccountCreated : Event<BankAccount>
{
public AccountCreated(BankAccount payload) : base(payload) { }
}
public class MoneyDeposited : Event<BankAccount>
{
public MoneyDeposited(BankAccount payload) : base(payload) { }
}
public class MoneyWithdrawn : Event<BankAccount>
{
public MoneyWithdrawn(BankAccount payload) : base(payload) { }
}
Implement Saga
Sagas handle commands, update entities, and raise events:
public class AccountSaga : Saga<BankAccount>,
IHandles<CreateAccount>,
IHandles<DepositMoney>,
IHandles<WithdrawMoney>
{
public async Task Handle(CreateAccount command)
{
var account = new BankAccount
{
Id = command.Payload.Id,
AccountName = command.Payload.AccountName,
Balance = command.Payload.InitialAmount,
CreatedOn = DateTime.UtcNow
};
await repository.Persist(account);
await Raise(new AccountCreated(account));
}
public async Task Handle(DepositMoney command)
{
var account = await repository.Get<BankAccount>(command.Payload.Id);
account.Balance += command.Payload.Amount;
await repository.Persist(account);
await Raise(new MoneyDeposited(account));
}
public async Task Handle(WithdrawMoney command)
{
var account = await repository.Get<BankAccount>(command.Payload.Id);
if (account.Balance < command.Payload.Amount)
throw new InvalidOperationException("Insufficient funds");
account.Balance -= command.Payload.Amount;
await repository.Persist(account);
await Raise(new MoneyWithdrawn(account));
}
}
Create Aggregate
Aggregates expose business operations and send commands:
public interface IAccountAggregate : IAggregate
{
void CreateAccount(int id, string holder, decimal amount);
void Deposit(int id, decimal amount);
void Withdraw(int id, decimal amount);
}
public class AccountAggregate : Aggregate<BankAccount>, IAccountAggregate
{
public void CreateAccount(int id, string holder, decimal amount)
{
Send(new CreateAccount(new CreateAccountPayload
{
Id = id, AccountName = holder, InitialAmount = amount
}));
}
public void Deposit(int id, decimal amount)
=> Send(new DepositMoney(new TransactionPayload { Id = id, Amount = amount }));
public void Withdraw(int id, decimal amount)
=> Send(new WithdrawMoney(new TransactionPayload { Id = id, Amount = amount }));
}
Build Read Models
Views project events into optimized read models:
public class AccountView : View,
IProjectOn<AccountCreated>,
IProjectOn<MoneyDeposited>,
IProjectOn<MoneyWithdrawn>
{
public async Task Apply(AccountCreated @event)
{
var view = new AccountViewModel
{
Id = @event.Payload.Id,
AccountName = @event.Payload.AccountName,
CurrentBalance = @event.Payload.Balance,
CreatedDate = @event.Payload.CreatedOn
};
await provider.Push(view);
}
public async Task Apply(MoneyDeposited @event)
{
var view = await provider.Find<AccountViewModel>(@event.Payload.Id);
view.CurrentBalance = @event.Payload.Balance;
view.TransactionCount++;
await provider.Push(view);
}
public async Task Apply(MoneyWithdrawn @event)
{
var view = await provider.Find<AccountViewModel>(@event.Payload.Id);
view.CurrentBalance = @event.Payload.Balance;
view.TransactionCount++;
await provider.Push(view);
}
}
Run It
Now use your aggregate to execute business operations:
// Use the aggregate
var factory = serviceProvider.GetRequiredService<IAggregateFactory>();
var account = await factory.Create<IAccountAggregate>();
account.CreateAccount(1, "John Doe", 1000m);
account.Deposit(1, 500m);
account.Withdraw(1, 200m);
// Give async processing time to complete
await Task.Delay(500);
// Query the read model
var viewStore = serviceProvider.GetRequiredService<IViewModelStoreAdapter>();
var accountView = await viewStore.Find<AccountViewModel>(1);
Console.WriteLine($"Account: {accountView.AccountName}");
Console.WriteLine($"Balance: {accountView.CurrentBalance:C}");
Console.WriteLine($"Transactions: {accountView.TransactionCount}");
The aggregate sent commands → the saga handled them, persisted entities, and raised events → the view projected events into a read model. All with full audit trail and replay capability!
Next Steps
- 📖 Full Documentation — Deep dive into all components
- 🏗️ Architecture Guide — Understand the component interactions
- ☁️ Cloud Integration — Add AWS SQS/SNS messaging
- 📚 Companion Book — 32 chapters of hands-on examples
- 💻 Sample Code — Complete working examples