Professional Documents
Culture Documents
The Domain
For this tutorial, we'll work in the cafe domain. Our focus will be on the
concept of a tab, which tracks the visit of an individual or group to the cafe.
When people arrive to the cafe and take a table, a tab is opened. They may
then order drinks and food. Drinks are served immediately by the table staff,
however food must be cooked by a chef. Once the chef has prepared the
food, it can then be served.
During their time at the restaurant, visitors may order extra food or drinks. If
they realize they ordered the wrong thing, they may amend the order - but not
after the food and drink has been served to and accepted by them.
Finally, the visitors close the tab by paying what is owed, possibly with a tip
for the serving staff. Upon closing a tab, it must be paid for in full. A tab with
unserved items cannot be closed unless the items are either marked as
served or cancelled first.
Events
In the scenario described above, various verbs were picked out. When
working in a database centric way, it is common to listen carefully for nouns,
mapping them to tables and relating them. Designing in terms of commands
and events puts the focus on verbs. This is good, since things that make a
domain interesting tend to be tied up in the verbs rather than the nouns. Every
business has a customer (hopefully!) - it's what they do for them that matters.
Looking through the scenario, focusing on the language we find within it, we
look for things that happen that lead to some kind of new informationin the
domain. We map these happenings to a set of events. Since events are about
things that have taken place, they are named in the past tense.
Here are a set of events we may come up with from the cafe tab scenario.
TabOpened
DrinksOrdered
FoodOrdered
DrinksCancelled
FoodCancelled
DrinksServed
FoodPrepared
FoodServed
TabClosed
Note that the events are very domain-focused. The difference between
ordering drinks and ordering food matters, so we capture these into different
events.
Commands
Commands are things that indicate requests to our domain. While an event
states that something certainly happened, a command may
beaccepted or rejected. An accepted command leads to zero or more events
being emitted to incorporate new facts into the system. A rejected command
leads to some kind of exception.
Commands are also identified by looking for verbs. However, they are focused
around what the user considers as an operation. For example, while it is
important that food and drink are handled differently in the domain, wait staff
will most certainly not want to enter drinks into the system and order them,
then separately enter food into the system, with no way to get an overview of
the order being placed! It's likely that each person at a table will specify their
food and drink together, and somebody will probably change their mind after
learning what their friend orders. Therefore, there will be a single command for
placing an order.
Here are the initial set of commands we arrive at for this domain:
OpenTab
PlaceOrder
AmendOrder
MarkDrinksServed
MarkFoodPrepared
MarkFoodServed
CloseTab
Notice how the names include a verb in the imperative mood.
Exceptions
An important part of the modeling process is thinking about the things that can
cause a command to be refused. The Edument CQRS Starter Kit is decidedly
opinionated here: we are expected to model these "sad paths" into exception
types, just as commands and events are expressed as DTOs. Furthermore,
these exception types may carry details of why the request was not
acceptable. This is because the domain logic should tell the frontend what
was wrong, rather than leaving it to ask by trying to inspect some state - or
worse, leave the user to guess.
Looking to the scenario, we can identify three notable exceptions we need in
our model:
CannotCancelServedItem
TabHasUnservedItems
MustPayEnough
The names here try to explain why the command failed.
Aggregates
Of course, man does not live by verbs alone. At some point, there has to be
nouns. More concretely, there has to be a way to talk about current state in
order to decide if a command should be accepted. For example, to refuse to
cancel a served item, we have to know that it was served.
All of the things we need are captured in the stream of past events, since they
capture all facts introduced into the system. However, somehow we need a
way to pick out the orders and servings that relate to a particular tab. More
generally, all events should be tied to some kind of entity that they together
describe the evolution and current state of.
The missing piece here is known as an aggregate. Each aggregate has its
own stream of events. Taken together, they can be used to compute its current
state. Aggregates are completely isolated from each other. Decisions about
whether to accept a command are made solely on the basis of the command
itself and the information contained in the aggregate's past events.
Concretely, an aggregate is an object. It may have internal structure (that is, it
may be built up out of many other objects), but it does not reach out to objects
outside of itself. This is demanding from a design perspective, as it forces us
to identify and segregate business concepts.
Our current example has been carefully selected to have just one aggregate
type - namely, a tab. However, in most systems you will have more work to do
in order to identify aggregates. The authors of this tutorial have found that
starting from the events and commands, then trying to group them based on
invariants (business rules you need to uphold), is a good strategy.
Moving forwards
We're now ready to start writing test cases and build up our domain logic. As
we do so, we should be ready to revisit aspects of our design, and we'll also
need to flesh out the details of what our commands and events contain.
None of the infrastructure in the starter kit requires events to inherit from a
particular base class. The only requirement comes from its various event store
bindings, which require a field Id of type Guid.
The command looks very similar, in this particular case:
public class OpenTab
{
public Guid Id;
public int TableNumber;
public string Waiter;
}
Once again, there is not inheritance requirement. In fact, there is no need for
a Guid Id field here either; it's just a convention.
On conventions, you may wonder whether we should suffix our command and
event type names with Command and Event. The answer is that no, it just creates
noise. It turns out that humans are really rather good at knowing if a verb is an
imperative ("Cook me a spicy burrito!") or in the past tense ("I cooked you a
spicy burrito, but then my chihuahua ate it").
Before we can write tests, we need to declare two more classes: one for our
command handler, and another for our tab aggregate.
public class TabCommandHandlers
{
}
public class TabAggregate : Aggregate
{
}
The test says that given we have no event history for the
tab, when an OpenTab command is issued, then we expect a TabOpened event to
be produced.
Happily, however, the error it fails with gives us a big hint what to do next:
CafeTests.TabTests.CanOpenANewTab: Command handler
TabCommandHandlers does not yet handle command OpenTab
Adding a command handler involves implementing the
generic IHandleCommand interface. It should be parameterized with both the
command to handle and the type of the aggregate. Note that after typing the
implementation, Ctrl + . can be used to create the stub Handle method, ready
to fill out.
public class TabCommandHandlers :
IHandleCommand<OpenTab, TabAggregate>
{
public IEnumerable Handle(Func<Guid, TabAggregate> al, OpenTab c)
{
yield return new TabOpened
{
Id = c.Id,
TableNumber = c.TableNumber,
Waiter = c.Waiter
};
}
}
Taking Orders
Next, we'll look at the process of taking orders. First up, we'll define
the DrinksOrdered and FoodOrdered events.
public class OrderedItem
{
public int MenuNumber;
public string Description;
public bool IsDrink;
public decimal Price;
}
public class DrinksOrdered
{
public Guid Id;
public List<OrderedItem> Items;
}
public class FoodOrdered
{
public Guid Id;
public List<OrderedItem> Items;
}
They share the OrderedItem class. While it is important that events remain
independent of each other, re-use of this kind is not problematic.
The command is unsurprising:
public class PlaceOrder
{
Now for the test. The tab should be opened before placing an order. At this
point, we realize that we didn't identify that failure mode earlier. That's fine; the
process of writing tests often brings about such clarifications. So we add an
exception type:
public class TabNotOpen : Exception
{
}
You can create a file per exception type, but it can be much less clutter to
create an Exceptions.cs and put all of the exceptions for an aggregate into it.
Next, we write a test:
[Test]
public void CanNotOrderWithUnopenedTab()
{
Test(
Given(),
When(new PlaceOrder
{
Id = testId,
Items = new List<OrderedItem> { testDrink1 }
}),
ThenFailWith<TabNotOpen>());
}
With that detail taken care of, now it's time to write some more interesting
tests. We map out three cases of ordering: just drinks, just food, and a mix of
food and drink. We want to make sure that items are categorized into the
correct events, and that empty drink or food order events are not emitted.
[Test]
public void CanPlaceDrinksOrder()
{
Test(
Given(new TabOpened
{
Id = testId,
TableNumber = testTable,
Waiter = testWaiter
}),
When(new PlaceOrder
{
Id = testId,
Items = new List<OrderedItem> { testDrink1, testDrink2 }
}),
Then(new DrinksOrdered
{
Id = testId,
Items = new List<OrderedItem> { testDrink1, testDrink2 }
}));
}
[Test]
public void CanPlaceFoodOrder()
{
Test(
Given(new TabOpened
{
Id = testId,
TableNumber = testTable,
Waiter = testWaiter
}),
When(new PlaceOrder
{
Id = testId,
Items = new List<OrderedItem> { testFood1, testFood1 }
}),
Then(new FoodOrdered
{
Id = testId,
Items = new List<OrderedItem> { testFood1, testFood1 }
}));
}
[Test]
public void CanPlaceFoodAndDrinkOrder()
{
Test(
Given(new TabOpened
{
Id = testId,
TableNumber = testTable,
Waiter = testWaiter
}),
When(new PlaceOrder
{
Id = testId,
Items = new List<OrderedItem> { testFood1, testDrink2 }
}),
Then(new DrinksOrdered
{
Id = testId,
Items = new List<OrderedItem> { testDrink2 }
},
new FoodOrdered
{
Id = testId,
Items = new List<OrderedItem> { testFood1 }
}));
}
{
};
Id = c.Id,
Items = drink
This is the first time that we use the first argument to the handler, al. Short
for aggregate loader, it takes the ID of an aggregate and loads it. In the case
of a test, this means applying all of the events from the given part of the test.
In production, it would load the events from some kind of event store and
apply them.
As is the command:
public class MarkDrinksServed
{
public Guid Id;
public List<int> MenuNumbers;
}
The testing is less obvious, however. Clearly, we can write a test like this:
[Test]
public void OrderedDrinksCanBeServed()
{
Test(
Given(new TabOpened
{
Id = testId,
TableNumber = testTable,
Waiter = testWaiter
},
new DrinksOrdered
{
Id = testId,
Items = new List<OrderedItem> { testDrink1, testDrink2 }
}),
When(new MarkDrinksServed
{
Id = testId,
MenuNumbers = new List<int>
{ testDrink1.MenuNumber, testDrink2.MenuNumber }
}),
Then(new DrinksServed
{
Id = testId,
MenuNumbers = new List<int>
{ testDrink1.MenuNumber, testDrink2.MenuNumber }
}));
}
With that out of the way, the test now fails with:
CafeTests.TabTests.OrderedDrinksCanBeServed: Command handler
TabCommandHandlers does not yet handle command MarkDrinksServed
This can be fixed with a command handler that simply issues the event.
public IEnumerable Handle(Func<Guid, TabAggregate> al, MarkDrinksServed c)
{
yield return new DrinksServed
{
Id = c.Id,
MenuNumbers = c.MenuNumbers
};
}
The test does now pass, but it's somehow unsatisfying. It's almost as if we
have cheated: we got a passing test, but we've not actually written any logic
related to tracking the ordered drinks and recording their serving. In fact, all
our test really seems to assert is that we can turn a simple command into a
simple event by copying some fields from one to the other. Wow!
So how can we write tests that check we've actually written the required
domain logic? The answer typically lies in the sad paths: the tests that result in
an exception rather than events. For example, we can write tests that ensure:
You can not mark a drink as served if it wasn't ordered in the first place
You can not mark a drink as served if you already served it
Collectively, you can't serve a drink that is not still outstanding in the order.
This leads us to a new exception type:
public class DrinksNotOutstanding : Exception
{
}
One could argue that a well-behaved UI should never let the user make this
mistake. However, our domain logic should treat the sender of commands with
a degree of distrust. Responsibility for maintaining the integrity of the domain
state lies with the command handlers, which serve as a domain gatekeeper.
Having defined our new exception, we can now write a more interesting test:
[Test]
public void CanNotServeAnUnorderedDrink()
{
Test(
Given(new TabOpened
{
Id = testId,
TableNumber = testTable,
Waiter = testWaiter
},
new DrinksOrdered
Id = testId,
Items = new List<OrderedItem> { testDrink1 }
}),
When(new MarkDrinksServed
{
Id = testId,
MenuNumbers = new List<int> { testDrink2.MenuNumber }
}),
ThenFailWith<DrinksNotOutstanding>());
}
Essentially, this means that we can not mark our second test drink as served if
the order was for our first test drink. The test fails:
CafeTests.TabTests.CanNotServeAnUnorderedDrink: Expected exception
DrinksNotOutstanding, but got event result
To fix it, we have our aggregate start tracking the drinks that are still
outstanding:
private List<int> outstandingDrinks = new List<int>();
public void Apply(DrinksOrdered e)
{
outstandingDrinks.AddRange(e.Items.Select(i => i.MenuNumber));
}
Finally, the command handler is updated to use this method to validate the
command:
public IEnumerable Handle(Func<Guid, TabAggregate> al, MarkDrinksServed c)
{
var tab = al(c.Id);
if (!tab.AreDrinksOutstanding(c.MenuNumbers))
throw new DrinksNotOutstanding();
So, now we've tested that ordering drinks actually means something. But what
about the serving? By now, you may have started to notice a pattern: we don't
really have good test coverage until every event in our system appears in
the given clause of a sad path test. Therefore, we need to add a sad path
test involving serving of drinks. How can we do this? The easiest way is to
ensure that trying to serve an ordered drink two times fails:
[Test]
public void CanNotServeAnOrderedDrinkTwice()
{
Test(
Given(new TabOpened
{
Id = testId,
TableNumber = testTable,
Waiter = testWaiter
},
new DrinksOrdered
{
Id = testId,
Items = new List<OrderedItem> { testDrink1 }
},
new DrinksServed
{
Id = testId,
MenuNumbers = new List<int> { testDrink1.MenuNumber }
}),
When(new MarkDrinksServed
{
Id = testId,
MenuNumbers = new List<int> { testDrink1.MenuNumber }
}),
ThenFailWith<DrinksNotOutstanding>());
}
Making it pass just requires writing an Apply method for the DrinksServed event:
public void Apply(DrinksServed e)
{
foreach (var num in e.MenuNumbers)
outstandingDrinks.Remove(num);
}
We'll pass over the food workflow in the tutorial; there's no new concepts
involved with implementing it.
In some cases, commands and events end up looking the same. However,
they do not need to. It's fine for extra information to be incorporated into the
event, based on the model of history built up in the aggregate. It's also
reasonable for extra events to be emitted if a command triggers something
significant in the domain. For example, if we incorporated the notion of time to
serve food into our domain, we may wish to give a discount if some deadline
is not met. This would happen by virtue of the domain noticing the deadline
was broken and issuing an extra event granting the compensation along with
the food served event.
For closing the tab, the TabClosed event will look like this:
public class TabClosed
{
public Guid Id;
public decimal AmountPaid;
public decimal OrderValue;
public decimal TipValue;
}
This not only includes the total amount paid, but also incorporates the value of
the ordered food/drink and the amount left over, presumably given as a tip to
the waitstaff. The command, however, just includes the amount paid:
public class CloseTab
{
public Guid Id;
public decimal AmountPaid;
}
new DrinksOrdered
{
Id = testId,
Items = new List<OrderedItem> { testDrink2 }
},
new DrinksServed
{
Id = testId,
MenuNumbers = new List<int> { testDrink2.MenuNumber }
}),
When(new CloseTab
{
Id = testId,
AmountPaid = testDrink2.Price + 0.50M
}),
Then(new TabClosed
{
Id = testId,
AmountPaid = testDrink2.Price + 0.50M,
OrderValue = testDrink2.Price,
TipValue = 0.50M
}));
}
As we try to make this pass, we realize that our aggregate hasn't been
keeping track of the prices of the items served. The prices arrive in
thePlaceOrder command, but should not contribute to the bill until the items
have been served.
One of the advantages of working in terms of events, and only building up the
aggregate when we need it, is that we have the freedom to change the
models in our aggregates as our domain understanding grows. Naturally, this
applies during the initial development. But even after a system enters
production, we are still able to evolve our aggregate in the face of new
requirements.
This time, the changes are relatively straightforward. Currently, we just keep
track of the menu numbers of the ordered items:
private List<int> outstandingDrinks = new List<int>();
private List<int> outstandingFood = new List<int>();
private List<int> preparedFood = new List<int>();
The event applier for DrinksOrdered actually gets simpler, going from:
public void Apply(DrinksOrdered e)
{
outstandingDrinks.AddRange(e.Items.Select(i => i.MenuNumber));
To just:
public void Apply(DrinksOrdered e)
{
outstandingDrinks.AddRange(e.Items);
}
It's a similar story for FoodOrdered. Next, we introduce a property that we will
use to tally the value of served food and drink:
public decimal ServedItemsValue { get; private set; }
This is, of course, not complete yet. We need tests to cover the sad paths,
including one for closing tabs. You can find these in the sample application;
there is nothing especially notable about them.
Domain First
We have been able to focus strongly on the domain and its language as we
have gone about our development. Along the way, we discovered new failure
modes and incorporated those into our model. We didn't have to be
concerned with the details of persistence, or use any mock/stub objects to
keep our business logic isolated from the environment. And the tests that we
have built are really just data, which we could easily use to produce a report
or some other document that we can discuss with a domain expert.
While focusing on the domain and domain logic is important, it's not the only
thing we must develop in order to have a complete system. In fact, at present
our system can only accept/reject commands. We have nothing that we can
query for current state. This is what we will be concerned with in the next
section.
A read model is a model specialized for reads, that is, queries. It takes
events produced by the domain and uses them to build and maintain a model
that is suitable for answering the client's queries. It can draw on events from
different aggregates and even different types of aggregate. What it does from
there is unimportant. It may build and maintain...
An in-memory model of the data
A relational model of the data, stored in an RDBMS
Documents, stored in a document database
A graph model of the data, stored in a graph database
A search index
An XML file, CSV file, Excel sheet, etc.
You get to choose - and furthermore, you get to make the decision per read
model (providing a concrete way to realize polyglot persistence). Moreover,
multiple read models can be built from the same event. After all, events
represent facts that have already been incorporated into the system. They are
the normalized part of our system, and read models are a point of potential
denormalization. If that makes your inner relational animal feel scared, it's
worth remembering that you probably turn normalized data into denormalized
data all the time - using the JOIN operator in SQL. We're just choosing to do
the denormalization at a different point - potentially paying for it once per
change rather than once per query.
A read model could even, instead of exposing methods to do queries, expose
push collections, using Rx to turn streams of events from the domain into
objects suited to maintaining some kind of reactive UI. Thus, this approach fits
very well with reactive programming also.
In this tutorial, we'll build in-memory read models. However, the step up to
making a persistent read model, stored in a database or file of some kind, is
small. You can use all the familiar tools you wish to do it, too; there's nothing
wrong with implementing your read model using Linq to SQL, if it is the most
expedient way and will give you the performance you desire.
The current list of food that has been ordered and needs to be prepared
Cashiers need to see:
FoodOrdered,
While the naive approach is just to push all items to prepare into a list,
typically food that is ordered together should be delivered together. Thus, the
grouping should be retained - while realizing that a chef may violate it in
certain circumstances. Therefore,we'll declare two DTOs to represent the todo
list entries.
public class TodoListItem
{
public int MenuNumber;
public string Description;
}
public class TodoListGroup
{
public Guid Tab;
public List<TodoListItem> Items;
}
Since changes to the todo list and requests for it may arrive concurrently, we
must take care to be thread safe. Furthermore, we should never leak the
internal todo list. In fact, we must not leak the internal list of items per group
either, since those can also change over time. Thus, we should write
theGetTodoList method something like this:
public List<TodoListGroup> GetTodoList()
lock (todoList)
return (from grp in todoList
select new TodoListGroup
{
Tab = grp.Tab,
Items = new List<TodoListItem>(grp.Items)
}).ToList();
Note that we need to take care of the locking here as we're keeping a mutable
model around in memory. If your builders always wrote into a database and
your query methods already read from one, then you are most likely free of
these issues (because you've left them for the database to deal with).
What about the methods that will build the todo list? New orders are a fairly
simple bit of mapping:
public void Handle(FoodOrdered e)
{
var group = new TodoListGroup
{
Tab = e.Id,
Items = new List<TodoListItem>(
e.Items.Select(i => new TodoListItem
{
MenuNumber = i.MenuNumber,
Description = i.Description
}))
};
lock (todoList)
todoList.Add(group);
}
The FoodPrepared handler has a little more to think about. First, it should
remove all of the prepared items from the group. Then, if nothing remains in
the group, the group itself should also be removed.
public void Handle(FoodPrepared e)
{
lock (todoList)
{
var group = todoList.First(g => g.Tab == e.Id);
foreach (var num in e.MenuNumbers)
group.Items.Remove(
group.Items.First(i => i.MenuNumber == num));
}
}
if (group.Items.Count == 0)
todoList.Remove(group);
What's striking is that we essentially end up storing information about the tab
overall. With a few extensions, this read model would also be able to answer
queries related to tabs. Therefore, we can simplify our original plan, having
just two read models. The resulting new read model is calledOpenTabs, because
its focus is on queries related to tabs that are currently open. See the sample
application for the full details.
It's important to remain open to course corrections such as this during the
development process. Things that a developer can see as they build a system
differ from those a domain expert can see, which can lead to simplifications
and new opportunities.
These will be used by the web frontend, which we will look at next.
A way for a cashier to obtain an invoice and, upon payment, close the
tab
The takeaway message here is that not everything in your system will benefit
from intentful testing, event sourcing, DDD and so forth. Apply it in the core
domain. For the stuff that needs to be there but isn't the core driver of
business value, do the cheapest thing - but couple to it loosely.
Setup
Common Links
To make for easy navigation, we want all pages to display a set of links for
each member of wait staff's todo lists, and another set linking to the status
page for each open tab. We'll use ASP.NET MVC's ViewBag to store this data,
accessing it from the page layout. Since we will want this for almost all of the
pages, we'll factor out the logic to add it to an action filter.
public class IncludeLayoutDataAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext
filterContext)
{
if (filterContext.Result is ViewResult)
{
var bag = (filterContext.Result as ViewResult).ViewBag;
bag.WaitStaff = StaticData.WaitStaff;
bag.ActiveTables = Domain.OpenTabQueries.ActiveTableNumbers();
}
}
}
Notice how this makes a call to a read model. The layout can then incorporate
this information into the navigation.
<li class="nav-header">Wait Staff Todo Lists</li>
@foreach (var w in ViewBag.WaitStaff as List<string>)
{
<li>@Html.ActionLink(w, "Todo", "WaitStaff", new { id = w }, null)</li>
}
<li class="nav-header">Open Tabs</li>
@foreach (var t in ViewBag.ActiveTables as List<int>)
{
<li>@Html.ActionLink(t.ToString(), "Status", "Tab", new { id = t }, null)</li>
}
Opening A Tab
Opening a tab is done by sending an OpenTab command. The job of the web
application is to collect information to populate that command. One obvious
thing we could do is use the command as the model for the page - and in fact,
that will work out very nicely here. Thus, here is the Razor view:
@model Cafe.Tab.OpenTab
<h2>Open Tab</h2>
@using (Html.BeginForm())
{
<fieldset>
<label>Table Number</label>
@Html.TextBoxFor(m => m.TableNumber)
<label>
Waiter/Waitress
</label>
@Html.DropDownListFor(m => m.Waiter, (ViewBag.WaitStaff as List<string>)
.Select(w => new SelectListItem { Text = w, Value = w }))
<br>
<button type="submit" class="btn">Submit</button>
</fieldset>
}
We can then use model binding in order to have the MVC framework
instantiate and populate the command object for us, leaving us to fill in a Guid
for the tab we wish to open (a fresh Guid because this command should
happen on a new aggregate) and send the command using the dispatcher.
public ActionResult Open()
{
return View();
}
[HttpPost]
public ActionResult Open(OpenTab cmd)
{
cmd.Id = Guid.NewGuid();
Domain.Dispatcher.SendCommand(cmd);
return RedirectToAction("Order", new { id = cmd.TableNumber });
}
This almost works - but not quite! It turns out that MVC model binding will not
work with fields, only with properties. This is not a big problem; we just tweak
our command a little:
public class OpenTab
{
public Guid Id;
public int TableNumber { get; set; }
public string Waiter { get; set; }
}
Note this makes explicit what fields we welcome model binding of and which
should not be controllable from the web.
Ordering
Placing an order also involves issuing a command. This time, it's not quite so
easy to have the command as the model, but we can define a model that will
be easy to map to the command.
public class OrderModel
{
public class OrderItem
{
public int MenuNumber { get; set; }
public string Description { get; set; }
public int NumberToOrder { get; set; }
}
public List<OrderItem> Items { get; set; }
}
<h2>Place Order</h2>
@using (Html.BeginForm())
{
<fieldset>
<table class="table">
<thead>
<tr>
<td>Menu #</td>
<td>Description</td>
<td>Number To Order</td>
</tr>
</thead>
<tbody>
@for (int i = 0; i < Model.Items.Count; i++)
{
<tr>
<td>
@Model.Items[i].MenuNumber
@Html.HiddenFor(m => m.Items[i].MenuNumber)
</td>
<td>@Model.Items[i].Description</td>
<td>@Html.TextBoxFor(m => m.Items[i].NumberToOrder)</td>
</tr>
}
</tbody>
</table>
<button type="submit" class="btn">Place Order</button>
</fieldset>
}
We can again use model binding to have the form data mapped
into OrderModel. Then, we do a little bit of mapping to turn it into
the PlaceOrdercommand and send it using the dispatcher. We also need to
resolve the table number to its tab's ID.
[HttpPost]
public ActionResult Order(int id, OrderModel order)
{
var items = new List<Events.Cafe.OrderedItem>();
var menuLookup = StaticData.Menu.ToDictionary(k => k.MenuNumber, v => v);
foreach (var item in order.Items)
for (int i = 0; i < item.NumberToOrder; i++)
items.Add(new Events.Cafe.OrderedItem
{
MenuNumber = item.MenuNumber,
Description = menuLookup[item.MenuNumber].Description,
Price = menuLookup[item.MenuNumber].Price,
IsDrink = menuLookup[item.MenuNumber].IsDrink
});
Domain.Dispatcher.SendCommand(new PlaceOrder
{
Id = Domain.OpenTabQueries.TabIdForTable(id),
Items = items
});
Note that TabIdForTable is a method added to the read model, having realized
while building the web application that it would be useful. This is a normal
thing to do; after all, the queries are there to serve the needs of clients.
The view simply takes the model and renders it out as HTML. Note that we
take care to include a helpful link to the tab status page for each table we
should serve to.
@model Dictionary<int, List<CafeReadModels.OpenTabs.TabItem>>
<h2>Todo List For @ViewBag.Waiter</h2>
@foreach (var table in Model)
{
<h3>Table @table.Key</h3>
<table class="table">
<thead>
<tr>
<th>Menu #</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach (var item in table.Value)
{
<tr>
<td>@item.MenuNumber</td>
<td>@item.Description</td>
</tr>
}
</tbody>
</table>
There's nothing particularly exciting here; we really are just taking data out of
the read model and rendering it using the view.
The view, however, is a bit more interesting this time. We need to offer a way
to mark items as prepared. This needs a little care, because it's possible a
chef will manage to prepare only some of the items together, so we must offer
a way to mark out those that have been prepared. We'll use check boxes, to
keep things relatively simple.
@model List<CafeReadModels.ChefTodoList.TodoListGroup>
<h2>Meals To Prepare</h2>
@foreach (var group in Model)
{
var i = 0;
using (Html.BeginForm("MarkPrepared", "Chef"))
{
@Html.Hidden("id", group.Tab)
<table class="table">
<thead>
<tr>
<th>Menu #</th>
<th>Description</th>
<th>Prepared</th>
</tr>
</thead>
<tbody>
@foreach (var item in group.Items)
{
<tr>
<td>@item.MenuNumber</td>
<td>@item.Description</td>
<td>@Html.CheckBox(string.Format("prepared_{0}_{1}",
i++, item.MenuNumber.ToString()))</td>
</tr>
}
</tbody>
</table>
<button type="submit" class="btn">Mark Prepared</button>
<hr />
}
Often, views will end up playing this dual role: showing data from a read
model and also providing a way to issue some command to do the next piece
of work.
The Rest
The other operations that make up the web application are variations on the
things we've already seen in this tutorial, so we won't go over them in more
detail here. See the sample application if you're curious.
Task Based UI
Intentful testing leads us to commands that express domain-significant acts.
This in turn tends to be reflected in the UI, which is now focused on tasks
rather than editing. Since queries are developed according to the kinds of
reports that we need to present, they can also focus on what the user really
needs to see as they use the system.
This helps us to avoid the trap, common in systems that evolve from
designing a relational database, of everything really being about editing. Of
course, just creating commands isn't a magical promise of everything. If we'd
failed to focus on the language of the domain language, we may have ended
up with commands like CreateTab, UpdateTab and DeleteTab. In essence, we'd
have gone to all this effort only to recreate a CRUD system - one we could
have realized in a much more expedient manner!
All the pieces - intentful testing, a focus on the verbs, separating out the
handling of commands and queries, and task based UI - make a coherent
whole. They are best applied to the core domain, where it makes sense to
invest in building domain understanding, solid tests, and loose coupling with
the outside world.
consistency. However, it's not quite a free lunch. Here's some things you may
need to be aware of if you take this approach.
You need to be using an event storage that knows about transactions. If
you use a event store that is really a couple of SQL Server database tables,
for example, then you're covered.
You need the read models to also be persisting in a way that
participates in the transaction.
The failure of one read model cascades out to a complete failure to
apply the command. (Note that whether this is good or bad is system specific.)
Not everything can be transactional (like sending email).
There are a couple of alternatives that are worth mentioning. One is to use a
store-and-forward message queue to implement the publish/subscribe of
events. The placing of the events into the queue and the persistence of the
events to storage need to happen within a transaction. MSMQ and SQL
Server can be used this way, for example (but beware the distributed
transaction coordinator). The benefit of this is that as soon as the event is in
the subscriber's queues, you're done. Read models can individually and
transactionally receive their copy of the message. This means:
many times in this tutorial, the approach suggested here is aimed at your
core domain.
The typical CRUD system, with a grid of things to edit, has potential to be a
nightmare if you have eventual consistency. Why? Because when the user
adds or edits items, they expect to return to the grid and see it updated with
what they just did. If the grid is being updated by doing a query each time,
then there's no promise the update will have already taken place in the
appropriate read model.
Task based UIs, because they're about taking the user through a workflow,
tend to be less problematic in this regard, however. And doing task based UIs
for the core domain is often a worthwhile investment.