Implementing CQRS and Event
Sourcing with RavenDB
Elemar Júnior
@elemarjr
elemarjr@ravendb.net
elemarjr.com
https://github.com/elemarjr/cqrses-demo
quick-review
CQRS means "Command-query
responsibility segregation"
We segregate the responsibilit
y between commands (write
requests) and queries (read
requests).
Supporting schema-free
document databases, RavenDB
is an excellent option to be
used as Query Stack data store
tool.
A document could easily
consolidate all the necessary
data needed by the
presentation layer in an
elegant and organized way.
Even when using a single
database for the “command
stack” and “query stack”,
RavenDB databases are a
good option.
Indexes and transformers are
excellent alternatives to
provide data for the Query
Stack
What is Event Sourcing?
Storing all the changes (events)
to the system, rather than just
its current state.
This technique allows us to recover
the status of any entity/aggregate
simply "replaying" the changes
recorded in the events.
apply(aggregateState, domainEvent) ->
newAggregateState
apply(aggregateState, domainEvent) =>
match domainEvent with
EventType1 -> …
EventType2 -> …
EventType2 -> …
f(s, e) -> s’
f(f(s, e’), e’’) -> a’’
f(f(f(s, e’), e’’), e’’’) -> s’’’
foldl (apply) s ne -> s’
Is event sourcing a requirement
to do CQRS?
No! You can save your
aggregates in any form you
like.
#1 – Using “event-sourcing agnostic”
anemic model and transaction scripts
DISCLAIMER:
All the code in following samples is
NOT intended to be in any way
production
DISCLAIMER:
All the code in following samples is
NOT intended to be a definitive
solution
DISCLAIMER:
All the code in following samples is
NOT the best implementation neither
the correct one
public class Employee
{
public EmployeeId Id { get; private set; }
public FullName Name { get; private set; }
public Address HomeAddress { get; private set; }
public decimal Salary { get; private set; }
public Employee(EmployeeId id, FullName name, Address homeAddress, decimal salary)
{
Id = id;
Name = name;
HomeAddress = homeAddress;
Salary = salary;
}
}
public sealed class EmployeeId
{
private readonly string _value;
private EmployeeId(string value)
{
Throw.IfArgumentIsNullOrEmpty(value, nameof(value));
_value = value;
}
public static implicit operator string(EmployeeId source) { return source._value; }
public static implicit operator EmployeeId(string source)
{ return new EmployeeId(source);}
public override string ToString()
{ return _value; }
public override bool Equals(object obj)
{
var other = obj as EmployeeId;
if (other == null) return false;
return (other._value == _value);
}
public override int GetHashCode()
{ return _value.GetHashCode(); }
}
public sealed class FullName
{
public string GivenName { get; }
public string Surname { get; }
public FullName(string givenName, string surname
)
{
Throw.IfArgumentIsNullOrEmpty(givenName, nameof(givenName));
Throw.IfArgumentIsNullOrEmpty(surname, nameof(surname));
GivenName = givenName;
Surname = surname;
}
public override string ToString()
{
return $"{GivenName} {Surname}";
}
public override bool Equals(object obj)
{//**//}
public override int GetHashCode()
{//**//}
}
}
internal class EmployeeIdConverter : ITypeConverter
{
public bool CanConvertFrom(Type sourceType)
{
return sourceType == typeof (EmployeeId);
}
public string ConvertFrom(string tag, object value, bool allowNull)
{
return $"{tag}{value}";
}
public object ConvertTo(string value)
{
return value.IndexOf("/", StringComparison.Ordinal) != -1
? (EmployeeId) value.Split('/')[1]
: (EmployeeId) value;
}
}
Instance.Conventions.IdentityTypeConvertors.Add(
new EmployeeIdConverter()
);
public sealed class BrazilianAddress : Address
{
public string StreetName { get; private set; }
public int Number { get; private set; }
public string AdditionalInfo { get; private set; }
public string Neighborhood { get; private set; }
public string City { get; private set; }
public string State { get; private set; }
public string PostalCode { get; private set; }
public override string Country => "Brazil";
...
Serializer = Instance.Conventions.CreateSerializer();
Serializer.TypeNameHandling = TypeNameHandling.All;
public abstract class EmployeeCommand : Command
{
public EmployeeId Id { get; private set; }
protected EmployeeCommand(EmployeeId id)
{
Id = id;
}
}
public abstract class Command : Message
{}
public abstract class Message
{
public string MessageType { get; private set; }
protected Message()
{
MessageType = GetType().Name;
}
}
public sealed class RegisterEmployeeCommand
: EmployeeCommand
{
public FullName Name { get; }
public decimal InitialSalary { get; }
public RegisterEmployeeCommand(
EmployeeId id,
FullName name,
decimal initialSalary
) : base(id)
{
Name = name;
InitialSalary = initialSalary;
}
}
public class RegisterEmployeeHandler
: IMessageHandler<RegisterEmployeeCommand>
{
private readonly IBus _bus;
private readonly IEmployeeRepository _repository;
private readonly ILogger _logger;
public RegisterEmployeeHandler(IBus bus, IEmployeeRepository repository, ILogger logger)
{
_bus = bus;
_repository = repository;
_logger = logger;
}
public void Handle(RegisterEmployeeCommand message)
{
if (!_repository.IsRegistered(message.Id))
{
_repository.CreateEmployee(message.Id, message.Name, message.InitialSalary);
_bus.RaiseEvent(
new EmployeeRegisteredEvent(
message.Id,
message.Name,
message.InitialSalary
)
);
}
else
{
_bus.RaiseEvent(new FailedToRegisterEmployeeEvent(message.Id));
}
}
}
public bool IsRegistered(EmployeeId id)
{
var lid = $"employees/{id}";
return DocumentStoreHolder
.Instance
.DatabaseCommands.Head(lid) != null;
}
public void CreateEmployee(EmployeeId id, FullName name, decimal initialSalary)
{
using (var session = DocumentStoreHolder.Instance.OpenSession())
{
var employee = new Employee(id, name, Address.NotInformed, initialSalary);
session.Store(employee);
session.SaveChanges();
}
}
public abstract class Event : Message
{
public DateTime Timestamp { get; private set; }
protected Event()
{
Timestamp = DateTime.Now;
}
}
public sealed class EmployeeRegisteredEvent : EmployeeEvent
{
public FullName Name { get; private set; }
public decimal InitialSalary { get; private set; }
public EmployeeRegisteredEvent(
EmployeeId employeeId,
FullName name,
decimal initialSalary
) : base(employeeId)
{
Name = name;
InitialSalary = initialSalary;
}
}
public sealed class RaiseEmployeeSalaryCommand
: EmployeeCommand
{
public decimal Amount { get; }
public RaiseEmployeeSalaryCommand(
EmployeeId id,
decimal amount
) : base(id)
{
Amount = amount;
}
}
public void RaiseSalary(EmployeeId id, decimal amount)
{
DocumentStoreHolder.Instance.DatabaseCommands.Patch($"employees/{id}",
new ScriptedPatchRequest
{
Script = $"this.Salary += {amount.ToInvariantString()};"
});
}
public sealed class UpdateEmployeeHomeAddressCommand :
EmployeeCommand
{
public Address HomeAddress { get; }
public UpdateEmployeeHomeAddressCommand(
EmployeeId id,
Address address) : base(id)
{
HomeAddress = address;
}
}
public void UpdateHomeAddress(EmployeeId id, Address homeAddress)
{
var ro = RavenJObject.FromObject(homeAddress, DocumentStoreHolder.Serializer);
DocumentStoreHolder.Instance.DatabaseCommands.Patch($"employees/{id}", new[]
{
new PatchRequest
{
Type = PatchCommandType.Set,
Name = "HomeAddress",
Value = ro
}
});
}
Serializer = Instance.Conventions.CreateSerializer();
Serializer.TypeNameHandling = TypeNameHandling.All;
public partial class RavenDbEmployeeEventStore :
IMessageHandler<EmployeeRegisteredEvent>,
IMessageHandler<EmployeeHomeAddressUpdatedEvent>,
IMessageHandler<EmployeeSalaryRaisedEvent>
{
public void HandleInternal(Message message)
{
using (var session = DocumentStoreHolder.Instance.OpenSession())
{
session.Store(message);
session.SaveChanges();
}
}
public void Handle(EmployeeRegisteredEvent message)
{
HandleInternal(message);
}
public void Handle(EmployeeHomeAddressUpdatedEvent message)
{
HandleInternal(message);
}
public void Handle(EmployeeSalaryRaisedEvent message)
{
HandleInternal(message);
}
}
var defaultImpl = Instance.Conventions.FindTypeTagName;
Instance.Conventions.FindTypeTagName = t => t.IsSubclassOf(typeof(EmployeeEvent))
? "EmployeeEvents"
: defaultImpl(t);
public class EventsPerEmployeeResult
{
public EmployeeId EmployeeId { get; set; }
public int NumberOfEvents { get; set; }
}
private class EventsPerEmployeeIndex
: AbstractIndexCreationTask<EmployeeEvent, EventsPerEmployeeResult>
{
public override string IndexName => "EmployeeEvents/Summary";
public EventsPerEmployeeIndex()
{
Map = (events) =>
from e in events
select new EventsPerEmployeeResult
{
EmployeeId = e.EmployeeId,
NumberOfEvents = 1
};
Reduce = (inputs) =>
from input in inputs
group input by input.EmployeeId into g
select new EventsPerEmployeeResult
{
EmployeeId = g.Key,
NumberOfEvents = g.Sum(x => x.NumberOfEvents)
};
}
}
public class SalaryPerEmployeeResult
{
public EmployeeId EmployeeId { get; set; }
public string FullName { get; set; }
public decimal Salary { get; set; }
}
private class SalaryPerEmployeeIndex
: AbstractMultiMapIndexCreationTask<SalaryPerEmployeeResult>
{
public override string IndexName => "EmployeeEvents/SalaryPerEmployee";
public SalaryPerEmployeeIndex()
{
AddMap<EmployeeSalaryRaisedEvent>(events =>
from e in events
where e.MessageType == "EmployeeSalaryRaisedEvent"
select new
{ e.EmployeeId, FullName = "", Salary = e.Amount});
AddMap<EmployeeRegisteredEvent>(events =>
from e in events
where e.MessageType == "EmployeeRegisteredEvent"
select new
{e.EmployeeId, FullName = e.Name.GivenName + " " + e.Name.Surname,
Salary = e.InitialSalary});
Reduce = inputs =>
from input in inputs
group input by input.EmployeeId
into g
select new SalaryPerEmployeeResult()
{
EmployeeId = g.Key, FullName = g.Aggregate("", (a, b) => b.FullName != "" ? b.FullName : a),
Salary = g.Sum(x => x.Salary)
};
}
}
#2 – Using a “Event-Sourcing friendly”
domain model
public class EmployeeCommandsHandler : IMessageHandler<RegisterEmployeeCommand>,
IMessageHandler<RaiseSalaryCommand>, IMessageHandler<ChangeHomeAddressCommand>
{
private readonly IEmployeeRepository _repository;
public EmployeeCommandsHandler(IEmployeeRepository repository)
{ _repository = repository; }
public void Handle(RegisterEmployeeCommand command)
{
var newEmployee = new Employee(command.Id, command.Name, command.InitialSalary);
_repository.Save(newEmployee);
}
public void Handle(RaiseSalaryCommand command)
{
var employee = _repository.Load(command.EmployeeId);
employee.RaiseSalary(command.Amount);
_repository.Save(employee);
}
public void Handle(ChangeHomeAddressCommand command)
{
var employee = _repository.Load(command.EmployeeId);
employee.ChangeHomeAddress(command.NewAddress);
_repository.Save(employee);
}
}
public sealed class Employee : EventSourced<Guid>
{
public FullName Name { get; private set; }
public decimal Salary { get; private set; }
public Address HomeAddress { get; private set; }
private Employee(Guid id) : base(id)
{
Handles<EmployeeRegisteredEvent>(OnEmployeeRegistered);
Handles<EmployeeSalaryRaisedEvent>(OnEmployeeSalaryRaised);
Handles<EmployeeHomeAddressChangedEvent>(OnEmployeeHomeAddressChanged);
}
public Employee(Guid id, FullName name, decimal initialSalary)
: this(id)
{
Throw.IfArgumentIsNull(name, nameof(name));
Throw.IfArgumentIsNegative(initialSalary, nameof(initialSalary));
Update(new EmployeeRegisteredEvent(name, initialSalary));
}
public void RaiseSalary(decimal amount)
{
Throw.IfArgumentIsNegative(amount, nameof(amount));
Update(new EmployeeSalaryRaisedEvent(amount));
}
public void ChangeHomeAddress(Address address)
{
Throw.IfArgumentIsNull(address, nameof(address));
Update(new EmployeeHomeAddressChangedEvent(address));
}
private void OnEmployeeRegistered(EmployeeRegisteredEvent @event)
{
Name = @event.Name;
Salary = @event.InitialSalary;
}
private void OnEmployeeSalaryRaised(EmployeeSalaryRaisedEvent @event)
{
Salary += @event.Amount;
}
private void OnEmployeeHomeAddressChanged(EmployeeHomeAddressChangedEvent @event)
{
HomeAddress = @event.NewAddress;
}
protected void Update(VersionedEvent<TId> e)
{
e.SourceId = Id;
e.Version = Version + 1;
_handlers[e.GetType()].Invoke(e);
Version = e.Version;
_pendingEvents.Add(e);
}
public Employee(Guid id, IEnumerable<IVersionedEvent<Guid>> history)
: this(id)
{
LoadFrom(history);
}
protected void LoadFrom(IEnumerable<IVersionedEvent<TId>> pastEvents)
{
foreach (var e in pastEvents)
{
_handlers[e.GetType()].Invoke(e);
Version = e.Version;
}
}
public class EmployeeSalaryRaisedEvent : VersionedEvent<Guid>
{
public decimal Amount { get; }
public EmployeeSalaryRaisedEvent(decimal amout)
{
Amount = amout;
}
}
public class VersionedEvent<TSourceId> : IVersionedEvent<TSourceId>
{
public TSourceId SourceId { get; internal set; }
public DateTime When { get; private set; }
public int Version { get; internal set; }
public VersionedEvent()
{
When = DateTime.Now;
}
}
class EmployeeEvents
{
public EmployeeEvents(Guid id, IEnumerable<IVersionedEvent<Guid>> events)
{
Id = id;
Events = events.ToArray();
}
public Guid Id { get; private set; }
public IVersionedEvent<Guid>[] Events { get; private set; }
}
public Employee Load(Guid id)
{
EmployeeEvents data;
using (var session = _store.OpenSession())
{
data = session.Load<EmployeeEvents>(id);
}
return new Employee(id, data.Events);
}
const string EmployeeEntityVersion =
"Employee-Entity-Version";
private void SaveNewEmployee(Employee employee)
{
using (var session = _store.OpenSession())
{
var document = new EmployeeEvents(
employee.Id,
employee.PendingEvents
);
session.Store(document);
session.Advanced.GetMetadataFor(document)
.Add(EmployeeEntityVersion, employee.Version);
session.SaveChanges();
}
}
private void SaveEmployeeEvents(
Employee employee )
{
var patches = new List<PatchRequest>();
foreach (var evt in employee.PendingEvents)
{
patches.Add(new PatchRequest
{
Type = PatchCommandType.Add,
Name = "Events",
Value = RavenJObject.FromObject(evt, _serializer)
});
}
var localId = $"employees/{employee.Id}";
var addEmployeeEvents = new PatchCommandData()
{
Key = localId,
Patches = patches.ToArray()
};
var updateMetadata = new ScriptedPatchCommandData()
{
Key = localId,
Patch = new ScriptedPatchRequest
{
Script = $"this['@metadata']['{EmployeeEntityVersion}'] = {employee.Version}; "
}
};
_store.DatabaseCommands.Batch(new ICommandData[]
{
addEmployeeEvents,
updateMetadata
});
}
public void Save(Employee employee)
{
var head = GetHead(employee.Id);
var storedVersion = GetStoredVersionOf(head);
if (storedVersion != (employee.Version - employee.PendingEvents.Count()))
throw new InvalidOperationException("Invalid object state.");
if (head == null)
SaveNewEmployee(employee);
else
SaveEmployeeEvents(employee);
foreach (var evt in employee.PendingEvents)
Bus.RaiseEvent(evt);
}
Compilation Extensions =
entry point for more complex
logic, used to calculate value of
the index entry fields.
public static class Employee
{
public static string FullName(dynamic source)
{
var e = source.Events[0];
return $"{e.Name.GivenName} {e.Name.Surname}";
}
...
public static decimal Salary(dynamic source, int atVersion = 0)
{
var result = 0M;
foreach (var evt in source.Events)
{
if (IsEmployeeRegistered(evt))
result += (decimal) evt.InitialSalary;
else if (IsSalaryRaised(evt))
result += (decimal) evt.Amount;
if (atVersion != 0 && evt.Version == atVersion)
break;
}
return result;
}
public class EmployeeDynamicCompilationExtension :
AbstractDynamicCompilationExtension
{
public override string[] GetNamespacesToImport()
{
return new[] {typeof (Employee).Namespace};
}
public override string[] GetAssembliesToReference()
{
return new[] {typeof (Employee).Assembly.Location};
}
}
Done!
elemarjr.com
@elemarjr
linkedin.com/in/elemarjr
elemarjr@ravendb.net
Keep in touch!
Thank you!
Implementing CQRS and Event
Sourcing with RavenDB
Elemar Júnior
@elemarjr
elemarjr@ravendb.net
elemarjr.com

Implementing CQRS and Event Sourcing with RavenDB

