-
Notifications
You must be signed in to change notification settings - Fork 79
A Beginner's Guide To Implementing CQRS ES Part 4: More Events and Summary
This is Part 4 of a four-part 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 Aggregate Roots in Part 2 of this series, and saw the order of execution for those events in Part 3. Now we can implement commands and events that update existing Aggregate Roots
.
Let's imagine that we wish to implement an UpdateMovieTitleCommand
that looks like this:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Cqrs.Commands;
using Cqrs.Domain;
[Serializable]
[DataContract]
public class UpdateMovieTitleCommand : ICommandWithIdentity<string>
{
#region Implementation of ICommand
/// <summary>
/// The Id of the <see cref="ICommand{TAuthenticationToken}" /> itself, not the object being create. This helps identify one command from another if two are sent with the same information.
/// </summary>
[DataMember]
public Guid Id { get; set; }
/// <summary>
/// The version number you expect this command to shift the <see cref="AggregateRoot{TAuthenticationToken}">aggregate root</see> to. I.E. +1 from what you know it's current version to be.
/// </summary>
[DataMember]
public int ExpectedVersion { get; set; }
#endregion
#region Implementation of IMessageWithAuthenticationToken<string>
/// <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>
public string OriginatingFramework { get; set; }
/// <summary>
/// The frameworks this <see cref="T:Cqrs.Messages.IMessage"/> has been delivered to/sent via already.
/// </summary>
public IEnumerable<string> Frameworks { get; set; }
#endregion
#region Implementation of ICommandWithIdentity
/// <summary>
/// The Rsn of the <see cref="AggregateRoot{TAuthenticationToken}">aggregate root</see> being created.
/// </summary>
[DataMember]
public Guid Rsn { get; set; }
#endregion
[DataMember]
public string Title { get; set; }
public UpdateMovieTitleCommand(Guid rsn, string title)
{
Id = Guid.NewGuid();
Rsn = rsn;
Title = title;
}
}
Note that this class is the same as the CreateMovieCommand
with the one exception that the identifier (Rsn
) is required. Any command that operates on an already-existing Aggregate Root
needs to provide the identifer.
The corresponding event will look 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 Title { get; set; }
public MovieTitleUpdatedEvent(Guid rsn, string title)
{
Rsn = rsn;
Title = title;
}
}
Now, let's implement the corresponding command handler:
using Cqrs.Commands;
using Cqrs.Domain;
public class UpdateMovieTitleCommandHandler : ICommandHandler<string, UpdateMovieTitleCommand>
{
protected IUnitOfWork<string> UnitOfWork { get; private set; }
public UpdateMovieTitleCommandHandler(IUnitOfWork<string> unitOfWork)
{
UnitOfWork = unitOfWork;
}
#region Implementation of ICommandHandler<in UpdateMovieTitleCommand>
public void Handle(UpdateMovieTitleCommand command)
{
Movie item = UnitOfWork.Get<Movie>(command.Rsn);
item.UpdateTitle(command.Title);
UnitOfWork.Commit();
}
#endregion
}
This command handler differs only slightly from the one for CreateMovieCommand
. Instead of creating a new Aggregate Root
and calling the Add()
method on the UnitOfWork
, the Get()
method on the UnitOfWork
is called to retrieve the Aggregate Root
with its latest state in-tact.
Let's see the new Movie
Aggregate Root
class first:
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; }
}
/// <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);
ApplyChange(movieTitleUpdatedEvent);
}
}
public class MovieReview : AggregateRoot<string>
{
/// <summary>
/// The identifier of this review.
/// </summary>
public Guid Rsn
{
get { return Id; }
private set { Id = value; }
}
public MovieReview(Guid rsn)
{
Rsn = rsn;
}
}
One interesting curiosity is the new constructor. This is required for the automated systems within CQRS that will give you an existing Aggregate Root
with it's correct state. One of the nice benefits to CQRS/ES is that your can have as much or as little detail as you want when it comes to commands as well as recording and logging events. In this case we have discrete commands that make it clear what the intent was, rather than guessing what was intended by comparing data. Bugs and flaws in the software issuing requests can mean that a user may ask for one thing to happen, but because the wrong data was sent, we take the wrong action.
Of course, if you want this level of detail, you have to modify your event handlers to match. In our case, we have one event handler class that handles all events that affect the MovieEntity
:
using System.Configuration;
using System.Data.Linq;
using System.Linq;
using System.Transactions;
using Cqrs.Events;
public class UpdateMovieEntityEventHandler
: IEventHandler<string, MovieCreatedEvent>
, IEventHandler<string, MovieTitleUpdatedEvent>
{
public void Handle(MovieCreatedEvent createdEvent)
{
using (var transaction = new TransactionScope())
{
using (DataContext dbDataContext = new DataContext(ConfigurationManager.ConnectionStrings["MoviesDataStore"].ConnectionString))
{
Table<MovieEntity> table = dbDataContext.GetTable<MovieEntity>();
var entity = new MovieEntity
{
Rsn = createdEvent.Rsn,
Title = createdEvent.Title,
ReleaseDate = createdEvent.ReleaseDate,
RunningTimeMinutes = createdEvent.RunningTimeMinutes
};
table.InsertOnSubmit(entity);
dbDataContext.SubmitChanges();
}
transaction.Complete();
}
}
public void Handle(MovieTitleUpdatedEvent updatedEvent)
{
using (var transaction = new TransactionScope())
{
using (DataContext dbDataContext = new DataContext(ConfigurationManager.ConnectionStrings["MoviesDataStore"].ConnectionString))
{
Table<MovieEntity> table = dbDataContext.GetTable<MovieEntity>();
var entity = table
.Select(x => x)
.Single(movie => movie.Rsn == updatedEvent.Rsn);
entity.Title = @updatedEvent.Title;
dbDataContext.SubmitChanges();
}
transaction.Complete();
}
}
}
Note that we explicitly load the existing entity, edit it and explicitly update it. We don't do an insert if the entity is not found as this would imply that the datastore is corrupt. You would have no idea what events may have come before-hand and thus cannot know the values of the other properties. It is for this very reason you should always be explicit and clear in your handling of data so you don't hide bugs and corrupt data. Also, because we are using EntityFramework the operation .Select(x => x)
needs to be appended... just an oddity if using EntityFramework. In a later tutorial we'll cover how to build an application that will let you more easily switch between SQLServer and other things like MongoDB.
We now have the ability to change the title. While this is an extremely simple example, the principal is key to learning CQRS/ES.
Because our sample application has a user friendly interface, we'll inform the user of the change via the Console
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 was renamed to {0}.", updatedEvent.Title);
}
}
Finally we update our console application with a little bit of new logic to allow a user to rename their movie like so:
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using Cqrs.Commands;
using Cqrs.Configuration;
static class Program
{
private static IDictionary<int, MovieEntity> Movies { get; set; }
private static ICommandPublisher<string> CommandPublisher { get; set; }
public static void Main()
{
using (new SqlSampleRuntime<string, CreateMovieCommandHandler>())
{
CommandPublisher = DependencyResolver.Current.Resolve<ICommandPublisher<string>>();
string command = "Help";
while (HandleUserEntry(command))
{
command = Console.ReadLine();
}
}
}
private static bool HandleUserEntry(string text)
{
switch (text.ToLowerInvariant())
{
case "help":
Console.WriteLine("Add Movie");
Console.WriteLine("\tWill allow you to add a new movie.");
Console.WriteLine("Update Movie Title");
Console.WriteLine("\tWill allow you to update the title of an existing movie.");
Console.WriteLine("Get All Movies");
Console.WriteLine("\tWill get a list of all movies.");
Console.WriteLine("Quit");
Console.WriteLine("\tWill exit the running program.");
Console.WriteLine("Help");
Console.WriteLine("\tWill display this help menu.");
break;
case "add movie":
case "addmovie":
Console.WriteLine("Enter the title for a new movie.");
string title = Console.ReadLine();
int duration;
do
{
Console.WriteLine("Enter the duration (how long) in minutes, the new movie runs for.");
string durationValue = Console.ReadLine();
if (int.TryParse(durationValue, out duration))
break;
Console.WriteLine("The entered value of {0} was not a whole number.", durationValue);
} while (true);
CommandPublisher.Publish(new CreateMovieCommand(title, DateTime.Now, duration));
break;
case "update movie title":
case "updatemovietitle":
HandleUserEntry("Get All Movies");
int movieIndex;
do
{
Console.WriteLine("Enter the number of the movie would you like to update.");
string value = Console.ReadLine();
if (int.TryParse(value, out movieIndex))
break;
Console.WriteLine("The entered value of {0} was not a whole number.", value);
} while (true);
var movieToUpdate = Movies[movieIndex];
Console.WriteLine("Enter the new title for movie {0}.", movieToUpdate.Title);
string newTitle = Console.ReadLine();
CommandPublisher.Publish(new UpdateMovieTitleCommand(movieToUpdate.Rsn, newTitle));
break;
case "get all movies":
case "list all movies":
case "getallmovies":
case "listallmovies":
using (DataContext dbDataContext = new DataContext(System.Configuration.ConfigurationManager.ConnectionStrings["MoviesDataStore"].ConnectionString))
{
int index = 1;
Movies = dbDataContext
.GetTable<MovieEntity>()
.ToDictionary(movie => index++);
foreach (var movie in Movies)
{
Console.WriteLine("{0:N0}) {1}", movie.Key, movie.Value.Title);
Console.WriteLine("\t{0:N0} minute{1} long.", movie.Value.RunningTimeMinutes, movie.Value.RunningTimeMinutes == 1 ? null : "s");
}
Console.WriteLine("A total of {0} movie{1}", Movies.Count, Movies.Count == 1 ? null : "s");
}
break;
case "quit":
case "exit":
return false;
}
return true;
}
}
At this point it may seem that implementing CQRS/ES is a lot of work when you first get starting developing any application with it, but any subsequent work (e.g. implementing new actions and commands) will always be of the same difficulty, rather than getting more difficult.
In a CRUD application, one where the data store models the data, any time you make changes to the model you must update code and data, and risk breaking both of them. However, with CQRS and ES, any time you need to add a new command or new event, you just write the correct classes. There's no risk to the data structure, no risk to existing code, because creating new events and commands doesn't touch existing code. You're much more insulated from change by using these patterns in tandem.
Despite the benefits we can gain from using CQRS and Event Sourcing, there are some significant drawbacks:
- CQRS and ES can be hard to do. Really hard. CQRS and ES are complex architectures for complex applications, and everyone involved in building these kinds of applications will need to spend a significant amount of time just learning the concepts involved.
- Following from that, there's a lot of architectural overhead that's necessary for these ideas to work, and that makes the app much more confusing to new developers or people that haven't worked on it before.
- The Command and Event buses, in this implementation, are synchronous and all commands and events fire immediately. This may not be what you want in a more distributed system. Luckily there are modules with enterprise grade queues available, such as the Azure Servicebus modules.
Command-Query Responsibility Segregation and Event Sourcing become relatively easy to implement in a .NET application when using CQRS.NET. But don't let "simple" fool you: these architectures are complex enough that it will most likely take you a while to really understand what they are accomplishing. But, if you can see the benefit that these ideas provide (and believe me, there's a lot of benefit here) then you should at least consider using CQRS or ES for your next project.
Here's hoping this series has shed some light on Command-Query Responsibility Segregation, Event Sourcing, and how to do both in a .NET application. For those interested in making a step into some more advanced concepts you can continue this series with our next article, informative thinking.