Build your first aggregate
This guide creates a small counter aggregate using SQLite. It uses the same aggregate and repository APIs that a PostgreSQL application uses.
Define the event and aggregate
Section titled “Define the event and aggregate”Events are immutable application-owned values. Give every persisted event a stable name that is independent of its CLR type and a positive schema version.
using EventLoom;
public sealed class Counter(Guid id) : Aggregate<Guid>(id){ public int Value { get; private set; }
public void Increment(int amount) { if (amount <= 0) { throw new ArgumentOutOfRangeException(nameof(amount)); }
Raise(new CounterIncremented(amount)); }
private void Apply(CounterIncremented @event) => Value += @event.Amount;}
[EventType("counter.incremented", Version = 1)]public sealed record CounterIncremented(int Amount) : IDomainEvent<Counter>;The generic event contract links the event to its owning aggregate. EventLoom’s
analyzer verifies that Counter has exactly one correctly shaped
Apply(CounterIncremented) method and that another aggregate cannot raise this
event. An aggregate changes state only by raising an event. Raise applies the event
immediately and stores it in PendingEvents; replay applies persisted history
without adding pending events. An Apply method must be private or protected,
take exactly one event type, and return void.
Configure services
Section titled “Configure services”Register each event explicitly, configure repository identity once, and select SQLite:
services .AddEventLoom() .UseSqlite("Data Source=eventloom.db") .AddEvent<CounterIncremented>() .AddAggregate<Counter, Guid>(aggregate => aggregate .ConstructWith(id => new Counter(id)) .UseStream("counter", id => id.ToString("D")));Single-tenancy is the default and uses the stable internal tenant ID default.
Applications that need tenant isolation opt in with
UseMultiTenancy<TAccessor>(); see
Tenancy and ordering.
Create the schema for local development
Section titled “Create the schema for local development”For a local ASP.NET Core prototype, explicitly initialize a new database:
if (app.Environment.IsDevelopment()){ await app.InitializeEventLoomDevelopmentDatabaseAsync();}The helper refuses to run outside Development and does not migrate an existing schema. Use reviewed, host-owned EF Core migrations in production. See Production deployment.
Save and reload
Section titled “Save and reload”The configured repository resolves the scoped tenant and derives the stream ID and aggregate type:
var counter = new Counter(Guid.NewGuid());counter.Increment(3);
await repository.SaveAsync( counter, new EventMetadata(Actor: "counter-api"), appendId: "increment-command-42");
var reloaded = await repository.LoadAsync(counter.Id);Console.WriteLine(reloaded.Value); // 3appendId is optional. Supply a stable caller-owned command identifier when
retrying after an ambiguous transport failure; do not generate a new value for
each retry. See Append and read events for expected
versions, metadata, and explicit administrative operations.