SlideShare a Scribd company logo
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).
Implementing CQRS and Event Sourcing with RavenDB
Implementing CQRS and Event Sourcing with RavenDB
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.
Implementing CQRS and Event Sourcing with RavenDB
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;
Implementing CQRS and Event Sourcing with RavenDB
Implementing CQRS and Event Sourcing with RavenDB
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;
}
}
Implementing CQRS and Event Sourcing with RavenDB
Implementing CQRS and Event Sourcing with RavenDB
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);
}
Implementing CQRS and Event Sourcing with RavenDB
Implementing CQRS and Event Sourcing with RavenDB
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

More Related Content

What's hot (20)

PPTX
Grupo atlas cuello clase region pre vertebral
Grupo Atlas
 
PPTX
Escapula homoplato
Daysi Briseida
 
PDF
Jaula toracica
SistemadeEstudiosMed
 
PDF
Microsoft power point neuro i
anatofono201
 
PPTX
Anatomía de columna vertebral
Luis mendez
 
PPTX
Articulación del codo
Jessica Caraguay
 
PPTX
Huesos de la mano y el pie
Gabriel Cardoso
 
PDF
2018 diencefalic anatomy slideshare
MMENON
 
PPTX
Craneo
DrCabreraGuillen
 
PPTX
huesos del craneo
Fernando Villacres
 
PPTX
Musculos de la region cervical posterior
belenchi94
 
PPTX
Lesiones del metacarpo jalil
Efrén Quintero
 
PPT
Traquea brisa
ERUM DF
 
PPTX
Músculos extensores del antebrazo anatomia
Isabelly Berihuete
 
PPTX
Miembro Superior I
Rosario Román
 
PPS
Músculos+del+cuerpo+humano.ppt
isabellavg04
 
PPT
huesos miembro inferior
K-ro Varela
 
PDF
1. huesos del miembro inferior
franco gerardo
 
PPT
MAXILAR SUPERIOR
letycabrera
 
Grupo atlas cuello clase region pre vertebral
Grupo Atlas
 
Escapula homoplato
Daysi Briseida
 
Jaula toracica
SistemadeEstudiosMed
 
Microsoft power point neuro i
anatofono201
 
Anatomía de columna vertebral
Luis mendez
 
Articulación del codo
Jessica Caraguay
 
Huesos de la mano y el pie
Gabriel Cardoso
 
2018 diencefalic anatomy slideshare
MMENON
 
huesos del craneo
Fernando Villacres
 
Musculos de la region cervical posterior
belenchi94
 
Lesiones del metacarpo jalil
Efrén Quintero
 
Traquea brisa
ERUM DF
 
Músculos extensores del antebrazo anatomia
Isabelly Berihuete
 
Miembro Superior I
Rosario Román
 
Músculos+del+cuerpo+humano.ppt
isabellavg04
 
huesos miembro inferior
K-ro Varela
 
1. huesos del miembro inferior
franco gerardo
 
MAXILAR SUPERIOR
letycabrera
 

Similar to Implementing CQRS and Event Sourcing with RavenDB (20)

PPTX
An Introduction To CQRS
Neil Robbins
 
PPTX
TDC2016POA | Trilha .NET - CQRS e ES na prática com RavenDB
tdc-globalcode
 
PPTX
TDC2016SP - Trilha .NET
tdc-globalcode
 
PPTX
An intro to cqrs
Neil Robbins
 
DOC
slides
thamerr
 
PPTX
CQRS and Event Sourcing
Sergey Seletsky
 
PPT
Wcf data services
Eyal Vardi
 
PDF
Create a C# applicationYou are to create a class object called “Em.pdf
feelingspaldi
 
PPTX
Functional programming in C#
Thomas Jaskula
 
PPTX
TDC2016POA | Trilha .NET - C# como você nunca viu: conceitos avançados de pro...
tdc-globalcode
 
PPTX
dotNet Miami - June 21, 2012: Richie Rump: Entity Framework: Code First and M...
dotNet Miami
 
PPTX
Entity Framework: Code First and Magic Unicorns
Richie Rump
 
PPTX
The Good, The Bad and The Ugly of Event Sourcing
Dennis Doomen
 
PDF
Software Developer Training
rungwiroon komalittipong
 
PPTX
Introducción a LINQ
Mariano Sánchez
 
PPTX
Event sourcing in the functional world (22 07-2021)
Vitaly Brusentsev
 
PPTX
CQRS: Command/Query Responsibility Segregation
Brian Ritchie
 
PPTX
.NET Database Toolkit
wlscaudill
 
PPTX
Seminar - Scalable Enterprise Application Development Using DDD and CQRS
Mizanur Sarker
 
PPTX
Taming Brownfield Codebases with AOP
Donald Belcham
 
An Introduction To CQRS
Neil Robbins
 
TDC2016POA | Trilha .NET - CQRS e ES na prática com RavenDB
tdc-globalcode
 
TDC2016SP - Trilha .NET
tdc-globalcode
 
An intro to cqrs
Neil Robbins
 
slides
thamerr
 
CQRS and Event Sourcing
Sergey Seletsky
 
