Skip to content

A Beginner's Guide To Implementing CQRS ES Part 4: More Events and Summary

Sam edited this page Feb 11, 2018 · 14 revisions

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.

Updating an 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.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;
	}
}

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.

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.

Handling an Update Call

Let's see the new Movie Aggregate Root class first:

using System;
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 Movie(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);
	}
}

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.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.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.

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

#An Important Note 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.

Drawbacks

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.

Summary

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. If you liked this series or found it useful, please let us know in the comments.

Clone this wiki locally