Skip to content

Getting Started ​

Build an event-sourced system in five minutes. We'll create a library that tracks books — adding them, borrowing, returning, and reporting lost copies.

Install ​

bash
dotnet add package Nagare
dotnet add package Nagare.Sqlite      # SQLite — ideal for development

For production, swap in the database you use:

bash
dotnet add package Nagare.SqlServer   # SQL Server
dotnet add package Nagare.PostgreSql  # PostgreSQL

Define your types ​

An event-sourced system has three core types: commands (what you want to happen), events (what happened), and state (what's true right now).

Commands — imperative mood ​

csharp
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(AddBook), "add")]
[JsonDerivedType(typeof(BorrowBook), "borrow")]
[JsonDerivedType(typeof(ReturnBook), "return")]
[JsonDerivedType(typeof(ReportLost), "report-lost")]
public abstract record BookCommand;

public record AddBook(string Title, string Author, string Isbn) : BookCommand;
public record BorrowBook(string BorrowerId) : BookCommand;
public record ReturnBook() : BookCommand;
public record ReportLost() : BookCommand;

Events — past tense ​

csharp
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(BookAdded), "book-added")]
[JsonDerivedType(typeof(BookBorrowed), "book-borrowed")]
[JsonDerivedType(typeof(BookReturned), "book-returned")]
[JsonDerivedType(typeof(BookLost), "book-lost")]
public abstract record BookEvent : IAggregateEvent<BookEvent>, IJsonable;

public record BookAdded(string Title, string Author, string Isbn) : BookEvent;
public record BookBorrowed(string BorrowerId, DateTimeOffset BorrowedAt) : BookEvent;
public record BookReturned(DateTimeOffset ReturnedAt) : BookEvent;
public record BookLost(DateTimeOffset ReportedAt) : BookEvent;

State — derived from events ​

csharp
public record BookState(
    bool Exists,
    string? Title,
    string? BorrowerId,
    bool IsBorrowed,
    bool IsLost) : IAggregateState<BookState>
{
    public static BookState Default => new(false, null, null, false, false);
}

State is minimal — only what command handlers need to make decisions.

Write the aggregate ​

The aggregate is where commands and events meet. Command handlers decide, event handlers evolve state.

csharp
public class BookAggregate : Aggregate<BookCommand, BookEvent, BookState>
{
    protected override EventHandlers<BookEvent, BookState> RegisterEventHandlers() =>
        Events
            .On<BookAdded>((state, e) =>
                state with { Exists = true, Title = e.Title })
            .On<BookBorrowed>((state, e) =>
                state with { IsBorrowed = true, BorrowerId = e.BorrowerId })
            .On<BookReturned>((state, _) =>
                state with { IsBorrowed = false, BorrowerId = null })
            .On<BookLost>((state, _) =>
                state with { IsLost = true, IsBorrowed = false, BorrowerId = null })
            .Build();

    protected override CommandHandlers<BookCommand, BookEvent, BookState> RegisterCommandHandlers() =>
        Commands
            .On<AddBook>((state, cmd) =>
                state.Exists
                    ? Then.Reject("Book already exists")
                    : Then.Persist(new BookAdded(cmd.Title, cmd.Author, cmd.Isbn)))
            .On<BorrowBook>((state, cmd) =>
                !state.Exists      ? Then.Reject("Book does not exist")
                : state.IsBorrowed ? Then.Reject("Book is already borrowed")
                : state.IsLost     ? Then.Reject("Book is reported lost")
                : Then.Persist(new BookBorrowed(cmd.BorrowerId, DateTimeOffset.UtcNow)))
            .On<ReturnBook>((state, _) =>
                !state.IsBorrowed
                    ? Then.Reject("Book is not borrowed")
                    : Then.Persist(new BookReturned(DateTimeOffset.UtcNow)))
            .On<ReportLost>((state, _) =>
                !state.Exists  ? Then.Reject("Book does not exist")
                : state.IsLost ? Then.Accept()   // idempotent
                : Then.Persist(new BookLost(DateTimeOffset.UtcNow)))
            .Build();
}

Read the command handlers top to bottom — they're business rules written as code. "If the book doesn't exist, reject. If it's already borrowed, reject. If it's lost, reject. Otherwise, persist a borrow event."

Wire it up ​

csharp
using Nagare;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddNagare();
builder.Services.AddAggregate<BookAggregate, BookCommand, BookEvent, BookState>();
builder.Services.AddNagareSqliteStorage(connectionString: "Data Source=nagare.db");

var app = builder.Build();

Expose an endpoint ​

csharp
app.MapPost("/books/{id}", async (
    string id,
    BookCommand command,
    IAggregateRepository<BookAggregate, BookCommand, BookEvent, BookState> repo) =>
{
    var aggregate = await repo.Load(new AggregateId(id));
    var reply = await aggregate.Ask(command);

    return reply.Match(
        onAccepted: () => Results.Ok(),
        onRejected: ex => Results.BadRequest(ex.Message),
        onIgnored: () => Results.Ok());
});

app.Run();

Try it ​

bash
# Add a book
curl -X POST http://localhost:5000/books/dune-1 \
  -H "Content-Type: application/json" \
  -d '{"$type": "add", "Title": "Dune", "Author": "Frank Herbert", "Isbn": "978-0441172719"}'

# Borrow it
curl -X POST http://localhost:5000/books/dune-1 \
  -H "Content-Type: application/json" \
  -d '{"$type": "borrow", "BorrowerId": "user-42"}'

# Try to borrow it again — rejected
curl -X POST http://localhost:5000/books/dune-1 \
  -H "Content-Type: application/json" \
  -d '{"$type": "borrow", "BorrowerId": "user-99"}'
# → 400 "Book is already borrowed"

Events flow in, truth flows out. The event store now contains the complete history of everything that happened to this book.

Test it ​

bash
dotnet add package Nagare.Testing
csharp
var harness = new AggregateTestHarness<BookAggregate, BookCommand, BookEvent, BookState>();

harness
    .Given(new BookAdded("Dune", "Frank Herbert", "978-0441172719"))
    .When(new BorrowBook("user-42"))
    .ThenAccepted()
    .ThenExpect<BookBorrowed>(e => e.BorrowerId == "user-42");

harness
    .Given(new BookAdded("Dune", "Frank Herbert", "978-0441172719"),
           new BookBorrowed("user-1", DateTimeOffset.UtcNow))
    .When(new BorrowBook("user-2"))
    .ThenRejected("Book is already borrowed");

No database, no DI container — just your domain logic, verified in milliseconds.

What's next ​

You've built a working event-sourced system. From here:

  • Core Concepts — understand the philosophy behind the flow
  • Aggregates — design consistency boundaries that last
  • Projections — build read models from the event stream
  • Testing — the natural Given-When-Then vocabulary
  • Modular Monolith — scale from single process to distributed services

流れ — flow.