This is the final part of a four-part series describing how to build an application in .NET using the Command-Query Responsibility Segregation (CQRS) and Event Sourcing (ES) patterns, as well as the SimpleCQRS NuGet package. Click here for Part 1.
UPDATE (Sep 6th 2016): Also check out my follow-up series, Real-World CQRS/ES with ASP.NET and Redis.
We've already dealt with commands and events that create new Domain Objects 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 Domain Objects.
Updating an Existing Domain Object
Whenever you are updating or modifying an already-existing Domain Object, you can start by assuming that the end user will submit the ID of the object being changed. SimpleCQRS exposes a couple classes that run with this assumption to make our development of the corresponding commands and events easier. Let's imagine that we wish to implement an UpdateMovieCommand that looks like this:
public class UpdateMovieCommand : CommandWithAggregateRootId
{
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public int RunningTimeMinutes { get; set; }
public UpdateMovieCommand(Guid movieId, string title, DateTime releaseDate, int runningTime)
{
AggregateRootId = movieId;
Title = title;
ReleaseDate = releaseDate;
RunningTimeMinutes = runningTime;
}
}
Note that this class inherits from CommandWithAggregateRootId. Any command which operates on an already-existing Domain Object needs to inherit from that class.
Now, let's implement the corresponding command handler:
public class UpdateMovieCommandHandler : AggregateRootCommandHandler<UpdateMovieCommand, Movie>
{
protected IDomainRepository _repository;
public UpdateMovieCommandHandler(IDomainRepository repository) : base(repository) { }
public override void Handle(UpdateMovieCommand command, Movie movie)
{
movie.Update(command.AggregateRootId, command.Title, command.ReleaseDate, command.RunningTimeMinutes);
}
}
This command handler is actually simpler than the one for CreateMovieCommand. All it does is call the Update method on the Movie Domain Object with the values from the command. The magic is happening in the Domain Object itself.
Handling an Update Call
Let's see the new Movie domain object class first:
public class Movie : AggregateRoot
{
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public int RunningTimeMinutes { get; set; }
public List<MovieReview> Reviews { get; set; }
public Movie() { }
public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
{
Apply(new MovieCreatedEvent(movieId, title, releaseDate, runningTimeMinutes));
}
public void Update(Guid movieId, string newTitle, DateTime newReleaseDate, int newRunningTime)
{
if(newTitle != Title)
{
Apply(new MovieTitleChangedEvent(movieId, newTitle));
}
if(newReleaseDate != ReleaseDate)
{
Apply(new MovieReleaseDateChangedEvent(movieId, newReleaseDate));
}
if(newRunningTime != RunningTimeMinutes)
{
Apply(new MovieRunningTimeChangedEvent(movieId, newRunningTime));
}
}
protected void OnMovieCreated(MovieCreatedEvent domainEvent)
{
Id = domainEvent.AggregateRootId;
Title = domainEvent.Title;
ReleaseDate = domainEvent.ReleaseDate;
RunningTimeMinutes = domainEvent.RunningTimeMinutes;
}
protected void OnMovieTitleChanged(MovieTitleChangedEvent domainEvent)
{
Title = domainEvent.Title;
}
protected void OnMovieReleaseDateChanged(MovieReleaseDateChangedEvent domainEvent)
{
ReleaseDate = domainEvent.ReleaseDate;
}
protected void OnMovieRunningTimeChanged(MovieRunningTimeChangedEvent domainEvent)
{
RunningTimeMinutes = domainEvent.RunningTimeMinutes;
}
}
Note that the Update method selectively applies up to three different events, depending on which of the properties of the Movie changed. This is one of the nice benefits to CQRS/ES being used in this manner; you can have as much detail or as little detail as you want when it comes to recording and logging the events.
Of course, if you want this level of detail, you have to modify your event handlers to match. In our case, we'll have one event handler class that handles all four possible Movie events:
public class MovieEventHandler : IHandleDomainEvents<MovieCreatedEvent>,
IHandleDomainEvents<MovieTitleChangedEvent>,
IHandleDomainEvents<MovieReleaseDateChangedEvent>,
IHandleDomainEvents<MovieRunningTimeChangedEvent>
{
public void Handle(MovieCreatedEvent createdEvent)
{
using (MoviesReadModel entities = new MoviesReadModel())
{
entities.Movies.Add(new Movie()
{
Id = createdEvent.AggregateRootId,
Title = createdEvent.Title,
ReleaseDate = createdEvent.ReleaseDate,
RunningTimeMinutes = createdEvent.RunningTimeMinutes
});
entities.SaveChanges();
}
}
public void Handle(MovieTitleChangedEvent domainEvent)
{
using (MoviesReadModel entities = new MoviesReadModel())
{
var movie = entities.Movies.Find(domainEvent.AggregateRootId);
movie.Title = domainEvent.Title;
entities.SaveChanges();
}
}
public void Handle(MovieReleaseDateChangedEvent domainEvent)
{
using (MoviesReadModel entities = new MoviesReadModel())
{
var movie = entities.Movies.Find(domainEvent.AggregateRootId);
movie.ReleaseDate = domainEvent.ReleaseDate;
entities.SaveChanges();
}
}
public void Handle(MovieRunningTimeChangedEvent domainEvent)
{
using (MoviesReadModel entities = new MoviesReadModel())
{
var movie = entities.Movies.Find(domainEvent.AggregateRootId);
movie.RunningTimeMinutes = domainEvent.RunningTimeMinutes;
entities.SaveChanges();
}
}
}
Now that we have the Command, Command Handler, Events, and Event Handlers set up for this scenario, the last piece is to add an Action to the MovieController:
[HttpPost]
[Route("update")]
public void Update(UpdateMovieCommand command)
{
CommandBus.Execute(command);
}
An Important Note
Let me make something clear, in case it isn't already: 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 and it's all good. There's no risk to the data structure, no risk to existing code, because creating new events and commands doesn't even 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 are hard to do. Really hard. CQRS and ES are complex architectures for complex applications, and everyone involved in building these kinds of apps 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 devs 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.
That said, those drawbacks are easy to deal with (or work around) if you see a clear benefit to using CQRS and ES.
Summary
Command-Query Responsibility Segregation and Event Sourcing become relatively easy to implement in a .NET application when using SimpleCQRS. 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 me know in the comments.
As always, you can check out the GitHub repository for this series. Feel free to download it, fork it, yell at it, whatever, just let me know if you found it useful (or stupid. Especially if you found it stupid).
Happy Coding!