by Rasmus Mikkelsen
https://github.com/rasmus
ID | FullName | Age |
1 |
rasmus mikkelsen |
21 |
{
"id": 1,
"fullName": "rasmus",
"age": 21
}
{
"id": 1,
"fullName": "rasmus",
"age": 23 // age edited
}
{
"id": 1,
"fullName": "rasmus",
"age": 25 // age edited again
}
ID | Version | Event | Data |
1 |
1 |
Created |
fullName: rasmus m
|
1 |
2 |
NewAge |
age: 23 |
1 |
3 |
NewAge |
age: 25 |
ID
and Version
form an unique keyVersion
specifies the order of events
{
"id": 1,
"version": 1,
"type": "Created",
"meta" : {
"ip": "147.29.150.82",
"via": "browser"
},
"event:" {
"fullName": "rasmus mikkelsen",
"age": 21
}
}
{
"id": 1,
"version": 2,
"type": "NewAge",
"meta" : {
"ip": "147.29.150.82",
"via": "browser"
},
"event": {
"age": 23
}
}
{
"id": 1,
"version": 3,
"type": "NewAge",
"meta" : {
"ip": "103.228.53.155",
"via": "mobile-api"
},
"event": {
"age": 25
}
}
ID
and Version
form a unique keyWill be illustrated shortly
Will be illustrated next slide
public class CreateUserCommand : Command<UserAggregate, UserId>
{
public string FullName { get; }
public int Age { get; }
public CreateUserCommand(
UserId aggregateId,
string fullName, int age)
: base(aggregateId)
{
FullName = fullName;
Age = age;
}
}
public class CreateUserCommandHandler :
ICommandHandler<UserAggregate,UserId,
IExecutionResult,CreateUserCommand>
{
public Task<IExecutionResult> ExecuteCommandAsync(
UserAggregate aggregate,
CreateUserCommand command,
CancellationToken cancellationToken)
{
var result = aggregate.Create(
command.FullName, command.Age);
return Task.FromResult(result);
}
}
public class UserId : Identity<UserId>
{
public UserId(string value) : base(value) {}
}
public class UserAggregate : AggregateRoot<UserAggregate, UserId>
{
// Public to do easy/lazy testing
public string? FullName { get; private set; }
public int? Age { get; private set; }
public UserAggregate(UserId id) : base(id) { }
// We'll fill in the rest later
}
public class UserAggregate : AggregateRoot<UserAggregate, UserId>
{
// ...
public IExecutionResult Create(string fullName, int age)
{
if (age < 13)
return ExecutionResult.Failed("Too young");
Emit(new CreatedEvent(fullName, age), GetMetadata());
return ExecutionResult.Success();
}
}
Emit
zero or or more
[EventVersion("Created", 1)]
public class CreatedEvent : AggregateEvent<UserAggregate, UserId>
{
public string FullName { get; }
public int Age { get; }
public CreatedEvent(
string fullName,
int age)
{
FullName = fullName;
Age = age;
}
}
Apply
method
public class UserAggregate : AggregateRoot<UserAggregate, UserId>
{
// ...
public void Apply(CreatedEvent e)
{
FullName = e.FullName;
Age = e.Age;
}
}
Emit
Apply
for every event type
// Create initial input
var (userId, fullName, age) = (UserId.New, "rasmus mikkelsen", 21);
// Send command via command bus
var command = new CreateUserCommand(userId, fullName, age);
var executionResult = await _commandBus.PublishAsync(command);
// executionResult.IsSuccess == true;
// Fetch aggregate directly
var userAggregate = await _aggregateStore.LoadAsync<UserAggregate>(
userId);
// userAggregate.FullName == "rasmus mikkelsen";
// userAggregate.Age == 21;
public class UpdateReadModelWithCreatedUser :
ISubscribeSynchronousTo<UserAggregate, UserId, CreatedEvent>
{
public Task HandleAsync(
IDomainEvent<UserAggregate, UserId, CreatedEvent> domainEvent,
CancellationToken cancellationToken)
{
// Do awesome update of read model here \o/ ... the easy
// solution is to simply read the aggregate and map it to a
// read model. Remember to take the version into account!
return Task.CompletedTask;
}
}
... for when changes to the domain happen.
[EventVersion("Created", 2)]
public class CreatedEventV2 : AggregateEvent<UserAggregate, UserId>
{
public string FirstName { get; }
public string LastName { get; }
public int Age { get; }
public CreatedEvent(
string firstName, string lastName, int age)
{
FullName = fullName; LastName = lastName; Age = age;
}
}
public class UserCreatedEventUpgrader : IEventUpgrader<UserAggregate, UserId>
{
public IEnumerable<IDomainEvent<UserAggregate, UserId>> Upgrade(
IDomainEvent<UserAggregate, UserId> domainEvent)
{
var createdEvent = domainEvent as IDomainEvent<UserAggregate, UserId, CreatedEvent>;
if (createdEvent == null) {
yield return domainEvent;
yield return break;
}
var nameParts = createdEvent.FullName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var createdEventV2 = _domainEventFactory.Upgrade<UserAggregate, UserId>(
domainEvent,
new CreatedEventV2(
nameParts[0],
nameParts[1],
createdEvent.Age
));
yield return createdEventV2;
}
}
public class UserAggregate : AggregateRoot<UserAggregate, UserId>
{
// Public to do easy/lazy testing
public string? FirstName { get; private set; }
public string? LastName { get; private set; }
public int? Age { get; private set; }
public UserAggregate(UserId id) : base(id) { }
// We'll fill in the rest later
}
Apply
method
public class UserAggregate : AggregateRoot<UserAggregate, UserId>
{
// ...
// We delete the old Apply method for the old event
public void Apply(CreatedEventV2 e)
{
FirstName = e.FirstName;
LastName = e.LastName;
Age = e.Age;
}
}
public class UserAggregate : AggregateRoot<UserAggregate, UserId>
{
public IExecutionResult Create(
string firstName, string lastName, int age)
{
if (age < 13)
return ExecutionResult.Failed("Too young");
Emit(new CreatedEventV2(firstName, lastName, age), GetMeta());
return ExecutionResult.Success();
}
}
... it leads to pain and suffering... sometimes.
... you are absolutely sure