Wcf data services
Eyal Vardi
 
Create a C# applicationYou are to create a class object called “Em.pdf
feelingspaldi
 
Functional programming in C#
Thomas Jaskula
 
TDC2016POA | Trilha .NET - C# como você nunca viu: conceitos avançados de pro...
tdc-globalcode
 
dotNet Miami - June 21, 2012: Richie Rump: Entity Framework: Code First and M...
dotNet Miami
 
Entity Framework: Code First and Magic Unicorns
Richie Rump
 
The Good, The Bad and The Ugly of Event Sourcing
Dennis Doomen
 
Software Developer Training
rungwiroon komalittipong
 
Introducción a LINQ
Mariano Sánchez
 
Event sourcing in the functional world (22 07-2021)
Vitaly Brusentsev
 
CQRS: Command/Query Responsibility Segregation
Brian Ritchie
 
.NET Database Toolkit
wlscaudill
 
Seminar - Scalable Enterprise Application Development Using DDD and CQRS
Mizanur Sarker
 
Taming Brownfield Codebases with AOP
Donald Belcham
 
Ad

More from Oren Eini (12)

PPTX
Staying friendly with the gc
Oren Eini
 
PPTX
Rebooting design in RavenDB
Oren Eini
 
PPTX
Should I use a document database?
Oren Eini
 
PDF
Delving into Documents with Data Subscriptions
Oren Eini
 
PDF
Building Codealike: a journey into the developers analytics world
Oren Eini
 
PPTX
Zapping ever faster: how Zap sped up by two orders of magnitude using RavenDB
Oren Eini
 
PDF
Know thy cost (or where performance problems lurk)
Oren Eini
 
PPTX
Lessons from the Trenches - Building Enterprise Applications with RavenDB
Oren Eini
 
PPTX
RavenDB embedded at massive scales
Oren Eini
 
PDF
RavenDB for modern web apps
Oren Eini
 
PPTX
RavenDB 3.5
Oren Eini
 
PPTX
RavenDB 4.0
Oren Eini
 
Staying friendly with the gc
Oren Eini
 
Rebooting design in RavenDB
Oren Eini
 
Should I use a document database?
Oren Eini
 
Delving into Documents with Data Subscriptions
Oren Eini
 
Building Codealike: a journey into the developers analytics world
Oren Eini
 
Zapping ever faster: how Zap sped up by two orders of magnitude using RavenDB
Oren Eini
 
Know thy cost (or where performance problems lurk)
Oren Eini
 
Lessons from the Trenches - Building Enterprise Applications with RavenDB
Oren Eini
 
RavenDB embedded at massive scales
Oren Eini
 
RavenDB for modern web apps
Oren Eini
 
RavenDB 3.5
Oren Eini
 
RavenDB 4.0
Oren Eini
 
Ad

Recently uploaded (20)

PPTX
Building and Operating a Private Cloud with CloudStack and LINBIT CloudStack ...
ShapeBlue
 
PPTX
The Yotta x CloudStack Advantage: Scalable, India-First Cloud
ShapeBlue
 
PDF
RAT Builders - How to Catch Them All [DeepSec 2024]
malmoeb
 
PDF
Peak of Data & AI Encore - Real-Time Insights & Scalable Editing with ArcGIS
Safe Software
 
PDF
UiPath vs Other Automation Tools Meeting Presentation.pdf
Tracy Dixon
 
PDF
visibel.ai Company Profile – Real-Time AI Solution for CCTV
visibelaiproject
 
PDF
TrustArc Webinar - Navigating Data Privacy in LATAM: Laws, Trends, and Compli...
TrustArc
 
PPTX
python advanced data structure dictionary with examples python advanced data ...
sprasanna11
 
PDF
The Past, Present & Future of Kenya's Digital Transformation
Moses Kemibaro
 
PDF
How Current Advanced Cyber Threats Transform Business Operation
Eryk Budi Pratama
 
PPTX
Simplifying End-to-End Apache CloudStack Deployment with a Web-Based Automati...
ShapeBlue
 
PPTX
Extensions Framework (XaaS) - Enabling Orchestrate Anything
ShapeBlue
 
PDF
NewMind AI Weekly Chronicles – July’25, Week III
NewMind AI
 
PDF
Bitcoin+ Escalando sin concesiones - Parte 1
Fernando Paredes García
 
PDF
Market Insight : ETH Dominance Returns
CIFDAQ
 
PPTX
Top Managed Service Providers in Los Angeles
Captain IT
 
PDF
HR agent at Mediq: Lessons learned on Agent Builder & Maestro by Tacstone Tec...
UiPathCommunity
 
PDF
CIFDAQ'S Token Spotlight for 16th July 2025 - ALGORAND
CIFDAQ
 
PPTX
AVL ( audio, visuals or led ), technology.
Rajeshwri Panchal
 
PDF
2025-07-15 EMEA Volledig Inzicht Dutch Webinar
ThousandEyes
 
Building and Operating a Private Cloud with CloudStack and LINBIT CloudStack ...
ShapeBlue
 
The Yotta x CloudStack Advantage: Scalable, India-First Cloud
ShapeBlue
 
