TDC2016POA | Trilha .NET - CQRS e ES na prática com RavenDB

Preview:

Citation preview

CQRS e Event Sourcing na prática com RavenDB

Elemar Júnior@elemarjr

elemarjr@ravendb.netelemarjr@gmail.com

elemarjr.com

Olá, eu sou Elemar Jr

Eu gosto de RavenDB

CQRS e Event Sourcing na prática com RavenDB

Mas, vamos falar de

Vamos pensar em um cenário...

Precisamos criar um um cadastro de funcionários...

Coisa simples...Nome, Endereço Residencial e Salário

Começamos com um protótipo

[Route("api/[controller]")]public class EmployeesController : Controller{ [HttpGet] public IEnumerable<Employee> Get() { /* .. */ }

[HttpGet("{id}")] public Employee Get(string id) { /* .. */ }

[HttpPost] public void Post([FromBody]Employee value) { /* .. */ }

[HttpPut("{id}")] public void Put(string id, [FromBody]Employee value) { /* .. */ }

[HttpDelete("{id}")] public void Delete(string id) { /* .. */}}

Podemos modelar uma API...

public class Employee{ public string Id { get; set; } public string Name { get; set; } public Address HomeAddress { get; set; } public decimal Salary { get; set; }}Definir o Modelo ...

[Route("api/[controller]")]public class EmployeesController : Controller{ [HttpGet] public IEnumerable<Employee> Get() { /* .. */ }

[HttpGet("{id}")] public Employee Get(string id) { /* .. */ }

[HttpPost] public void Post([FromBody]Employee value) { /* .. */ }

[HttpPut("{id}")] public void Put(string id, [FromBody]Employee value) { /* .. */ }

[HttpDelete("{id}")] public void Delete(string id) { /* .. */}}

public class EmployeesRepository{ public Employee Load(string id) { using (var session = DocumentStoreHolder.Instance.OpenSession()) { return session.Load<Employee>(id); } }

public void Save(Employee newEmployee) { using (var session = DocumentStoreHolder.Instance.OpenSession()) { session.Store(newEmployee); session.SaveChanges(); } }}

Implementar persistência....

Pronto!

Será que ficou bom?

Vejamos...

UI (HTML + Angular)

WebAPI

RavenDB

Presentation

Business

Persistence?

Esse modelo não está anêmico?

public class Employee{ public string Id { get; set; } public string Name { get; set; } public Address HomeAddress { get; set; } public decimal Salary { get; set; }}

Cadê a linguagem ubíqua?[Route("api/[controller]")]public class EmployeesController : Controller{ [HttpGet] public IEnumerable<Employee> Get() { /* .. */ }

[HttpGet("{id}")] public Employee Get(string id) { /* .. */ }

[HttpPost] public void Post([FromBody]Employee value) { /* .. */ }

[HttpPut("{id}")] public void Put(string id, [FromBody]Employee value) { /* .. */ }

[HttpDelete("{id}")] public void Delete(string id) { /* .. */}}

Será que escala?

Será que é fácil de alterar?

Vamos tentar resgatar conceitos...

ViewModelInputModel

Modelo expressa o domíniopublic sealed class Employee { public Guid Id { get; private set; } public FullName Name { get; private set; } public decimal Salary { get; private set; } public Address HomeAddress { get; private set; }

public Employee(Guid id, FullName name, decimal initialSalary) { /* .. */ }

public void RaiseSalary(decimal amount) { /* .. */ }

public void ChangeHomeAddress(Address address) { /* .. */ }}

Modelo expressa o domíniopublic sealed class Employee { public Guid Id { get; private set; } public FullName Name { get; private set; } public decimal Salary { get; private set; } public Address HomeAddress { get; private set; }

public Employee(Guid id, FullName name, decimal initialSalary) { /* .. */ }

public void RaiseSalary(decimal amount) { /* .. */ }

public void ChangeHomeAddress(Address address) { /* .. */ }}

Modelo expressa o domíniopublic sealed class Employee { public Guid Id { get; private set; } public FullName Name { get; private set; } public decimal Salary { get; private set; } public Address HomeAddress { get; private set; }

public Employee(Guid id, FullName name, decimal initialSalary) { /* .. */ }

public void RaiseSalary(decimal amount) { /* .. */ }

public void ChangeHomeAddress(Address address) { /* .. */ }}

Consolidação da Linguagem Ubíqua

public sealed class Employee { public Guid Id { get; private set; } public FullName Name { get; private set; } public decimal Salary { get; private set; } public Address HomeAddress { get; private set; }

public Employee(Guid id, FullName name, decimal initialSalary) { /* .. */ }

public void RaiseSalary(decimal amount) { /* .. */ }

public void ChangeHomeAddress(Address address) { /* .. */ }}

Voltando ao protótipo...

public Employee(Guid id, FullName name, decimal initialSalary){ /* .. */ }

public sealed class Employee { public Guid Id { get; private set; } public FullName Name { get; private set; } public decimal Salary { get; private set; } public Address HomeAddress { get; private set; }

public Employee(Guid id, FullName name, decimal initialSalary) { /* .. */ }

public void RaiseSalary(decimal amount) { /* .. */ }

public void ChangeHomeAddress(Address address) { /* .. */ }}

Consolidação da Linguagem Ubíqua

Cadê o protótipo?

public void RaiseSalary(decimal amount){ /* .. */ }

?

Command

InputModel

Anatomia de um Comandopublic sealed class UpdateEmployeeHomeAddressCommand : EmployeeCommand{ public Address HomeAddress { get; }

public UpdateEmployeeHomeAddressCommand( EmployeeId id, Address address) : base(id) { HomeAddress = address; }}

