A small framework for implementing Aggregate Roots, which are a critical part of Event Sourcing, DDD and CQRS, backed by an event store.
Provides connectors for Event Store and MongoDB.
Package | NuGet | |
---|---|---|
Aggregail | ||
Aggregail.Newtonsoft.Json | ||
Aggregail.System.Text.Json | ||
Aggregail.Testing | ||
Aggregail.MongoDB | ||
Aggregail.MongoDB.Admin | ||
Aggregail.EventStore | WIP |
A practical example
The aggregate root in our example will be a goat. Every aggregate needs at least one event to create it,
let's call that event GoatCreated
:
public class GoatCreated
{
public static readonly EventType<GoatCreated> EventType = "GoatCreated";
public string Name { get; set; }
}
It wouldn't be much of an example if we could not update the Aggregate Root after creation, so let's
have an event for that GoatUpdated
:
public class GoatUpdated
{
public static readonly EventType<GoatUpdated> EventType = "GoatUpdated";
public string Name { get; set; }
}
Our goat itself is an example of the minimum amount of code required to make a somewhat interesting Aggregate Root:
public class Goat : AbstractAggregate<Guid, Goat>
{
static Goat()
{
Configuration = new AggregateConfiguration<Guid, Goat>("goat", Guid.Parse)
.Constructs(GoatCreated.EventType, (id, e) => new Goat(id, e))
.Applies(GoatUpdated.EventType, (goat, e) => goat.Apply(e));
}
public static Goat Create(Guid id, string name) =>
Create(GoatCreated.EventType, new GoatCreated { Name = name }, (id, e) => new Goat(id, e));
private Goat(Guid id, GoatCreated e) : base(id)
{
Name = e.Name;
}
public string Name { get; private set; }
public void Update(string name) =>
Append(GoatUpdated.EventType, new GoatUpdated { Name = name }, Apply);
private void Apply(GoatUpdated e)
{
Name = e.Name;
}
}
The code above defines two commands and two events, and configures the applicator
(or constructor) for each event type for the Aggregate Root Goat
.
Command | Event | Applicator |
---|---|---|
static Goat Create(Guid id, string name) |
GoatCreated |
Goat(Guid id, GoatCreated e) |
void Update(string name) |
GoatUpdated |
void Apply(GoatUpdated e) |
public class CreateGoatRequest
{
public Guid Id { get; set; }
public string Name { get; set; }
}
[Route("goats")]
public class GoatsController
{
private readonly IEventStore _store;
public GoatsController(IEventStore store)
{
_store = store;
}
[HttpPost]
public async Task<IActionResult> CreateAsync(CreateGoatRequest request)
{
var goat = Goat.Create(request.Id, request.Name);
await gota.CommitAsync(_store);
return Created();
}
}
[Route("goats")]
public class GoatsController
{
private readonly IEventStore _store;
public GoatsController(IEventStore store)
{
_store = store;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(Guid id)
{
var goat = await Goat.FromAsync(_store, id);
if (goat == null)
{
return NotFound();
}
return Ok(goat);
}
}
public class UpdateGoatRequest
{
public string Name { get; set; }
}
[Route("goats")]
public class GoatsController
{
private readonly IEventStore _store;
public GoatsController(IEventStore store)
{
_store = store;
}
[HttpPatch("{id}")]
public async Task<IActionResult> UpdateAsync(Guid id, UpdateGoatRequest request)
{
var goat = await Goat.FromAsync(_store, id);
if (goat == null)
{
return NotFound();
}
goat.Update(request.Name);
await goat.CommitAsync(_store);
return Ok();
}
}
Deleting Aggregate Roots, and thus streams is currently not supported by Aggregail, but is actively being worked upon. We know it is a vital part in supporting several use cases due to GDPR. If using MongoDB as an event store, streams can easily be deleted without the use of Aggregail:
db.aggregail.deleteMany({ stream: "goat-a4a4e832-f577-4461-a50c-d9c83342ee6f" })
Likewise, when using Event Store, the streams can be deleted using the IEventStoreConnection
, or using the UI.