Skip to content
This repository
Newer
Older
100644 811 lines (631 sloc) 33.487 kb
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
1 ## Chapter 4
2 # Extending and Enhancing the Orders and Registrations Bounded Contexts
3
4 *Exploring further*
5
6 # A Description of the Orders and Reservations Bounded Context
7
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
8 The Orders and Reservations Bounded Context was described in some detail
8201374c » dominicbetts
2012-03-28 Added to intro section
9 in the previous chapter. This chapter describes some changes that the
10 team made in this bounded context during the second stage of their CQRS
11 journey.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
12
13 The specific topics described in this chapter include:
14
8201374c » dominicbetts
2012-03-28 Added to intro section
15 * Improvements to the way that message correlation works with the
8c675de8 » dominicbetts
2012-04-19 Terminology Update ReservationProcess to RegistrationProcess
16 **RegistrationProcess*. This illustrates how aggregate instances
a715dfc1 » dominicbetts
2012-04-18 Terminology change - Workflow class now called Process.
17 within the bounded context can interact in a complex manner.
8201374c » dominicbetts
2012-03-28 Added to intro section
18 * Implementing a record locator to enable a registrant to retrieve an
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
19 order that was saved during a previous session. This illustrates
20 adding some additional logic to the write-side that enables you to
21 locate an aggregate instance without knowing its unique Id.
8201374c » dominicbetts
2012-03-28 Added to intro section
22 * Adding a countdown timer to the UI to enable a registrant to track how
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
23 much longer they have to complete an order. This illustrates
24 enhancements to the write-side to support displaying rich information
25 in the UI.
26 * Supporting orders for multiple seat types simultaneously. For example
27 a registrant requests five seats for pre-conference event and eight
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
28 seats for the full conference. This adds more complex business logic
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
29 into the write-side.
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
30 * Supporting partially fulfilled orders where the only some of the
31 seats that the registrant requests are available. This adds more
32 complex business logic into the write-side.
33 * CQRS command validation using MVC. This illustrates how to make use
34 of the model validation feature in MVC to validate your CQRS commands
35 before you send them to the domain.
36
33d4651b » dominicbetts
2012-04-13 Pundit/Mani:
37 > **Note:** The Contoso Conference Management System described in this
38 > chapter is not the final version of the system. This guidance
39 > describes a journey, so some of the design decisions and
40 > implementation details change in later steps in the journey. These
41 > changes are described in subsequent chapters.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
42
43 ## Working Definitions for this Chapter
44
45 The following definitions are used for the remainder of this chapter.
46 For more detail, and possible alternative definitions see [A CQRS/ES
47 Deep Dive][r_chapter4] in the Reference Guide.
48
49 ### Command
50
33d4651b » dominicbetts
2012-04-13 Pundit/Mani:
51 A command is a request for the system to perform an action that changes
52 the state of the system. Commands are imperatives, for example
53 **MakeSeatReservation**. In this bounded context, commands originate
54 either from the UI as a result of a user initiating a request, or from
55 a workflow when the workflow is directing an aggregate to perform an
56 action.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
57
33d4651b » dominicbetts
2012-04-13 Pundit/Mani:
58 Commands are processed once by a single recipient. A command bus
59 transports commands that command handlers then dispatch to aggregates.
60 Sending a command is an asynchronous operation with no return value.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
61
62 ### Event
63
64 An event describes something that has happened in the system, typically
65 as a result of a command. Aggregates in the domain model raise events.
66
67 Multiple subscribers can handle a specific event. Aggregates publish
68 events to an event bus; handlers register for specific types of event on
6582a682 » dominicbetts
2012-04-05 Incorporated feedback from Horsdal in Pundit:
69 the event bus and then deliver the events to the subscriber. In this
70 bounded context, the only subscriber is a workflow.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
71
a715dfc1 » dominicbetts
2012-04-18 Terminology change - Workflow class now called Process.
72 ### Coordinating Workflow
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
73
a715dfc1 » dominicbetts
2012-04-18 Terminology change - Workflow class now called Process.
74 In this bounded context, a Coordinating Workflow (or workflow) is a
75 class that coordinates the behavior of the aggregates in the domain. A
76 workflow subscribes to the events that the aggregates raise, and then
77 follow a simple set of rules to determine which command or commands to
78 send. The workflow does not contain any business logic, simply logic to
79 determine the next command to send. The workflow is implemented as a
80 state machine, so when the workflow responds to an event, it can change
81 its internal state in addition to sending a new command.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
82
8201374c » dominicbetts
2012-03-28 Added to intro section
83 The workflow in this bounded context can receive commands as well as
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
84 subscribe to events.
85
86 ## User Stories
87
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
88 This chapter discusses the implementation of two user stories in
89 addition to describing some changes and enhancements to the **Orders and
90 Reservations** bounded context.
8201374c » dominicbetts
2012-03-28 Added to intro section
91
92 ### Implement a Login using a Record Locator
93
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
94 When a registrant creates an order for seats at a conference, the system
95 generates a five character **order access code** and sends it to the
96 registrant by email. The registrant can use her email address and the
97 **order access code** on the conference web site to retrieve the order
6582a682 » dominicbetts
2012-04-05 Incorporated feedback from Horsdal in Pundit:
98 from the system at a later date. The registrant may wish to retrieve the
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
99 order to review it, or to complete the registration process by assigning
100 attendees to seats.
8201374c » dominicbetts
2012-03-28 Added to intro section
101
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
102 ### Inform the Registrant How Much Time Remains to Complete an Order
8201374c » dominicbetts
2012-03-28 Added to intro section
103
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
104 When a registrant creates an order, the system reserves the seats
105 requested by the registrant until the order is complete or the
106 reservations expire. To complete an order, the registrant must submit
107 her details, such as name and email address, and make a successful
108 payment.
109
110 To help the registrant, the system displays a count-down timer to inform
111 the registrant how much time remains to complete the order before the
112 seat reservations expire.
113
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
114 ### Enabling a Registrant to Create an Order that Includes Multiple Seat Types
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
115
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
116 When a registrant creates an order, the registrant may request different
117 numbers of different seat types. For example, a registrant may request
118 five seats for the full conference and three seats for the
119 pre-conference workshop.
8201374c » dominicbetts
2012-03-28 Added to intro section
120
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
121 ### Handling Partially Fulfilled Orders
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
122
b480ed9f » dominicbetts
2012-04-13 Adding the partially fulfilled order story.
123 When a registrant creates an order, it may not be possible to completely
124 fulfill the order. For example, a registrant may request a registrant
125 may request five seats for the full conference, five seats for the
126 drinks reception, and three seats for the pre-conference workshop. There
127 may only be three seats available and one seat for the drinks reception,
128 but more than three seats available for the pre-conference workshop. The
129 system displays this information to the registrant and gives the
130 registrant the opportunity to adjust the number of each type of seat in
131 the order before continuing to the payment process.
132
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
133 ## Architecture
134
135 What are the key architectural features? Server-side, UI, multi-tier, cloud, etc.
136
137 # Patterns and Concepts
138
139 * What are the primary patterns or approaches that have been adopted for this bounded context? (CQRS, CQRS/ES, CRUD, ...)
140
141 * What were the motivations for adopting these patterns or approaches for this bounded context?
142
143 * What trade-offs did we evaluate?
144
145 * What alternatives did we consider?
146
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
147 ## Record Locators
148
149 The system uses **Access Codes** instead of passwords to avoid the
150 overhead for the registrant of setting up an account with the system.
151 Many registrants may use the system only once, so there is no need to
152 create a permanent account with a user ID and a password.
153
154 The system needs to be able to retrieve order information quickly based
155 on the registrant's email address and access code. To provide a minimum
156 level of security, the access codes that the system generates should not
157 be predictable, and the order information that registrants can retrieve
158 should not contain any sensitive information.
159
160 ## Querying the Read-side
161
8201374c » dominicbetts
2012-03-28 Added to intro section
162 The previous chapter focused on the write-side model and implementation,
163 in this chapter we'll explore the read-side implementation in more
164 detail. In particular you'll see how the team implemented the read model
165 and the querying mechanism from the MVC controllers.
166
167 In this initial exploration of the CQRS pattern, the team decided to use
168 SQL views in the database as the underlying source of the data queried
169 by the MVC controllers on the read-side. These views currently exist in
170 the same database as the normalized tables that the write model uses.
171
172 > **JanaPersona:** The team will split the database into two and explore
940f958c » dominicbetts
2012-04-02 Command Validation
173 > options for pushing changes from the normalized write-side to the
174 > de-normalized read-side in a later stage of the journey.
8201374c » dominicbetts
2012-03-28 Added to intro section
175
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
176 ### Storing De-normalized Views in a Database
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
177
178 One common option for storing the read-side data is to use a set of
179 relational database tables to hold the de-normalized views. The
180 read-side should be optimized for fast reads, so there is typically no
181 benefit in storing normalized data because this will require complex
182 queries to construct the data for the client. This implies that goals
183 for the read-side should be to keep the queries as simple as possible,
184 and to structure the tables in the database in such a way that they can
185 be read quickly and efficiently.
186
187 An important area for consideration is the interface whereby a client
188 such as an MVC controller action submits a query to the read-side model.
189
190 ![Figure 1][fig1]
191
192 **The Read-side storing data in a relational database**
193
e47a0d7d » dominicbetts
2012-04-03 Fixed links and images
194 In figure 1, a client such as an MVC controller action invokes a method
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
195 on a **ViewRepository** class to request the data that it needs. The
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
196 **ViewRepository** class in turn runs a query against the de-normalized
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
197 data in the database.
198
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
199 The team at Contoso evaluated two approaches to implementing the **ViewRepository** class: using the **IQueryable** interface and using custom data access objects (DAOs).
200
201 #### Using the **IQueryable** Interface
202
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
203 One approach to consider for the **ViewRepository** class is to have it
204 return an **IQueryable** instance that enables the client to use LINQ to
205 specify its query. It is very easy to return an **IQueryable** instance
206 from many ORMs such as Entity Framework or NHibernate. The following
207 code snippet illustrates how the client can submit such queries.
208
209 ```Cs
210 var ordersummaryDTO = repository.Query<OrderSummaryDTO>().Where(LINQ query to retrieve order summary);
211 var orderdetailsDTO = repository.Query<OrderDetailsDTO>().Where(LINQ query to retrieve order details);
212 ```
213
214 This approach has a number of advantages:
215
216 * **Simplicity #1.** This approach uses a thin abstraction layer over
940f958c » dominicbetts
2012-04-02 Command Validation
217 the underlying database. It is supported by multiple ORMs and
218 minimizes the amount of code that you must write.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
219 * **Simplicity #2.** You only need to define a single repository and a
940f958c » dominicbetts
2012-04-02 Command Validation
220 single **Query** method.
2f560cf4 » dominicbetts
2012-04-11 Updates to IQueryable discussion
221 * **Simplicity #3.** You don't need a separate query object. On the
940f958c » dominicbetts
2012-04-02 Command Validation
222 read-side the queries should be simple because you have already
2f560cf4 » dominicbetts
2012-04-11 Updates to IQueryable discussion
223 de-normalized the data from the write-side to support the read-side
224 clients.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
225 * **Simplicity #4.** You can make use of LINQ to provide support for
940f958c » dominicbetts
2012-04-02 Command Validation
226 features such as filtering, paging, and sorting in the client.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
227 * **Testability.** You can use LINQ to Objects for mocking.
228
940f958c » dominicbetts
2012-04-02 Command Validation
229 > **MarkusPersona:** In the RI, using Entity Framework, we didn't need
230 > to write any code at all to expose the **IQueryable** instance. We
231 > also had just a single **ViewRepository** class.
232
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
233 Possible objections to this approach include:
234
235 * It is not easy to replace the data store with a non-relational
2f560cf4 » dominicbetts
2012-04-11 Updates to IQueryable discussion
236 database (that does not expose an **IQueryable** object. However, you
237 can choose to implement the write-model differently in each bounded
238 context using an approach that is appropriate to that bounded context.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
239 * The client might abuse the **IQueryable** interface be performing
940f958c » dominicbetts
2012-04-02 Command Validation
240 operations that can be done more efficiently as a part of the
241 de-normalization process. You should ensure that the de-normalized
242 data fully meets the requirements of the clients.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
243 * Using the **IQueryable** interface hides the queries away. However,
2f560cf4 » dominicbetts
2012-04-11 Updates to IQueryable discussion
244 since you de-normalize the data from the write-side, the queries
940f958c » dominicbetts
2012-04-02 Command Validation
245 against the relational database tables are unlikely to be complex.
6582a682 » dominicbetts
2012-04-05 Incorporated feedback from Horsdal in Pundit:
246 * It's hard to know if your integration tests cover all the different
247 uses of the **Query** method.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
248
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
249 #### Using Custom DAOs
250
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
251 An alternative approach is to have the **ViewRepository** expose custom
252 **Find** and **Get** methods as shown in the following code snippets.
253
254 ```Cs
255 var ordersummaryDTO = dao.FindAllSummarizedOrders(userId);
256 var orderdetailsDTO = dao.GetOrderDetails(orderId);
257 ```
258
259 You could also choose to use different DAO classes. This would make it
260 easier to access different data sources.
261
262 ```Cs
263 var ordersummaryDTO = OrderSummaryDAO.FindAll(userId);
264 var orderdetailsDTO = OrderDetailsDAO.Get(orderId);
265 ```
266
267 This approach has a number of advantages:
268
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
269 * **Simplicity #1.** Dependencies are clearer for the client. For
270 example, the client references an explicit **IOrderSummaryDAO**
271 instance rather than a generic **IViewRepository** instance.
272 * **Simplicity #2.** For the majority of queries, there are only one or
273 two predefined ways to access the object. Different queries typically
274 return different projections.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
275 * **Flexibility #1.** The **Get** and **Find** methods hide details such
940f958c » dominicbetts
2012-04-02 Command Validation
276 as the partitioning of the data store and the data access methods such
277 as an ORM or executing SQL code explicitly. This makes it easier to
278 change these choices in the future.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
279 * **Flexibility #2.** The **Get** and **Find** methods could use an ORM,
2f560cf4 » dominicbetts
2012-04-11 Updates to IQueryable discussion
280 LINQ, and the **IQueryable** interface behind the scenes to get the
281 data from the data store. This is a choice that you could make on a
940f958c » dominicbetts
2012-04-02 Command Validation
282 method by method basis.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
283 * **Performance #1.** You can easily optimize the queries that the
940f958c » dominicbetts
2012-04-02 Command Validation
284 **Find** and **Get** methods run.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
285 * **Performance #2.** The data access layer executes all queries. There
940f958c » dominicbetts
2012-04-02 Command Validation
286 is no risk that the client MVC controller action tries to run complex
287 and inefficient LINQ queries against the data source.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
288 * **Testability.** It is easier to specify unit tests for the **Find**
940f958c » dominicbetts
2012-04-02 Command Validation
289 and **Get** methods than to create suitable unit tests for the range
290 of possible LINQ queries that a client could specify.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
291
292 Possible objections to this approach include:
293
294 * Using the **IQueryable** interface makes it much easier to use grids
2f560cf4 » dominicbetts
2012-04-11 Updates to IQueryable discussion
295 that support features such as paging, filtering, and sorting in the
296 UI.
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
297
b480ed9f » dominicbetts
2012-04-13 Adding the partially fulfilled order story.
298 ## Making Information about Partially Fulfilled Orders Available to the Read-side
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
299
300 The UI displays data about orders that it obtains by querying the model
b480ed9f » dominicbetts
2012-04-13 Adding the partially fulfilled order story.
301 on the read-side. Part of the data that the UI displays to the
302 registrant is information about partially fulfilled orders: for each
303 seat type in the order, the number of seats requested and the number of
304 seats that are available. This information is temporary data that is
305 only used while the registrant is creating the order using the UI; the
306 business only needs to store information about seats that were actually
307 purchased, not the difference between what the registrant requested and
308 what the registrant purchased.
309
310 The consequence of this is that the informationa about how many seats
311 the registrant requested only needs to exist in the model on the
312 read-side.
313
314 > **JanaPersona:** You can't store this information in an HTTP session
315 > because the registrant may leave the site in between requesting the
316 > seats and completing the order.
317
318 A further consequence, is that the underlying storage on the read-side
319 cannot be simple SQL views because it includes data that is not stored
320 in the underlying table storage on the write-side. This means that this
321 information must be passed to the read-side by using events.
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
322
323 Figure 2 shows all the commands and events that the **Order** and
324 **SeatsAvailability** aggregates use and how the **Order** aggregate
325 pushes changes to the read-side by raising events.
326
327 ![Figure 2][fig2]
328
329 **The new architecture of the reservation process**
330
331 The **OrderPlaced**, **OrderUpdated**, **OrderPartiallyReserved**,
332 **OrderRegistrantAssigned**, and **OrderReservationCompleted** events
333 are handled by the **OrderViewModelGenerator** class that uses
334 **OrderDTO** and **OrderItemDTO** instances to persist changes to the
335 view tables.
336
337 > **BharathPersona:** If you look ahead to the next chapter, [Preparing
338 > for the V1 Release][j_chapter5], you'll see that the team extended the
339 > use of events and migrated the **Orders and Reservations** bounded
340 > context to use event sourcing.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
341
940f958c » dominicbetts
2012-04-02 Command Validation
342 ## CQRS Command Validation
343
344 When you implement the write-model, you should try to ensure that
345 commands very rarely fail. This gives the best user experience, and
346 makes it much easier to implement the asynchronous behavior in your
347 application.
348
349 One approach, adopted by the team, is to use the model validation
350 features in ASP.NET MVC 3.
351
352 You should be careful to distinguish between errors and business
353 failures. Examples of errors include:
354
355 * A message not being delivered due to a failure in the messaging
356 infrastructure.
357 * Data not being persisted due to a connectivity problem with the
358 database.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
359
940f958c » dominicbetts
2012-04-02 Command Validation
360 In many cases, especially in the cloud, you can handle these errors by
361 retrying the operation.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
362
940f958c » dominicbetts
2012-04-02 Command Validation
363 A business failure should have a predetermined business response. For
364 example:
365
366 * If a seat cannot be reserved because there are no seats left, then the
367 system should add the request to a wait-list.
368 * If a credit card payment fails, the user should be given the chance to
369 try a different card, or to set up payment by invoice.
370
371 > **BharathPersona:** Your domain experts should help you to identify
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
372 > possible business failures and determine the way that you handle
940f958c » dominicbetts
2012-04-02 Command Validation
373 > them.
374
2f560cf4 » dominicbetts
2012-04-11 Updates to IQueryable discussion
375 ## The Count-down Timer and the Read-model
376
377 The count-down timer that displays how much time remains to complete the
378 order to the registrant is part of the business data in the system, and
379 not just a part of the infrastructure. When a registrant creates an
380 order and reserves seats, the count-down begins. The count-down
381 continues, even if the registrant leaves the conference web site. The UI
382 must be able to display the correct count-down value if the registrant
383 returns to the site, therefore the reservation expiry time is a part of
384 the data that is available from the read-model.
385
940f958c » dominicbetts
2012-04-02 Command Validation
386 # Implementation Details
387
388 This section describes some of the significant features of the
389 implementation of the orders and reservations bounded context that are
390 described in this chapter. You may find it useful to have a copy of the
391 code so you can follow along. You can download a copy of the code from
392 the repository on github: [mspnp/cqrs-journey-code][repourl].
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
393
33d4651b » dominicbetts
2012-04-13 Pundit/Mani:
394 > **Note:** Do not expect the code samples to exactly match the code in
395 > the reference implementation. This chapter describes a step in the
396 > CQRS journey, the implementation may well change as we learn more and
397 > refactor the code.
398
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
399 ## The Order Access Code Record Locator
400
401 A registrant may need to retrieve an Order, either to view it, or to
402 complete registering attendees to seats. This may happen in a different
403 web session, so the registrant must supply some information to locate
404 the previously saved order.
405
406 The following code sample shows how the **Order** class generates an new
407 five character order access code that is persisted as part of the
408 **Order** instance.
409
410 ```Cs
411 public string AccessCode { get; set; }
412
413 protected Order()
414 {
415 ...
416 this.AccessCode = HandleGenerator.Generate(5);
417 }
418 ```
419
420 To retrieve an **Order** instance, a registrant must provide her email
421 address and the order access code. The system will use these two items
422 to locate the correct order. This logic is part of the read-side.
423
424 The following code sample from the **OrderController** class in the web
425 application shows how the MVC controller submits the query to the
426 repository to discover the unique **OrderId** value. This **Find**
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
427 action passes the **OrderId** value to a **Display** action that
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
428 displays the order information to the registrant.
429
430 ```Cs
431 [HttpPost]
432 public ActionResult Find(string conferenceCode, string email, string accessCode)
433 {
434 var repo = this.repositoryFactory();
435 using (repo as IDisposable)
436 {
437 var order = repo.Query<OrderDTO>()
438 .Where(o => o.RegistrantEmail == email && o.AccessCode == accessCode)
439 .FirstOrDefault();
440
441 if (order == null)
442 return RedirectToAction("Find", new { conferenceCode = conferenceCode });
443
444 return RedirectToAction("Display", new { conferenceCode = conferenceCode, orderId = order.OrderId });
445 }
446 }
447 ```
448
449 This illustrates two ways of querying the read-side. The first, as shown
450 in the previous code sample, enables you to use a LINQ query against an
451 **IQueryable** instance. The second, as used in the MVC controller's
452 **Display** action, retrieves a single instance based on its unique Id.
453 The following code sample shows the **Find** and **Query** methods in
454 the **OrmViewRepository** class that the MVC controller uses.
455
456 ```Cs
457 public T Find<T>(Guid id) where T : class
458 {
459 return this.Set<T>().Find(id);
460 }
461
462 public IQueryable<T> Query<T>() where T : class
463 {
464 return this.Set<T>();
465 }
466 ```
467
468 ## The Count-down Timer
469
470 When a registrant creates and order and makes a seat reservation, those
471 seats are reserved for a fixed period of time. The
8c675de8 » dominicbetts
2012-04-19 Terminology Update ReservationProcess to RegistrationProcess
472 **RegistrationProcess** instance, which forwards the reservation from
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
473 the **SeatsAvailability** aggregate, passes the time that the
474 reservation expires to the **Order** aggregate. The following code
475 sample shows how the **Order** aggregate receives and stores the
476 reservation expiry time.
477
478 ```Cs
6196063d » dominicbetts
2012-04-12 Updates to reflect changes in how reservation timeouts are modelled.
479 public DateTime? ReservationExpirationDate { get; private set; }
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
480
6196063d » dominicbetts
2012-04-12 Updates to reflect changes in how reservation timeouts are modelled.
481 public void MarkAsReserved(DateTime expirationDate, IEnumerable<SeatQuantity> seats)
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
482 {
6196063d » dominicbetts
2012-04-12 Updates to reflect changes in how reservation timeouts are modelled.
483 ...
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
484
6196063d » dominicbetts
2012-04-12 Updates to reflect changes in how reservation timeouts are modelled.
485 this.ReservationExpirationDate = expirationDate;
486 this.Items.Clear();
487 this.Items.AddRange(seats.Select(seat => new OrderItem(seat.SeatType, seat.Quantity)));
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
488 }
489
490 ```
491
6196063d » dominicbetts
2012-04-12 Updates to reflect changes in how reservation timeouts are modelled.
492 > **MarkusPersona:** The **ReservationExpirationDate** is intially set
493 > in the **Order** constructor to a time 15 minutes after the **Order**
494 > is instantiated. This time may be revised by the
8c675de8 » dominicbetts
2012-04-19 Terminology Update ReservationProcess to RegistrationProcess
495 > **RegistrationProcess** workflow based on when the reservations
6196063d » dominicbetts
2012-04-12 Updates to reflect changes in how reservation timeouts are modelled.
496 > are actually made. It is this time the workflow sends to the **Order**
497 > aggregate in the **MarkSeatsAsReserved** command.
498
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
499 The MVC **RegistrationController** class retrieves the order information
500 on the read-side. The **OrderDTO** class includes the reservation expiry
501 time that is then passed to the view using the **ViewBag** class, as
502 shown in the following code sample.
503
504 ```Cs
505 [HttpGet]
506 public ActionResult SpecifyRegistrantDetails(string conferenceCode, Guid orderId)
507 {
508 var repo = this.repositoryFactory();
509 using (repo as IDisposable)
510 {
511 var orderDTO = repo.Find<OrderDTO>(orderId);
512 var conferenceDTO = repo.Query<ConferenceDTO>()
513 .Where(c => c.Code == conferenceCode)
514 .FirstOrDefault();
515
516 this.ViewBag.ConferenceName = conferenceDTO.Name;
517 this.ViewBag.ConferenceCode = conferenceDTO.Code;
518 this.ViewBag.ExpirationDateUTCMilliseconds = orderDTO.BookingExpirationDate.HasValue ? ((orderDTO.BookingExpirationDate.Value.Ticks - EpochTicks) / 10000L) : 0L;
519 this.ViewBag.OrderId = orderId;
520
521 return View(new AssignRegistrantDetails { OrderId = orderId });
522 }
523 }
524 ```
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
525
d8c0c8da » dominicbetts
2012-03-30 Added brief summaries to Journey 02 of bounded contexts to date.
526 The MVC view then uses Javascript to display an animated count-down
527 timer.
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
528
940f958c » dominicbetts
2012-04-02 Command Validation
529 ## Using ASP.NET MVC 3 Validation for Commands
530
531 You should try to ensure that any commands that the MVC controllers in
c2f271ee » dominicbetts
2012-04-10 Updates to reflect code refactorings.
532 your application send to the write-model will succeed. You can use the
533 features in MVC to validate the commands both client-side and
534 server-side before sending them to the write-model.
940f958c » dominicbetts
2012-04-02 Command Validation
535
536 > **MarkusPersona:** Client-side validation is primarily a convenience
537 > to the user that avoids the need to for round trips to the server to
538 > help the user complete a form correctly. You still need server-side
539 > validation to ensure that the data is validated before it is forwarded
540 > to the write-model.
541
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
542 The following code sample shows the **AssignRegistrantDetails** command
940f958c » dominicbetts
2012-04-02 Command Validation
543 class that uses **DataAnnotations** to specify the validation
544 requirements; in this example, that the **FirstName**, **LastName**, and
545 **Email** fields are not empty.
546
547 ```Cs
548 using System;
549 using System.ComponentModel.DataAnnotations;
550 using Common;
551
552 public class AssignRegistrantDetails : ICommand
553 {
554 public AssignRegistrantDetails()
555 {
556 this.Id = Guid.NewGuid();
557 }
558
559 public Guid Id { get; private set; }
560
561 public Guid OrderId { get; set; }
562
563 [Required(AllowEmptyStrings = false)]
564 public string FirstName { get; set; }
565
566 [Required(AllowEmptyStrings = false)]
567 public string LastName { get; set; }
568
569 [Required(AllowEmptyStrings = false)]
570 public string Email { get; set; }
571 }
572 ```
573
574 The MVC view uses this command class as its model class. The following
575 code sample from the **SpecifyRegistrantDetails.cshtml** file shows how
576 the model is populated.
577
578 ```HTML
579 @model Registration.Commands.AssignRegistrantDetails
580
581 ...
582
583 <div class="editor-label">@Html.LabelFor(model => model.FirstName)</div><div class="editor-field">@Html.EditorFor(model => model.FirstName)</div>
584 <div class="editor-label">@Html.LabelFor(model => model.LastName)</div><div class="editor-field">@Html.EditorFor(model => model.LastName)</div>
585 <div class="editor-label">@Html.LabelFor(model => model.Email)</div><div class="editor-field">@Html.EditorFor(model => model.Email)</div>
586
587 ```
588
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
589 Client-side validation based on the **DataAnnotations** attributes is
590 configured in the **Web.config** file as shown in the following snippet.
940f958c » dominicbetts
2012-04-02 Command Validation
591
592 ```XML
593 <appSettings>
594 ...
595 <add key="ClientValidationEnabled" value="true" />
596 <add key="UnobtrusiveJavaScriptEnabled" value="true" />
597 </appSettings>
598 ```
599
600 The server-side validation occurs in the controller before the command
601 is sent. The following code sample from the **RegistrationController**
602 class shows how the controller uses the **IsValid** property to validate
603 the command. Remember that this example uses an instance of the command
604 as the model.
605
606 ```Cs
607 [HttpPost]
608 public ActionResult SpecifyRegistrantDetails(string conferenceCode, Guid orderId, AssignRegistrantDetails command)
609 {
610 if (!ModelState.IsValid)
611 {
612 return SpecifyRegistrantDetails(conferenceCode, orderId);
613 }
614
615 this.commandBus.Send(command);
616
617 return RedirectToAction("SpecifyPaymentDetails", new { conferenceCode = conferenceCode, orderId = orderId });
618 }
619 ```
620
c2f271ee » dominicbetts
2012-04-10 Updates to reflect code refactorings.
621 For an additional example, see the **RegisterToConference** command and
622 the **StartRegistration** action in the **RegistrationController**
623 class.
624
940f958c » dominicbetts
2012-04-02 Command Validation
625 For more information, see [Models and Validation in ASP.NET
626 MVC][modelvalidation] on MSDN.
627
b480ed9f » dominicbetts
2012-04-13 Adding the partially fulfilled order story.
628 ## Pushing Changes to the Read-side
629
630 Some information about orders only needs to exist on the read-side. In
631 particular, the information about partially fulfilled orders is only
632 used in the UI and is not part of the business information persisted by
633 the domain model on the write-side.
634
635 This means that the system can't use SQL views as the underlying storage
636 mechanism on the read-side because views cannot contain data that does
637 not exist in the tables that they are based on.
638
639 The system stores the de-normalized order data in two tables in a SQL
640 database: the **OrdersView** and **OrderItemsView** tables. The
641 **OrderItemsView** table includes the **RequestedSeats** column that
642 contains data that only exists on the read-side.
643
644 <table border="1">
645 <tr><th>Column</th><th>Description</th><tr>
646 <tr><td>OrderId</td><td>A unique identifier for the Order</td><tr>
647 <tr><td>ReservationExpirationDate</td><td>The time when the seat reservations expire</td><tr>
648 <tr><td>StateValue</td><td>The state of the Order: Created, PartiallyReserved, ReservationCompleted, Rejected, Confirmed</td><tr>
649 <tr><td>RegistrantEmail</td><td>The email address of the Registrant</td><tr>
650 <tr><td>AccessCode</td><td>The Access Code that the Registrant can use to access the Order</td><tr>
651 </table>
652
653 **OrdersView Table**
654
655 <table border="1">
656 <tr><th>Column</th><th>Description</th><tr>
657 <tr><td>OrderItemId</td><td>A unique identifier for the Order Item</td><tr>
658 <tr><td>SeatType</td><td>The type of Seat requested</td><tr>
659 <tr><td>RequestedSeats</td><td>The number of seats requested</td><tr>
660 <tr><td>ReservedSeats</td><td>The number of seats reserved</td><tr>
661 <tr><td>OrderID</td><td>The OrderId in the parent OrdersView table</td><tr>
662 </table>
663
664 **OrderItemsView Table**
665
666 To populate these tables in the read-model, the read-side handles events
667 raised by the write-side and uses them to write to these tables. See
668 Figure 2 above for more details.
669
670 The **OrderViewModelGenerator** class handles these events and updates
671 the read-side repository.
672
673 ```Cs
674 public class OrderViewModelGenerator :
675 IEventHandler<OrderPlaced>, IEventHandler<OrderUpdated>,
676 IEventHandler<OrderPartiallyReserved>, IEventHandler<OrderReservationCompleted>,
677 IEventHandler<OrderRegistrantAssigned>
678 {
679 private Func<IViewRepository> repositoryFactory;
680
681 public OrderViewModelGenerator(Func<IViewRepository> repositoryFactory)
682 {
683 this.repositoryFactory = repositoryFactory;
684 }
685
686 public void Handle(OrderPlaced @event)
687 {
688 var repository = this.repositoryFactory();
689 using (repository as IDisposable)
690 {
691 var dto = new OrderDTO(@event.OrderId, Order.States.Created)
692 {
693 AccessCode = @event.AccessCode,
694 };
695 dto.Lines.AddRange(@event.Seats.Select(seat => new OrderItemDTO(seat.SeatType, seat.Quantity)));
696
697 repository.Save(dto);
698 }
699 }
700
701 public void Handle(OrderRegistrantAssigned @event)
702 {
703 ...
704 }
705
706 public void Handle(OrderUpdated @event)
707 {
708 ...
709 }
710
711 public void Handle(OrderPartiallyReserved @event)
712 {
713 ...
714 }
715
716 public void Handle(OrderReservationCompleted @event)
717 {
718 ...
719 }
720
721 ...
722 }
723 ```
724
725 The following code sample shows the **OrmViewRepository** class.
726
727 ```Cs
728 public class OrmViewRepository : DbContext, IViewRepository
729 {
730 ...
731
732 public T Find<T>(Guid id) where T : class
733 {
734 return this.Set<T>().Find(id);
735 }
736
737 public IQueryable<T> Query<T>() where T : class
738 {
739 return this.Set<T>();
740 }
741
742 public void Save<T>(T entity) where T : class
743 {
744 var entry = this.Entry(entity);
745
746 if (entry.State == System.Data.EntityState.Detached)
747 this.Set<T>().Add(entity);
748
749 this.SaveChanges();
750 }
751 }
752 ```
753
754 **JanaPersona:** Notice that this repository class in the read-side
755 includes a **Save** method to persist the changes sent from the
756 write-side and handled by the **OrderViewModelGenerator** handler class.
757
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
758 ## Refactoring the SeatsAvailability Aggregates
759
760 In the first stage of our CQRS, the domain included a
761 **ConferenceSeatsAvailabilty** aggregate root class that modelled the
762 number of seats remaining for a conference. In this stage of the
763 journey, the team replaced the **ConferenceSeatsAvailabilty** aggregate
764 with a **SeatsAvailability** aggregate to reflect the fact that there
765 may be multiple seat types available at a particular conference; for
766 example, full conference seats, pre-conference workshop seats, and
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
767 cocktail party seats. Figure 3 shows the new **SeatsAvailability**
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
768 aggregate and its constituent classes.
769
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
770 ![Figure 3][fig3]
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
771
772 **The **SeatsAvailability** and its associated commands and events.
773
774 This aggregate now models the following facts:
775
776 * There may be multiple seat types at a conference.
777 * There may be different numbers of seats available for each seat type.
778
779 The domain now includes a **SeatQuantity** value type that you can use
780 to represent a quantity of a particular seat type.
781
782 Previously, the aggregate raised either a **ReservationAccepted** or
783 **ReservationRejected** event depending on whether there were sufficient
784 seats. Now the aggregate raises a **SeatsReserved** event that reports
785 how many seats of a particular type could be reserved. This means that
786 the number of seats reserved may not match the number of seats
787 requested; this information is passed back to the UI for the registrant
788 to make a decision on how to proceed with the registration.
789
790 ### The AddSeats Method
791
792 You may have noticed in Figure 2 that the **SeatsAvailability**
793 aggregate includes an **AddSeats** method with no corresponding command.
794 The **AddSeats** method adjusts the total number of available seats of a
a1286259 » dominicbetts
2012-04-10 Change conference owner to business customer as per UL.
795 given type. The Business Customer is responsible for making any such
15bc833a » dominicbetts
2012-04-05 Added section on support for multiple seat types.
796 adjustments, and does this in the Conference Management bounded context.
797 The Conference Management bounded context raises an event whenever the
798 total number of available seats changes, the **SeatsAvailability** class
799 then handles the event when its handler invokes the **AddSeats** method.
800
e08aa35e » dominicbetts
2012-03-28 Chapter Rename and initial commit of Journey Chapter 4.
801 # Testing
802
803 Describe any special considerations that relate to testing for this bounded context.
bb575254 » dominicbetts
2012-03-29 Moved IQueryable to Journey 04
804
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
805 [j_chapter5]: Journey_05_PaymentsBC.markdown
e47a0d7d » dominicbetts
2012-04-03 Fixed links and images
806 [r_chapter4]: Reference_04_DeepDive.markdown
807
808 [fig1]: images/Journey_04_ViewRepository.png?raw=true
e0576047 » dominicbetts
2012-04-12 Extended pushing changes to the read-side section
809 [fig2]: images/Journey_04_Architecture.png?raw=true
810 [fig3]: images/Journey_04_SeatsAvailability.png?raw=true
940f958c » dominicbetts
2012-04-02 Command Validation
811 [modelvalidation]: http://msdn.microsoft.com/en-us/library/dd410405(VS.98).aspx
Something went wrong with that request. Please try again.