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:

Terminal
# 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:

C# — 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();
💡 Tip

For PostgreSQL, use AddSourceFlowEfStoresWithCustomProvider(options => options.UseNpgsql(...)). For SQLite, use options.UseSqlite(...).

Define Your Domain

Create your domain entity implementing IEntity:

C# — BankAccount.cs
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:

C# — Commands.cs
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:

C# — Events.cs
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:

C# — AccountSaga.cs
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:

C# — AccountAggregate.cs
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:

C# — AccountView.cs
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:

C# — Program.cs (continued)
// 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}");
📌 What Just Happened?

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