-
Notifications
You must be signed in to change notification settings - Fork 79
A Beginner's Guide To Implementing CQRS ES Part 5: Informative thinking
This is Part 5 of a series describing how to build an application in .NET using the Command-Query Responsibility Segregation and Event Sourcing patterns, as well as the [CQRS.NET]. Click here for Part 1.
We've already dealt with commands and events that create new and update existing Aggregate Roots
in Part 2 and Part 4 of this series. Now we can look at more informative events.
Let's imagine that we want to know if the title was actually changed or not, to do so we need a more informative MovieTitleUpdatedEvent
that looks like this:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Cqrs.Domain;
using Cqrs.Events;
using Cqrs.Messages;
public class MovieTitleUpdatedEvent : IEventWithIdentity<string>
{
#region Implementation of IEvent
/// <summary>
/// The Id of the <see cref="IEvent{TAuthenticationToken}" /> itself, not the object that was created. This helps identify one event from another if two are sent with the same information.
/// </summary>
[DataMember]
public Guid Id { get; set; }
/// <summary>
/// The version number the <see cref="AggregateRoot{TAuthenticationToken}">aggregate root</see> shifted to as a result of the request.
/// </summary>
[DataMember]
public int Version { get; set; }
/// <summary>
/// The time the event was generated. Application of the event may happen at a different time.
/// </summary>
[DataMember]
public DateTimeOffset TimeStamp { get; set; }
#endregion
#region Implementation of IMessageWithAuthenticationToken<Guid>
/// <summary>
/// The authentication token used to identify the requester.
/// </summary>
[DataMember]
public string AuthenticationToken { get; set; }
#endregion
#region Implementation of IMessage
/// <summary>
/// The correlation id used to group together events and notifications.
/// </summary>
[DataMember]
public Guid CorrelationId { get; set; }
/// <summary>
/// The originating framework this message was sent from.
/// </summary>
[DataMember]
public string OriginatingFramework { get; set; }
/// <summary>
/// The frameworks this <see cref="IMessage"/> has been delivered to/sent via already.
/// </summary>
[DataMember]
public IEnumerable<string> Frameworks { get; set; }
#endregion
#region Implementation of IEventWithIdentity
/// <summary>
/// The Rsn of the <see cref="AggregateRoot{TAuthenticationToken}">aggregate root</see> create.
/// </summary>
[DataMember]
public Guid Rsn { get; set; }
#endregion
[DataMember]
public string OriginalTitle { get; set; }
[DataMember]
public string Title { get; set; }
public MovieTitleUpdatedEvent(Guid rsn, string originalTitle, string title)
{
Rsn = rsn;
OriginalTitle = originalTitle;
Title = title;
}
}
This doesn't require us changing the UpdateMovieTitleCommandHandler
, only the Movie
Aggregate Root
. Specifically we need to keep track, in the current state, the current value of the Title. Let's see the new Movie
Aggregate Root
class:
using System;
using cdmdotnet.Logging;
using Cqrs.Configuration;
using Cqrs.Domain;
public class Movie : AggregateRoot<string>
{
/// <summary>
/// The identifier of this movie.
/// </summary>
public Guid Rsn
{
get { return Id; }
private set { Id = value; }
}
public string Title { get; private set; }
/// <summary>
/// Instantiates a new instance of a new movie.
/// </summary>
public Movie(Guid rsn)
{
Rsn = rsn;
}
/// <summary>
/// Instantiates a new instance of an existing movie.
/// </summary>
public Movie(IDependencyResolver dependencyResolver, ILogger logger, Guid rsn)
{
Rsn = rsn;
}
public void Create(string title, DateTime releaseDate, int runningTimeMinutes)
{
MovieCreatedEvent movieCreatedEvent = new MovieCreatedEvent(Rsn, title, releaseDate, runningTimeMinutes);
ApplyChange(movieCreatedEvent);
}
public void UpdateTitle(string title)
{
MovieTitleUpdatedEvent movieTitleUpdatedEvent = new MovieTitleUpdatedEvent(Rsn, Title, title);
ApplyChange(movieTitleUpdatedEvent);
}
}
The two changes here are the Title
property, holding the current value (state) of the movies title, and passing this current (originally state pre-update) value of the Title
.
Of course, this asks the question of how do we apply the current value to the Title
property? This is done via an Apply
method:
using System;
using cdmdotnet.Logging;
using Cqrs.Configuration;
using Cqrs.Domain;
public class Movie : AggregateRoot<string>
{
/// <summary>
/// The identifier of this movie.
/// </summary>
public Guid Rsn
{
get { return Id; }
private set { Id = value; }
}
public string Title { get; private set; }
/// <summary>
/// Instantiates a new instance of a new movie.
/// </summary>
public Movie(Guid rsn)
{
Rsn = rsn;
}
/// <summary>
/// Instantiates a new instance of an existing movie.
/// </summary>
public Movie(IDependencyResolver dependencyResolver, ILogger logger, Guid rsn)
{
Rsn = rsn;
}
public void Create(string title, DateTime releaseDate, int runningTimeMinutes)
{
MovieCreatedEvent movieCreatedEvent = new MovieCreatedEvent(Rsn, title, releaseDate, runningTimeMinutes);
ApplyChange(movieCreatedEvent);
}
public void UpdateTitle(string title)
{
MovieTitleUpdatedEvent movieTitleUpdatedEvent = new MovieTitleUpdatedEvent(Rsn, Title, title);
ApplyChange(movieTitleUpdatedEvent);
}
private void Apply(MovieCreatedEvent movieCreatedEvent)
{
Title = movieCreatedEvent.Title;
}
private void Apply(MovieTitleUpdatedEvent movieTitleUpdatedEvent)
{
Title = movieTitleUpdatedEvent.Title;
}
}
The Apply()
methods (One for each event that specify a Title
value) are called automatically, first, by the ApplyChange
method, so the new state is immediately applied to any relevant properties, and each time, automatically, in an operation called re-hydration
. Re-hydration is operation that replays all the changes/events, to the Aggregate Root
by the UnitOfWork
when you call the UnitOfWork.Get()
method back in the command handler.
Because we now have more informative events, we can improve our sample application friendly user interface:
using System;
using Cqrs.Events;
public class SendConsoleFeedbackEventHandler
: IEventHandler<string, MovieCreatedEvent>
, IEventHandler<string, MovieTitleUpdatedEvent>
{
public void Handle(MovieCreatedEvent createdEvent)
{
Console.WriteLine("A new movie was added with the title {0}.", createdEvent.Title);
}
public void Handle(MovieTitleUpdatedEvent updatedEvent)
{
Console.WriteLine("The movie {0} was renamed to {1}.", updatedEvent.OriginalTitle, updatedEvent.Title);
}
}
It is important to note that we didn't change the name of the property Title
on the event to something like NewTitle
. The reason for this is that all your previously stored event in your EventStore
wouldn't have a property called NewTitle
so during re-hydration
there would be no information to set. This choice not to rename the event property is also the reason we don't have to update the UpdateMovieEntityEventHandler
class.
This leeds specifically into one of the more advanced concepts of an event sourced application... event versioning. That is a topic we'll leave for a later date as there are multiple ways to handle that.
The next article takes a deeper step, moving from the console to the web.