Last time, we started a very basic Event Sourcing/Domain
Events/CQRS framework. Be careful, I made an edit in the nested
DomainEvents+Handler<T>.Handles<E>() method, the
AggregateRoot.Replay method will not work as is, but we won’t need it.
We’ll build an equally simplistic application for personal library
management.
The Ubiquitous Language will be minimal.
A Book can be Registered with a
Title and an ISBN.
A Book can be Lent to a
Borrower at some Date for an Expected
Time Span.
A Book can then be Returned. If it is
Returned after Expected Time Span, the return
is Late.
That’s enough for our first try.
The Command Context
The State Change Events
Here is the code for the three events that we found in the Ubiquitous
language:
public
class BookRegistered
{
public readonly
BookId Id;
public readonly
string Title;
public readonly
string Isbn;
public BookRegistered(BookId id, string
title, string isbn)
{
Id = id;
Title =
title;
Isbn =
isbn;
}
}
public
class BookLent
{
public readonly
BookId Id;
public readonly
string Borrower;
public readonly
DateTime Date;
public readonly
TimeSpan ExpectedDuration;
public BookLent(BookId
id, string borrower, DateTime date,
TimeSpan expectedDuration)
{
Id = id;
Borrower =
borrower;
Date =
date;
ExpectedDuration = expectedDuration;
}
}
public
class BookReturned
{
public readonly
BookId Id;
public readonly
string By;
public readonly
TimeSpan After;
public readonly
bool Late;
public BookReturned(BookId id, string @by,
TimeSpan after,
bool late)
{
Id = id;
By = by;
After =
after;
Late =
late;
}
}
These events will usually be serialized to the event storage and on a
service bus, but here everything runs in memory.
The Book Aggregate Root
The book will need to be referenced by an identity in our system. We’ll hide
a Guid behind a BookId struct :
public
struct BookId : IEquatable<BookId>
{
private Guid id;
private BookId(Guid
id) { this.id = id; }
public static
BookId NewBookId() { return new BookId(Guid.NewGuid()); }
public bool
Equals(BookId other) { return other.id.Equals(id); }
public override
bool Equals(object obj)
{
if (ReferenceEquals(null,
obj)) return false;
if (obj.GetType() != typeof(BookId))
return false;
return Equals((BookId)obj);
}
public override
int GetHashCode() { return id.GetHashCode(); }
}
Now, the Book class itself :
public
class Book
: AggregateRoot<BookId>
{
private readonly
BookId id;
private string title;
private string isbn;
private string
borrower;
private DateTime
date;
private TimeSpan
expectedDuration;
public Book(BookId id,
IEnumerable<object> events)
{
this.id = id;
foreach (dynamic @event in
events)
Apply(@event);
}
public Book(BookId id,
string title, string isbn)
{
this.id = id;
var @event = new BookRegistered(id, title, isbn);
Apply(@event);
Append(@event);
}
public override
BookId Id { get { return id; } }
public void
Lend(string borrower, DateTime date,
TimeSpan expectedDuration)
{
if (this.borrower != null)
throw new
InvalidOperationException("The book is already lent.");
var @event =
new BookLent(id, borrower, date,
expectedDuration);
Apply(@event);
Append(@event);
}
public void
Return(DateTime returnDate)
{
if (borrower == null)
throw new
InvalidOperationException("The book has not been lent.");
if (returnDate < date)
throw new
ArgumentException(
"The book cannot be returned before being
lent.");
var actualDuration = returnDate - date;
var @event = new BookReturned(
id,
borrower,
actualDuration,
actualDuration > expectedDuration);
Apply(@event);
Append(@event);
}
private void
Apply(BookRegistered @event)
{
title = @event.Title;
isbn = @event.Isbn;
}
private void
Apply(BookLent @event)
{
borrower = @event.Borrower;
date = @event.Date;
expectedDuration = @event.ExpectedDuration;
}
private void
Apply(BookReturned @event)
{
borrower = null;
}
}
The class implements AggregateRoot<BookId> and so provides an
explicitly implemented UncommittedEvents property.
The first .ctor is used to load the Aggregate Root, the second one is used
to build a new Aggregate Root.
The public methods (Lend and Return) are the commands on the Aggregate Root
as defined in the Ubiquitous Language.
The structure is always the same :
- Validate arguments and state
- Prepare state transition using domain logic
- Apply state transition (no domain logic should happen here)
- Append state transition to uncommitted events
The first .ctor uses dynamic to dispatch each event object on the
corresponding specific Apply method. In case you implement the pattern is
previous C# version, it is advised to provide a Replay method in the base class
that will perform the dynamic dispatch based on reflection.
That’s all for the entity. No ORM, no mapping… easy.
The Repository
It is often clearer to provide a specific repository interface that exposes
only available methods. With event sourcing, it’s not that useful… we’ll write
it anyway in case you’d like to use dependency injection. The interface is part
of the domain and should be in the same assembly as the entity and the
events.
public
interface IBookRepository
{
void Add(Book
book);
Book this[BookId id] { get;
}
}
The implementation will simply derive from the Repository base class, it can
be in the application assembly.
internal class
BookRepository :
Repository<BookId, Book>,
IBookRepository
{
protected override
Book CreateInstance(BookId id,
IEnumerable<object> events)
{
return new Book(id, events);
}
}
Add and the indexer are implemented by the base class. The only thing to
provide is a way to instantiate the class with expected parameters.
We could use Activator.CreateInstance or reflection to provide a generic
implementation. I choose to make it simpler to read.
The Query context
The Report Database
We’ll mimic a reporting table of book lent state :
This would be the data returned from table rows :
public
class BookState
{
public BookId Id {
get; set;
}
public string Title {
get; set;
}
public bool Lent {
get; set;
}
}
And this will hide the data table implementation :
public
interface IBookStateQuery
{
IEnumerable<BookState> GetBookStates();
BookState GetBookState(BookId id);
IEnumerable<BookState> GetLentBooks();
void AddBookState(BookId id, string
title);
void SetLent(BookId
id, bool lent);
}
We can simply query data to report in the UI, and update data state.
Implementation will be in memory for now :
class
BookStateQuery : IBookStateQuery
{
private readonly
Dictionary<BookId, BookState> states =
new Dictionary<BookId, BookState>();
public IEnumerable<BookState> GetBookStates()
{
return states.Values;
}
public BookState
GetBookState(BookId id)
{
return states[id];
}
public IEnumerable<BookState> GetLentBooks()
{
return states.Values.Where(b => b.Lent);
}
public void
AddBookState(BookId id, string title)
{
var state = new
BookState { Id = id, Title = title };
states.Add(id, state);
}
public void
SetLent(BookId id, bool lent)
{
states[id].Lent = lent;
}
}
The important point here is that no domain logic occurs.
A RDBMS implementation could use an ORM or simply build DTOs from a
DataReader.
The event handlers
We can now denormalize domain states to the reporting database using an
event handler :
class
BookStateHandler :
Handles<BookRegistered>,
Handles<BookLent>,
Handles<BookReturned>
{
private readonly
IBookStateQuery stateQuery;
public BookStateHandler(IBookStateQuery stateQuery)
{
this.stateQuery = stateQuery;
}
public void
Handle(BookRegistered @event)
{
stateQuery.AddBookState(@event.Id, @event.Title);
}
public void
Handle(BookLent @event)
{
Console.WriteLine("Book
lent to {0}", @event.Borrower);
stateQuery.SetLent(@event.Id, true);
}
public void
Handle(BookReturned @event)
{
Console.WriteLine("Book
returned by {0}", @event.By);
stateQuery.SetLent(@event.Id, false);
}
}
The Console.WriteLine are here to view when things happen, you would usually
not use it in your production code. Logging this would not provide much
benefits since all the events are already stored in the EventStorage.
Using this handler, the IBookStateQuery will be up to date with current
Command Context state. In an asynchronous environment, this is where eventual
consistency is introduced.
We will also add a service that will notify when a user returned a book too
late :
class
LateReturnNotifier :
Handles<BookReturned>
{
public void
Handle(BookReturned @event)
{
if (@event.Late)
{
Console.WriteLine("{0} was late", @event.By);
}
}
}
Here again, no domain logic, we just do the infrastructure stuff, usually
sending an email or a SMS.
View it in Action
class
Program
{
static void
Main(string[] args)
{
ISessionFactory factory = new
SessionFactory(new
EventStorage());
IBookStateQuery query = new BookStateQuery();
DomainEvents.RegisterHanlder(() => new
BookStateHandler(query));
DomainEvents.RegisterHanlder(() => new
LateReturnNotifier());
var bookId = BookId.NewBookId();
using (var session =
factory.OpenSession())
{
var books = new BookRepository();
books.Add(new Book(bookId,
"The Lord of the Rings",
"0-618-15396-9"));
session.SubmitChanges();
}
ShowBooks(query);
using (var session =
factory.OpenSession())
{
var books = new BookRepository();
var book = books[bookId];
book.Lend("Alice",
new DateTime(2009, 11, 2),
TimeSpan.FromDays(14));
session.SubmitChanges();
}
ShowBooks(query);
using (var session =
factory.OpenSession())
{
var books = new BookRepository();
var book = books[bookId];
book.Return(new DateTime(2009, 11, 8));
session.SubmitChanges();
}
ShowBooks(query);
using (var session =
factory.OpenSession())
{
var books = new BookRepository();
var book = books[bookId];
book.Lend("Bob",
new DateTime(2009, 11, 9),
TimeSpan.FromDays(14));
session.SubmitChanges();
}
ShowBooks(query);
using (var session =
factory.OpenSession())
{
var books = new BookRepository();
var book = books[bookId];
book.Return(new DateTime(2010, 03, 1));
session.SubmitChanges();
}
ShowBooks(query);
}
private static
void ShowBooks(IBookStateQuery query)
{
foreach (var state
in query.GetBookStates())
Console.WriteLine("{0} is {1}.",
state.Title,
state.Lent ? "lent" : "home");
}
}
We start by instantiating storage for the command context (the
ISessionFactory) and the query context (the IBookStateQuery). In production you’ll use persistent
storages (a persistent event storage and a RDBMS). I highly recommend using a
Dependency Injection Container for real size projects.
Then we wire the handlers on domain events.
The application can start.
- We register a book in the library.
- We lend it to Alice on 2009-11-02 for 14 days
- She returns it on 2009-11-08, she’s on time
- We lend it to Bob on 2009-11-09 for 14 days,
- He returns it on 2010-03-01, he’s late
The output is the following :
The Lord of the Rings is
home. // written from
state
Book lent to
Alice
// written by the book state handler
The Lord of the Rings is
lent. // written from
state
Book returned by
Alice
// written by the book state handler
The Lord of the Rings is
home. // written from
state
Book lent to
Bob
// written by the book state handler
The Lord of the Rings is
lent. // written from
state
Book returned by
Bob
// written by the book state handler
Bob was
late
// written by the late return notifier
The Lord of the Rings is
home. // written from
state
We have here a clear separation between Command that handles the domain
logic and Query that handles presentation logic.
Have fun. Questions and remarks expected !