Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editoral review of the exercises #13

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions NewExercises/Exercise-1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

Let's start simple. The solution contains a very simple "e-commerce" web application that sells pierogi.

Users have to log in by providing their name. Then, they can place orders by adding items to a shopping cart and finally submitting the shopping cart for processing.
Users must log in by providing their name. Once they are authenticated, they can place items in their shopping cart. Once their shopping cart is ready, they can submit the order for processing.

The domain model of the application consists of two aggregates: `ShoppingCart` and `Order`. The shopping cart is created immediately when a user navigates to the shopping page. Items are added to the cart. Finally, when the user decides to submit the order, the `Order` aggregate is created based on the contents of the `ShoppingCart` aggregate.
The domain model of the application consists of two [aggregates](https://martinfowler.com/bliki/DDD_Aggregate.html): `ShoppingCart` and `Order`. The shopping cart is immediately created when a user navigates to the shopping page. Items are added to the cart. Finally, when the user decides to submit the order, the `Order` aggregate is created based on the contents of the `ShoppingCart` aggregate.

Order processing happens asynchronously via a batch job but is not shown in this simplistic example.

Try placing few orders to get the feeling of the application. Maybe put some breakpoints and see the flow.
Try placing few orders to get a sense of how the application works. Consider placing some breakpoints, or even breaking the application, to observe the flow.

Perhaps try to break the application? What happens if you open the same order in two browser tabs and click `Submit` on both pages? Can you explain the behavior?
Consider what happens if you open the same shopping cart in two browser tabs and click `Submit` on both pages? Can you explain the behavior?
8 changes: 5 additions & 3 deletions NewExercises/Exercise-10/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Exercise 10: Primary key-based deduplication

Create-type operation can be de-duplicated based on the ID of the entity/aggregate to be created. Add such logic to the order. And test.
Create-type operations can be deduplicated based on the ID of the entity/aggregate to be created.

- Go to the `SubmitOrderHandler` class and change the `Guid`-based order ID generation strategy with the value of the `CartId` property of the `SubmitOrder` message.
Try to add such deduplication logic to the order and test the behavior.

- In the `SubmitOrderHandler` class, change the `Guid`-based order ID generation strategy with the value of the `CartId` property of the `SubmitOrder` message.
- Run the solution to see the result
- Modify the code of the `SubmitOrderHandler` to discard the message if an order already exists by using `repository.Get` method. Remember to check the `version` part of the return because the `Get` method always returns a non-null item.
- Modify the code of the `SubmitOrderHandler` to discard the message if an order already exists by using `repository.Get` method. Remember to check the `version` because the `Get` method always returns a non-null item.
13 changes: 7 additions & 6 deletions NewExercises/Exercise-11/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ When writing integration tests for message-based systems it's common to make ass

### Goal

The goal of this exercise is to write a single integration test that depends on the result of processing the last message in a conversation. The conversation starts with `PlaceOrder` command that has a 1 in 20 chance of triggering the final `FinalizeOrder` command or re-sending the same command with `1` seconds delay:
The goal of this exercise is to write a single integration test that depends on the processing result of the last message in a conversation. The conversation starts with the `PlaceOrder` command that has a 1 in 20 chance of triggering the final `FinalizeOrder` command or re-sending the same command with `1` seconds delay:

```csharp
if (new Random().Next(0, 20) == 0)
Expand Down Expand Up @@ -43,11 +43,12 @@ static IEnumerable<string> TestCases => Enumerable.Range(1, 25).Select(n => $"{n

### Step 1

Can we solve the problem with a simple patch? Can we add `Task.Delay` in our test to make it pass consistently and not make it take a couple of minutes to pass?
Can we solve the problem with a simple patch? Can we add `Task.Delay` in our test to make it pass consistently?

### Step 2

Let's add behavior to the message processing pipeline in our endpoint so that for every incoming message and all messages that are generated we capture their identifiers and sent this data off to a dedicated queue for further processing using `TracingBehavior`:
Let's add behavior to the message processing pipeline in our endpoint to capture identifiers for every incoming message and its resulting outgoing messages and send this data off to a dedicated queue for further processing.
This `TracingBehavior` is registered on the endpoint:

```csharp
(endpoint, store) = await Program.StartEndpoint(c =>
Expand All @@ -56,16 +57,16 @@ Let's add behavior to the message processing pipeline in our endpoint so that fo
});
```

TASK: Add a missing piece of logic to the `TracingBehavior` to capture outgoing message identifiers in the `OutgoingMessageIds` property of the `TracingMessage`. Run the test in the `Debug` mode to make sure the data is being captured.
TASK: Add the missing logic to the `TracingBehavior` to capture outgoing message identifiers in the `OutgoingMessageIds` property of the `TracingMessage`. Run the test in `Debug` mode to ensure the data is captured.

### Step 3

Tracing messages sent to the `trace` queue will be processed by a dedicated endpoint encapsulated in the `Tracer` class.

TASK: Create an instance of the `Tracer` class in the `Setup` method of the test and make sure it's properly clean-up in the `Cleanup`
TASK: Create an instance of the `Tracer` class in the `Setup` method of the test and make sure it's properly cleaned up in the `Cleanup` method.

### Step 4

`Tracer` provides logic to setup conversation tracking and later to wait until the conversation finishes. In order, to set up, the conversation one needs to call the `Prepare` method which returns `conversationId` and `SendOptions` tuple.
`Tracer` provides logic to set up conversation tracking and wait until the conversation finishes. In order to set up, the conversation one needs to call the `Prepare` method which returns `conversationId` and `SendOptions` tuple.

TASK: Extend the test code by calling the `tracer.Prepare` and `tracer.WaitUntilFinished` to make sure that test asserts are called at the right time.
18 changes: 10 additions & 8 deletions NewExercises/Exercise-12/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
NOTE: This and couple of following exercises use automated tests for show how our system behaves in various scenarios that might happen in messaging systems.
NOTE: Starting this exercise, we use automated tests to showcase how the system behaves in various scenarios that can occur in messaging systems.

Our system has been extended with new functionality. After order has been placed, we can book a payment for a given order or cancel a payment that has already been booked. So far the deduplication mechanism depended on the idempotence of the operations conducted in the message handling logic. If we mark the payment as booked, we can do it multiple times and the result is the same as if we marked it once. Similarly, when we mark the payment as non-booked, we can repat the operation multiple times and still the payment stays non-booked.
Our system was extended with new functionality. After an order has been placed, a payment for a given order can be booked or cancelled. So far, the deduplication mechanism depended on the idempotency of the message handling logic.
Marking the payment as booked, is an operation that has the same observable result, regardless of how many times we execute it.
Similarly, when we mark the payment as not-booked, we can repeat the operation multiple times without unexpected side effects.

Let's see what happens when some of these messages get reordered:
Let's see what happens when some of these messages are reordered:

* Open `IntegrationTests.cs` in the `Test` project and naviage to `ChangeStatus` test
* Use `SendInOrder` utility method to simulate scenario in which oder is placed, payment is booked and later cancelled but the `BookPayment` command in duplicated and the duplicate arrives as the last message:
* Open `IntegrationTests.cs` in the `Test` project and navigate to the `ChangeStatus` test
* Use the `SendInOrder` method to simulate a scenario in which an order is placed, the payment is booked and later cancelled. We want the `BookPayment` command to be duplicated and the duplicate to arrive as the last message:
```csharp
await SendInOrder(new IMessage[]
{
Expand All @@ -16,11 +18,11 @@ await SendInOrder(new IMessage[]
}
);
```
* Run `ChangeStatus` test and check if the assertion holds. If not then can you tell what is wrong?
* Add `List<Guid>` property to `Order` enity called `ProcessedMessages`
* Run the `ChangeStatus` test and check if the assertion holds. If not, can you tell what's wrong?
* Add a `List<Guid>` property called `ProcessedMessages` to the `Order` entity
```csharp
public List<Guid> ProcessedMessages { get; set; } = new List<Guid>();
```
* Go to the `BookPaymentHandler` and `CancelPaymentHandler` and modify the deduplication logic to use message Ids. At the end of the message processing in the handler, but before the `Order` is persisted, include the Id of the message being processed in the `ProcessedMessages` collection. At the beginning of the handler code check if the message already exists in the collection. If so, return without doing anything in the handler.
* In the `BookPaymentHandler` and `CancelPaymentHandler`, modify the deduplication logic to use message Ids. At the end of the message processing logic, right before the `Order` is persisted, include the Id of the message being processed in the `ProcessedMessages` collection. At the beginning of the handler, check whether the message already exists in the collection. If so, exit the handler, discarding the message.

What aspect of a message handler is missing in this exercise?
13 changes: 7 additions & 6 deletions NewExercises/Exercise-13/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
## Deterministic message identifiers

Due to considerable sucess of our the business, the system has been extended with new `Marketing` endpoint, reponsible for tracking value of payments booked for any given customer. When status of an order is changed, either `PaymentBooked` or `PaymentCancelled` event is published.
Due to the success of our the business, the system has been extended with a new `Marketing` endpoint, responsible for tracking the total expenditure of our customers (the total of all payments).
When the status of an order is changed, either `PaymentBooked` or `PaymentCancelled` event is published.

Unfortunatelly, our production support team claims that once in a while the calculated value for a customer does not match the total from all the payments. Let's see if we can reproducte such a scenario.
Unfortunately, our support team claims that once in a while, the calculated total expenditure per customer does not match the actual total of all payments. Let's see if we can reproduce such a scenario.

* Go to the `BookPaymentHandler` or `CancelPaymentHandler` and take a look at the logic. Notice that it is almost the same as in the previous exercise but it also includes publishing of events. Notice that the basic structure remains the same regardless what type of duplication mechanism is used:
* Go to the `BookPaymentHandler` or `CancelPaymentHandler` and consider the logic. Notice that it is mostly identical to the logic in the previous exercise but it includes events being published. Notice that the basic structure remains the same regardless of the deduplication mechanism used:

```
if (!IsDuplicate())
Expand All @@ -16,7 +17,7 @@ if (!IsDuplicate())
SendAndPublishMessages()'
```

* Go to `TrackTotalPaymentsValue` test and check if it passes. Why does it fail? Open the results of the test run and check what is are the `messageId` values for both duplicates of the `BookPayment` message. Why are they different? Doest it resemble any situation we have seen before?
* In the `BookPaymentHandler` and `CancelPaymentHandler` use `PublishWithId` extension method and use `Utils` class to ensure that the published messages have Ids that are deterministically derived from the incoming message Id.
* Is our deterministic Id generation strategy good?
* Check if the `TrackTotalPaymentsValue` test passes. Why does it fail? Open the results of the test run and check the `messageId` values for both duplicates of the `BookPayment` message. Why are they different? Does this resemble any situation we've seen before?
* In the `BookPaymentHandler` and `CancelPaymentHandler`, use the `PublishWithId` extension method and the `Utils` class to ensure that the published messages have Ids that are deterministically derived from the incoming message Id.
* Is our deterministic Id generation strategy a good solution?
* Does the `ChangeStatus` test still pass?
34 changes: 18 additions & 16 deletions NewExercises/Exercise-14/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
## State based message generation
## State-based message generation

Now that we can reliably calculate value of all the payments made by a customer the business wants to put that to a good use. Our team needs to add a small feature i.e. when a customer goes over 100 USD in total paymets for the first time we want to send them a coupon.
Now that we can reliably calculate the total of all payments made by a customer, the business wants to put this information to good use.
Our team needs to add a small feature i.e. when a customer exceeds 100 USD in total payments, we want to send them a coupon.

* Go to the `IssueCouponCardAfterFirst100USDSpent` test and define the follwong sequence of message processing. What could be a production scenario in which this could happen?
* In the `IssueCouponCardAfterFirst100USDSpent` test, define the following sequence of message processing:

```csharp
new IMessage[] {
Expand All @@ -12,12 +13,13 @@ new IMessage[] {
bookSecondPayment
}
```

* Run the test and check if the asserition holds
* Put logic in the `DropMessagesBehavior` to ensure that the `GrantCoupon` message is skipped (dropped) the first time it is sent. This simulates situation when `PaymentBookHandler` failes on sending out `GrantCoupon` and the incoming message is retried. Use the same method as in [Exercise 5](../Exercise-5/README.md):
What could be a production scenario in which this could happen?

* Run the test and check if the assertion holds
* Add logic in the `DropMessagesBehavior` to ensure that the `GrantCoupon` message is discarded the first time it is sent. This simulates a situation when `PaymentBookHandler` fails when sending out `GrantCoupon` and the incoming message is retried. Use the same method as in [Exercise 5](../Exercise-5/README.md):
* Add a boolean flag `dropped`
* In the `Invoke` method check if the `context.Message.Instance` is `GrantCoupon`. If so, and the flag is `false` then flip the flag and return. Otherwise invoke `await next()`. This logic will ensure that only the first instance of the `GrantCoupon` message is dropped.
* To account for this simulated failure we should add another copy of the `bookFirstPayment` to the messages list for the test:
* In the `Invoke` method, check if the `context.Message.Instance` is `GrantCoupon`. If so, and the flag is `false`, flip the flag and return. Otherwise, invoke `await next()`. This logic will ensure that only the first instance of the `GrantCoupon` message is dropped.
* To account for this simulated failure, we will add an additional copy of the `bookFirstPayment` to the messages list for the test:

```csharp
new IMessage[] {
Expand All @@ -29,19 +31,19 @@ new IMessage[] {
}
```

* With that change the test should pass again. Re-run the test. Does it work? Can you tell why? Try placing some breakpoints in the `PaymentBookedHandler`:
* in line 18 to check how many times the handler is invoked
* in line 36 to see how many times the business logic is invoked
* in line 38 to see how many times the `GrantCoupon` message is generated
* With that change in place, the test should pass again. Re-run the test. Does it work? Can you tell why? Try placing some breakpoints in the `PaymentBookedHandler`:
* on line 18 to check how many times the handler is invoked
* on line 36 to see how many times the business logic is invoked
* on line 38 to see how many times the `GrantCoupon` message is generated
* Can you explain the behavior?
* Use `public List<ICommand> OutgoingMessages = new List<ICommand>();` property in the `Payments` entity to store the outoging messages.
* Instead of sending the message immediately in line 38, add it to the `OutgoingMessages` collection.
* Make sure that items in the `OutgoingMessages` are _always_ sent out (including the times when duplicates arrive). Add following code to the bottom of the handler:
* Use a `public List<ICommand> OutgoingMessages = new List<ICommand>();` property in the `Payments` entity to store the outgoing messages.
* Instead of sending the message immediately, on line 38, add it to the `OutgoingMessages` collection.
* Make sure that items in the `OutgoingMessages` are _always_ sent out (including the cases in which a duplicate is detected). Add the following code to the bottom of the handler:

```csharp
foreach (var outgoingMessage in payments.OutgoingMessages)
{
await context.SendImmediately(outgoingMessage);
}
```
* Run all the test in the `Tests` project
* Run all the tests in the `Tests` project.
Loading