RAT Builders - How to Catch Them All [DeepSec 2024]
malmoeb
 
Peak of Data & AI Encore - Real-Time Insights & Scalable Editing with ArcGIS
Safe Software
 
UiPath vs Other Automation Tools Meeting Presentation.pdf
Tracy Dixon
 
visibel.ai Company Profile – Real-Time AI Solution for CCTV
visibelaiproject
 
TrustArc Webinar - Navigating Data Privacy in LATAM: Laws, Trends, and Compli...
TrustArc
 
python advanced data structure dictionary with examples python advanced data ...
sprasanna11
 
The Past, Present & Future of Kenya's Digital Transformation
Moses Kemibaro
 
How Current Advanced Cyber Threats Transform Business Operation
Eryk Budi Pratama
 
Simplifying End-to-End Apache CloudStack Deployment with a Web-Based Automati...
ShapeBlue
 
Extensions Framework (XaaS) - Enabling Orchestrate Anything
ShapeBlue
 
NewMind AI Weekly Chronicles – July’25, Week III
NewMind AI
 
Bitcoin+ Escalando sin concesiones - Parte 1
Fernando Paredes García
 
Market Insight : ETH Dominance Returns
CIFDAQ
 
Top Managed Service Providers in Los Angeles
Captain IT
 
HR agent at Mediq: Lessons learned on Agent Builder & Maestro by Tacstone Tec...
UiPathCommunity
 
CIFDAQ'S Token Spotlight for 16th July 2025 - ALGORAND
CIFDAQ
 
AVL ( audio, visuals or led ), technology.
Rajeshwri Panchal
 
2025-07-15 EMEA Volledig Inzicht Dutch Webinar
ThousandEyes
 

Implementing CQRS and Event Sourcing with RavenDB

  • 1. Implementing CQRS and Event Sourcing with RavenDB Elemar Júnior @elemarjr [email protected] elemarjr.com
  • 5. We segregate the responsibilit 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 could easily consolidate all the necessary data needed by the presentation layer in an elegant and organized way.
  • 10. Even when using a single database for the “command stack” and “query stack”, RavenDB databases are a good option.
  • 11. Indexes and transformers are excellent alternatives to provide data for the Query Stack
  • 12. What is Event Sourcing? Storing all the changes (events) to the system, rather than just its current state.
  • 14. This technique allows us to recover the status of any entity/aggregate simply "replaying" the changes recorded in the events.
  • 16. apply(aggregateState, domainEvent) => match domainEvent with EventType1 -> … EventType2 -> … EventType2 -> …
  • 17. f(s, e) -> s’
  • 19. f(f(f(s, e’), e’’), e’’’) -> s’’’
  • 20. foldl (apply) s ne -> s’
  • 21. Is event sourcing a 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 code in following samples is NOT intended to be in any way production
  • 24. DISCLAIMER: All the code in following samples is NOT intended to be a definitive solution
  • 25. DISCLAIMER: All the code in following samples is NOT the best implementation neither the correct one
  • 26. 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; } }
  • 27. 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(); } }
  • 28. 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() {//**//} } }
  • 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 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;
  • 33. 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; } }
  • 34. 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; } }
  • 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(EmployeeId id) { var lid = $"employees/{id}"; return DocumentStoreHolder .Instance .DatabaseCommands.Head(lid) != null; }
  • 37. 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(); } }
  • 38. 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; } }
  • 39. public sealed class RaiseEmployeeSalaryCommand : EmployeeCommand { public decimal Amount { get; } public RaiseEmployeeSalaryCommand( EmployeeId id, decimal amount ) : base(id) { Amount = amount; } }
  • 40. public void RaiseSalary(EmployeeId id, decimal amount) { DocumentStoreHolder.Instance.DatabaseCommands.Patch($"employees/{id}", new ScriptedPatchRequest { Script = $"this.Salary += {amount.ToInvariantString()};" }); }
  • 41. public sealed class UpdateEmployeeHomeAddressCommand : EmployeeCommand { public Address HomeAddress { get; } public UpdateEmployeeHomeAddressCommand( EmployeeId id, Address address) : base(id) { HomeAddress = address; } }
  • 42. 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;
  • 43. 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); } }
  • 44. var defaultImpl = Instance.Conventions.FindTypeTagName; Instance.Conventions.FindTypeTagName = t => t.IsSubclassOf(typeof(EmployeeEvent)) ? "EmployeeEvents" : defaultImpl(t);
  • 45. 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) }; } }
  • 46. public class SalaryPerEmployeeResult { public EmployeeId 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 – Using a “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 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)); }
  • 51. 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)); }
  • 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(Guid id, 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(Guid id) { 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( 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 }); }
  • 62. 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); }
  • 65. Compilation Extensions = entry point for more complex logic, used to calculate value of the index entry fields.
  • 66. public static class Employee { public static string FullName(dynamic source) { var e = source.Events[0]; return $"{e.Name.GivenName} {e.Name.Surname}"; } ...
  • 67. 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; }
  • 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. Done!
  • 72. Implementing CQRS and Event Sourcing with RavenDB Elemar Júnior @elemarjr [email protected] elemarjr.com