Command

InputModel

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); }}

InputModel

Command

Chegamos ao Manifesto Reativo

Resolvemos comandos! O que fazemos com consultas

Vamos lembrar...

ViewModel

Logo, seria natural...

E isso é CQRS

... mas e Event Sourcing?

Employee

RaiseSalaryCommand

SalaryRaisedEvent

Command

InputModel

Anatomia de um Evento

public class EmployeeHomeAddressChangedEvent : VersionedEvent<Guid>{ public Address NewAddress { get; }

public EmployeeHomeAddressChangedEvent(Address newAddress) { NewAddress = newAddress; }}

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; }}

Anatomia de um Evento

public class EmployeeHomeAddressChangedEvent : VersionedEvent<Guid>{ public Address NewAddress { get; }

public EmployeeHomeAddressChangedEvent(Address newAddress) { NewAddress = newAddress; }}

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; }}

Anatomia de um Evento

public class EmployeeHomeAddressChangedEvent : VersionedEvent<Guid>{ public Address NewAddress { get; }

public EmployeeHomeAddressChangedEvent(Address newAddress) { NewAddress = newAddress; }}

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; }}

Stream de EventosEmployeeRegistered

HomeAddressUpdated

SalaryRaised

SalaryRaised

HomeAddressUpdated

SalaryRaised

Stream de EventosEmployeeRegistered

HomeAddressUpdated

SalaryRaised

SalaryRaised

HomeAddressUpdated

SalaryRaised

Por que não salvar?

Stream de Eventos

No RavenDB é fácil!

Stream de Eventos

No RavenDB é fácil!

... mas antes é preciso Gerar os Eventos

Entidade/Agregado gera Eventos

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));}

Entidade/Agregado gera Eventos

public void RaiseSalary(decimal amount){ Throw.IfArgumentIsNegative(amount, nameof(amount)); Update(new EmployeeSalaryRaisedEvent(amount));}

Entidade/Agregado gera Eventos

public void ChangeHomeAddress(Address address){ Throw.IfArgumentIsNull(address, nameof(address)); Update(new EmployeeHomeAddressChangedEvent(address));}

Atualiza estado a partir dos Eventos

protected void Update(VersionedEvent<TId> e){ e.SourceId = Id; e.Version = Version + 1;

_handlers[e.GetType()].Invoke(e);

Version = e.Version; _pendingEvents.Add(e);}

Atualiza estado a partir dos Eventos

protected void Update(VersionedEvent<TId> e){ e.SourceId = Id; e.Version = Version + 1;

_handlers[e.GetType()].Invoke(e);

Version = e.Version; _pendingEvents.Add(e);}

private Employee(Guid id) : base(id){ Handles<EmployeeRegisteredEvent>(OnEmployeeRegistered); Handles<EmployeeSalaryRaisedEvent>(OnEmployeeSalaryRaised); Handles<EmployeeHomeAddressChangedEvent>(OnEmployeeHomeAddressChanged); }

Atualiza estado a partir dos Eventos

protected void Update(VersionedEvent<TId> e){ e.SourceId = Id; e.Version = Version + 1;

_handlers[e.GetType()].Invoke(e);

Version = e.Version; _pendingEvents.Add(e);}

private void OnEmployeeRegistered(EmployeeRegisteredEvent @event){ Name = @event.Name; Salary = @event.InitialSalary;}

private void OnEmployeeSalaryRaised(EmployeeSalaryRaisedEvent @event){ Salary += @event.Amount;}

Atualiza estado a partir dos Eventos

protected void Update(VersionedEvent<TId> e){ e.SourceId = Id; e.Version = Version + 1;

_handlers[e.GetType()].Invoke(e);

Version = e.Version; _pendingEvents.Add(e);}

Stream de EventosEmployeeRegistered

HomeAddressUpdated

SalaryRaised

SalaryRaised

HomeAddressUpdated

SalaryRaised

Por que não salvar?

Application (InputModel)

CommandHandler (Command)

Entidade (Evento)

Application (InputModel)

CommandHandler (Command)

Entidade (Evento)

Application (InputModel)

CommandHandler (Command)

Entidade (Evento)

Salvar um documento com eventospublic 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);}

Salvar um documento com eventos

public JsonDocumentMetadata GetHead(Guid id){ string localId = $"employees/{id}"; return _store.DatabaseCommands.Head(localId);}

public int GetStoredVersionOf(JsonDocumentMetadata head){ return head ?.Metadata[EmployeeEntityVersion] .Value<int>() ?? 0;}

Salvar um documento com eventospublic 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);}

Salvar um documento com eventos

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 });}

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 });}

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 });}

Salvar um documento com eventospublic 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);}

Carregar um documento com eventos

public Employee Load(Guid id){ EmployeeEvents data; using (var session = _store.OpenSession()) { data = session.Load<EmployeeEvents>(id); } return new Employee(id, data.Events);}public Employee(Guid id, IEnumerable<IVersionedEvent<Guid>> history) : this(id){ LoadFrom(history);}

Carregar um documento com eventos

protected void LoadFrom(IEnumerable<IVersionedEvent<TId>> pastEvents){ foreach (var e in pastEvents) { _handlers[e.GetType()].Invoke(e); Version = e.Version; }}

elemarjr.com@elemarjrlinkedin.com/in/elemarjr

elemarjr@ravendb.netelemarjr@gmail.com

Mantenha contato!

CQRS e Event Sourcing na prática com RavenDB

Elemar Júnior@elemarjr

elemarjr@ravendb.netelemarjr@gmail.com

elemarjr.com