  • 1.
    Implementing CQRS andEvent Sourcing with RavenDB Elemar Júnior @elemarjr elemarjr@ravendb.net elemarjr.com
  • 2.
  • 3.
  • 4.
  • 5.
    We segregate theresponsibilit y between commands (write requests) and queries (read requests).
  • 8.
    Supporting schema-free document databases,RavenDB is an excellent option to be used as Query Stack data store tool.
  • 9.
    A document couldeasily consolidate all the necessary data needed by the presentation layer in an elegant and organized way.
  • 10.
    Even when usinga single database for the “command stack” and “query stack”, RavenDB databases are a good option.
  • 11.
    Indexes and transformersare excellent alternatives to provide data for the Query Stack
  • 12.
    What is EventSourcing? Storing all the changes (events) to the system, rather than just its current state.
  • 14.
    This technique allowsus to recover the status of any entity/aggregate simply "replaying" the changes recorded in the events.
  • 15.
  • 16.
    apply(aggregateState, domainEvent) => matchdomainEvent with EventType1 -> … EventType2 -> … EventType2 -> …
  • 17.
  • 18.
  • 19.
    f(f(f(s, e’), e’’),e’’’) -> s’’’
  • 20.
    foldl (apply) sne -> s’
  • 21.
    Is event sourcinga requirement to do CQRS? No! You can save your aggregates in any form you like.
  • 22.
    #1 – Using“event-sourcing agnostic” anemic model and transaction scripts
  • 23.
    DISCLAIMER: All the codein following samples is NOT intended to be in any way production
  • 24.
    DISCLAIMER: All the codein following samples is NOT intended to be a definitive solution
  • 25.
    DISCLAIMER: All the codein following samples is NOT the best implementation neither the correct one
  • 26.
    public class Employee { publicEmployeeId Id { get; private set; } public FullName Name { get; private set; } public Address HomeAddress { get; private set; } public decimal Salary { get; private set; } public Employee(EmployeeId id, FullName name, Address homeAddress, decimal salary) { Id = id; Name = name; HomeAddress = homeAddress; Salary = salary; } }
  • 27.
    public sealed classEmployeeId { private readonly string _value; private EmployeeId(string value) { Throw.IfArgumentIsNullOrEmpty(value, nameof(value)); _value = value; } public static implicit operator string(EmployeeId source) { return source._value; } public static implicit operator EmployeeId(string source) { return new EmployeeId(source);} public override string ToString() { return _value; } public override bool Equals(object obj) { var other = obj as EmployeeId; if (other == null) return false; return (other._value == _value); } public override int GetHashCode() { return _value.GetHashCode(); } }
  • 28.
    public sealed classFullName { public string GivenName { get; } public string Surname { get; } public FullName(string givenName, string surname ) { Throw.IfArgumentIsNullOrEmpty(givenName, nameof(givenName)); Throw.IfArgumentIsNullOrEmpty(surname, nameof(surname)); GivenName = givenName; Surname = surname; } public override string ToString() { return $"{GivenName} {Surname}"; } public override bool Equals(object obj) {//**//} public override int GetHashCode() {//**//} } }
  • 29.
    internal class EmployeeIdConverter: ITypeConverter { public bool CanConvertFrom(Type sourceType) { return sourceType == typeof (EmployeeId); } public string ConvertFrom(string tag, object value, bool allowNull) { return $"{tag}{value}"; } public object ConvertTo(string value) { return value.IndexOf("/", StringComparison.Ordinal) != -1 ? (EmployeeId) value.Split('/')[1] : (EmployeeId) value; } } Instance.Conventions.IdentityTypeConvertors.Add( new EmployeeIdConverter() );
  • 30.
    public sealed classBrazilianAddress : Address { public string StreetName { get; private set; } public int Number { get; private set; } public string AdditionalInfo { get; private set; } public string Neighborhood { get; private set; } public string City { get; private set; } public string State { get; private set; } public string PostalCode { get; private set; } public override string Country => "Brazil"; ... Serializer = Instance.Conventions.CreateSerializer(); Serializer.TypeNameHandling = TypeNameHandling.All;
  • 33.
    public abstract classEmployeeCommand : Command { public EmployeeId Id { get; private set; } protected EmployeeCommand(EmployeeId id) { Id = id; } } public abstract class Command : Message {} public abstract class Message { public string MessageType { get; private set; } protected Message() { MessageType = GetType().Name; } }
  • 34.
    public sealed classRegisterEmployeeCommand : EmployeeCommand { public FullName Name { get; } public decimal InitialSalary { get; } public RegisterEmployeeCommand( EmployeeId id, FullName name, decimal initialSalary ) : base(id) { Name = name; InitialSalary = initialSalary; } }
  • 35.
    public class RegisterEmployeeHandler :IMessageHandler<RegisterEmployeeCommand> { private readonly IBus _bus; private readonly IEmployeeRepository _repository; private readonly ILogger _logger; public RegisterEmployeeHandler(IBus bus, IEmployeeRepository repository, ILogger logger) { _bus = bus; _repository = repository; _logger = logger; } public void Handle(RegisterEmployeeCommand message) { if (!_repository.IsRegistered(message.Id)) { _repository.CreateEmployee(message.Id, message.Name, message.InitialSalary); _bus.RaiseEvent( new EmployeeRegisteredEvent( message.Id, message.Name, message.InitialSalary ) ); } else { _bus.RaiseEvent(new FailedToRegisterEmployeeEvent(message.Id)); } } }
  • 36.
    public bool IsRegistered(EmployeeIdid) { var lid = $"employees/{id}"; return DocumentStoreHolder .Instance .DatabaseCommands.Head(lid) != null; }
  • 37.
    public void CreateEmployee(EmployeeIdid, FullName name, decimal initialSalary) { using (var session = DocumentStoreHolder.Instance.OpenSession()) { var employee = new Employee(id, name, Address.NotInformed, initialSalary); session.Store(employee); session.SaveChanges(); } }
  • 38.
    public abstract classEvent : Message { public DateTime Timestamp { get; private set; } protected Event() { Timestamp = DateTime.Now; } } public sealed class EmployeeRegisteredEvent : EmployeeEvent { public FullName Name { get; private set; } public decimal InitialSalary { get; private set; } public EmployeeRegisteredEvent( EmployeeId employeeId, FullName name, decimal initialSalary ) : base(employeeId) { Name = name; InitialSalary = initialSalary; } }
  • 39.
    public sealed classRaiseEmployeeSalaryCommand : EmployeeCommand { public decimal Amount { get; } public RaiseEmployeeSalaryCommand( EmployeeId id, decimal amount ) : base(id) { Amount = amount; } }
  • 40.
    public void RaiseSalary(EmployeeIdid, decimal amount) { DocumentStoreHolder.Instance.DatabaseCommands.Patch($"employees/{id}", new ScriptedPatchRequest { Script = $"this.Salary += {amount.ToInvariantString()};" }); }
  • 41.
    public sealed classUpdateEmployeeHomeAddressCommand : EmployeeCommand { public Address HomeAddress { get; } public UpdateEmployeeHomeAddressCommand( EmployeeId id, Address address) : base(id) { HomeAddress = address; } }
  • 42.
    public void UpdateHomeAddress(EmployeeIdid, Address homeAddress) { var ro = RavenJObject.FromObject(homeAddress, DocumentStoreHolder.Serializer); DocumentStoreHolder.Instance.DatabaseCommands.Patch($"employees/{id}", new[] { new PatchRequest { Type = PatchCommandType.Set, Name = "HomeAddress", Value = ro } }); } Serializer = Instance.Conventions.CreateSerializer(); Serializer.TypeNameHandling = TypeNameHandling.All;
  • 43.
    public partial classRavenDbEmployeeEventStore : IMessageHandler<EmployeeRegisteredEvent>, IMessageHandler<EmployeeHomeAddressUpdatedEvent>, IMessageHandler<EmployeeSalaryRaisedEvent> { public void HandleInternal(Message message) { using (var session = DocumentStoreHolder.Instance.OpenSession()) { session.Store(message); session.SaveChanges(); } } public void Handle(EmployeeRegisteredEvent message) { HandleInternal(message); } public void Handle(EmployeeHomeAddressUpdatedEvent message) { HandleInternal(message); } public void Handle(EmployeeSalaryRaisedEvent message) { HandleInternal(message); } }
  • 44.
    var defaultImpl =Instance.Conventions.FindTypeTagName; Instance.Conventions.FindTypeTagName = t => t.IsSubclassOf(typeof(EmployeeEvent)) ? "EmployeeEvents" : defaultImpl(t);
  • 45.
    public class EventsPerEmployeeResult { publicEmployeeId EmployeeId { get; set; } public int NumberOfEvents { get; set; } } private class EventsPerEmployeeIndex : AbstractIndexCreationTask<EmployeeEvent, EventsPerEmployeeResult> { public override string IndexName => "EmployeeEvents/Summary"; public EventsPerEmployeeIndex() { Map = (events) => from e in events select new EventsPerEmployeeResult { EmployeeId = e.EmployeeId, NumberOfEvents = 1 }; Reduce = (inputs) => from input in inputs group input by input.EmployeeId into g select new EventsPerEmployeeResult { EmployeeId = g.Key, NumberOfEvents = g.Sum(x => x.NumberOfEvents) }; } }
  • 46.
    public class SalaryPerEmployeeResult { publicEmployeeId EmployeeId { get; set; } public string FullName { get; set; } public decimal Salary { get; set; } }
  • 47.
    private class SalaryPerEmployeeIndex :AbstractMultiMapIndexCreationTask<SalaryPerEmployeeResult> { public override string IndexName => "EmployeeEvents/SalaryPerEmployee"; public SalaryPerEmployeeIndex() { AddMap<EmployeeSalaryRaisedEvent>(events => from e in events where e.MessageType == "EmployeeSalaryRaisedEvent" select new { e.EmployeeId, FullName = "", Salary = e.Amount}); AddMap<EmployeeRegisteredEvent>(events => from e in events where e.MessageType == "EmployeeRegisteredEvent" select new {e.EmployeeId, FullName = e.Name.GivenName + " " + e.Name.Surname, Salary = e.InitialSalary}); Reduce = inputs => from input in inputs group input by input.EmployeeId into g select new SalaryPerEmployeeResult() { EmployeeId = g.Key, FullName = g.Aggregate("", (a, b) => b.FullName != "" ? b.FullName : a), Salary = g.Sum(x => x.Salary) }; } }
  • 48.
    #2 – Usinga “Event-Sourcing friendly” domain model
  • 49.
    public class EmployeeCommandsHandler: IMessageHandler<RegisterEmployeeCommand>, IMessageHandler<RaiseSalaryCommand>, IMessageHandler<ChangeHomeAddressCommand> { private readonly IEmployeeRepository _repository; public EmployeeCommandsHandler(IEmployeeRepository repository) { _repository = repository; } public void Handle(RegisterEmployeeCommand command) { var newEmployee = new Employee(command.Id, command.Name, command.InitialSalary); _repository.Save(newEmployee); } public void Handle(RaiseSalaryCommand command) { var employee = _repository.Load(command.EmployeeId); employee.RaiseSalary(command.Amount); _repository.Save(employee); } public void Handle(ChangeHomeAddressCommand command) { var employee = _repository.Load(command.EmployeeId); employee.ChangeHomeAddress(command.NewAddress); _repository.Save(employee); } }
  • 50.
    public sealed classEmployee : EventSourced<Guid> { public FullName Name { get; private set; } public decimal Salary { get; private set; } public Address HomeAddress { get; private set; } private Employee(Guid id) : base(id) { Handles<EmployeeRegisteredEvent>(OnEmployeeRegistered); Handles<EmployeeSalaryRaisedEvent>(OnEmployeeSalaryRaised); Handles<EmployeeHomeAddressChangedEvent>(OnEmployeeHomeAddressChanged); } public Employee(Guid id, FullName name, decimal initialSalary) : this(id) { Throw.IfArgumentIsNull(name, nameof(name)); Throw.IfArgumentIsNegative(initialSalary, nameof(initialSalary)); Update(new EmployeeRegisteredEvent(name, initialSalary)); }
  • 51.
    public void RaiseSalary(decimalamount) { Throw.IfArgumentIsNegative(amount, nameof(amount)); Update(new EmployeeSalaryRaisedEvent(amount)); } public void ChangeHomeAddress(Address address) { Throw.IfArgumentIsNull(address, nameof(address)); Update(new EmployeeHomeAddressChangedEvent(address)); }
  • 52.
    private void OnEmployeeRegistered(EmployeeRegisteredEvent@event) { Name = @event.Name; Salary = @event.InitialSalary; } private void OnEmployeeSalaryRaised(EmployeeSalaryRaisedEvent @event) { Salary += @event.Amount; } private void OnEmployeeHomeAddressChanged(EmployeeHomeAddressChangedEvent @event) { HomeAddress = @event.NewAddress; }
  • 53.
    protected void Update(VersionedEvent<TId>e) { e.SourceId = Id; e.Version = Version + 1; _handlers[e.GetType()].Invoke(e); Version = e.Version; _pendingEvents.Add(e); }
  • 54.
    public Employee(Guid id,IEnumerable<IVersionedEvent<Guid>> history) : this(id) { LoadFrom(history); } protected void LoadFrom(IEnumerable<IVersionedEvent<TId>> pastEvents) { foreach (var e in pastEvents) { _handlers[e.GetType()].Invoke(e); Version = e.Version; } }
  • 55.
    public class EmployeeSalaryRaisedEvent: VersionedEvent<Guid> { public decimal Amount { get; } public EmployeeSalaryRaisedEvent(decimal amout) { Amount = amout; } } public class VersionedEvent<TSourceId> : IVersionedEvent<TSourceId> { public TSourceId SourceId { get; internal set; } public DateTime When { get; private set; } public int Version { get; internal set; } public VersionedEvent() { When = DateTime.Now; } }
  • 58.
    class EmployeeEvents { public EmployeeEvents(Guidid, IEnumerable<IVersionedEvent<Guid>> events) { Id = id; Events = events.ToArray(); } public Guid Id { get; private set; } public IVersionedEvent<Guid>[] Events { get; private set; } }
  • 59.
    public Employee Load(Guidid) { EmployeeEvents data; using (var session = _store.OpenSession()) { data = session.Load<EmployeeEvents>(id); } return new Employee(id, data.Events); }
  • 60.
    const string EmployeeEntityVersion= "Employee-Entity-Version"; private void SaveNewEmployee(Employee employee) { using (var session = _store.OpenSession()) { var document = new EmployeeEvents( employee.Id, employee.PendingEvents ); session.Store(document); session.Advanced.GetMetadataFor(document) .Add(EmployeeEntityVersion, employee.Version); session.SaveChanges(); } }
  • 61.
    private void SaveEmployeeEvents( Employeeemployee ) { var patches = new List<PatchRequest>(); foreach (var evt in employee.PendingEvents) { patches.Add(new PatchRequest { Type = PatchCommandType.Add, Name = "Events", Value = RavenJObject.FromObject(evt, _serializer) }); } var localId = $"employees/{employee.Id}"; var addEmployeeEvents = new PatchCommandData() { Key = localId, Patches = patches.ToArray() }; var updateMetadata = new ScriptedPatchCommandData() { Key = localId, Patch = new ScriptedPatchRequest { Script = $"this['@metadata']['{EmployeeEntityVersion}'] = {employee.Version}; " } }; _store.DatabaseCommands.Batch(new ICommandData[] { addEmployeeEvents, updateMetadata }); }
  • 62.
    public void Save(Employeeemployee) { var head = GetHead(employee.Id); var storedVersion = GetStoredVersionOf(head); if (storedVersion != (employee.Version - employee.PendingEvents.Count())) throw new InvalidOperationException("Invalid object state."); if (head == null) SaveNewEmployee(employee); else SaveEmployeeEvents(employee); foreach (var evt in employee.PendingEvents) Bus.RaiseEvent(evt); }
  • 65.
    Compilation Extensions = entrypoint for more complex logic, used to calculate value of the index entry fields.
  • 66.
    public static classEmployee { public static string FullName(dynamic source) { var e = source.Events[0]; return $"{e.Name.GivenName} {e.Name.Surname}"; } ...
  • 67.
    public static decimalSalary(dynamic source, int atVersion = 0) { var result = 0M; foreach (var evt in source.Events) { if (IsEmployeeRegistered(evt)) result += (decimal) evt.InitialSalary; else if (IsSalaryRaised(evt)) result += (decimal) evt.Amount; if (atVersion != 0 && evt.Version == atVersion) break; } return result; }
  • 68.
    public class EmployeeDynamicCompilationExtension: AbstractDynamicCompilationExtension { public override string[] GetNamespacesToImport() { return new[] {typeof (Employee).Namespace}; } public override string[] GetAssembliesToReference() { return new[] {typeof (Employee).Assembly.Location}; } }
  • 69.
  • 70.
  • 71.
  • 72.
    Implementing CQRS andEvent Sourcing with RavenDB Elemar Júnior @elemarjr elemarjr@ravendb.net elemarjr.com