Skip to content

Commit

Permalink
Merge pull request #350 from Particular/dvdstelt-small-updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dvdstelt authored Jan 12, 2023
2 parents c03566b + c8d90ee commit 03bb803
Show file tree
Hide file tree
Showing 13 changed files with 41 additions and 126 deletions.
13 changes: 7 additions & 6 deletions exercises/01-composite-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Vertical slices are autonomous and span across all layers starting from the UI,
In this exercise you'll see that, when adding new elements, we operate within a single slice. This approach reduces the risk of unintentionally breaking unrelated elements.

The application consists of three vertical slices:

- Customers
- Sales
- Finance
Expand Down Expand Up @@ -95,7 +96,7 @@ using Microsoft.AspNetCore.Routing;
using System;
using System.Net.Http;

namespace Divergent.Customers.ViewModelComposition;
namespace Divergent.Finance.ViewModelComposition;

public class OrdersLoadedSubscriber : ISubscribeToCompositionEvents
{
Expand All @@ -112,14 +113,14 @@ public class OrdersLoadedSubscriber : ISubscribeToCompositionEvents
var orderIds = string.Join(",", ordersLoaded.OrderViewModelDictionary.Keys);

// Hardcoded to simplify the exercise. In a production app, a config object could be injected.
var url = $"http://localhost:20186/api/customers/byorders?orderIds={orderIds}";
var url = $"http://localhost:20187/api/prices/orders/total?orderIds={orderIds}";
var response = await new HttpClient().GetAsync(url);

dynamic[] customers = await response.Content.AsExpandoArrayAsync();
dynamic[] prices = await response.Content.AsExpandoArrayAsync();

foreach (dynamic customer in customers)
foreach (dynamic price in prices)
{
ordersLoaded.OrderViewModelDictionary[customer.OrderId].OrderCustomerName = customer.CustomerName;
ordersLoaded.OrderViewModelDictionary[price.OrderId].OrderTotalPrice = price.Amount;
}
});
}
Expand All @@ -132,7 +133,7 @@ Update the `Divergent.Frontend\wwwroot\app\presentation\ordersView.html` list te

```html
<br />
<strong>Order total:</strong> {{order.orderTotalPrice}}
<strong>Order total:</strong> ${{order.orderTotalPrice}}
```

#### Note:
Expand Down
67 changes: 18 additions & 49 deletions exercises/03-sagas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ class ShippingSaga : Saga<object>,
}
```

Override saga base class `ConfigureHowToFindSaga` abstract method and implement both interfaces, leaving methods bodies as is. Before we can implement these, we need to add some saga state.

## Exercise 3.2: add state to the saga

A saga is coordinating long-running business processes. The saga you implement, takes decisions based on data that it receives. Imagine a business process that is running for days, years even. Some business processes actually never end. Our saga can't keep all that data in memory forever, so it needs to store this data somewhere.
Expand All @@ -93,31 +91,14 @@ We need to define what state we want to store for our saga. This saga is about a

### Step 1

Define a new class called `ShippingSagaData` and have it inherit from `ContainSagaData`. Add two properties of type `int` for CustomerId and OrderId. You should have a class like this.
Define a new class called `ShippingSagaData` and have it inherit from `ContainSagaData`. Add two properties of type `int` for CustomerId and OrderId. Also add a property to store the products involved. We're not going to use the list of products right now, but we will later.

```c#
class ShippingSagaData : ContainSagaData
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
}
```

### Step 2

Now we'll add products. A minor problem is that we can't add `IList<int>` for the products as NServiceBus persisters can't map this properly to tables or documents in SQL Server and/or RavenDB. We need a complex type. Create a class called `Product`, and in `ShippingSagaData` add a property of `ICollection<Product>` to contain the products. Of course the `Product` class needs to hold the unique id of each ordered product, so we need to add a property for that as well. We'll end up with a class like this:

```c#
class ShippingSagaData : ContainSagaData
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
public ICollection<Product> Products { get; set; }

public class Product
{
public int Identifier { get; set; }
}
public List<int> Products { get; set; }
}
```

Expand All @@ -133,21 +114,28 @@ class ShippingSaga : Saga<ShippingSagaData>,
}
```

You should now get several Roslyn analyzer warnings. Implement the missing methods with your favorite refactoring tool.

### Step 4

Make sure everything compiles.
The `ConfigureHowToFindSaga` method has been added and we need to configure a property mapping for incoming messages to a specific Saga instance. We do this using the following code:

```c#
mapper.MapSaga(saga => saga.OrderId)
.ToMessage<OrderSubmittedEvent>(p => p.OrderId)
.ToMessage<PaymentSucceededEvent>(p => p.OrderId);
```

### Step 5

Store the `CustomerId` and products in the state of the saga. We can do this by accessing the saga's `Data` property, as in `Data.CustomerId`. Set the appropriate properties in both `Handle` methods. Note that we _do not_ set `Data.OrderId` (see next exercise).
We'll first deal with the `OrderSubmittedEvent`. Store the `CustomerId` and products in the state of the saga. We can do this by accessing the saga's `Data` property, as in `Data.CustomerId`. Set the appropriate properties in both `Handle` methods. Note that we _do not_ set `Data.OrderId` (see next exercise).

```c#
public Task Handle(OrderSubmittedEvent message, IMessageHandlerContext context)
{
Data.CustomerId = message.CustomerId;

var projection = message.Products.Select(p => new ShippingSagaData.Product { Identifier = p });
Data.Products = projection.ToList();
Data.Products = message.Products;

return Task.CompletedTask;
}

Expand All @@ -157,27 +145,7 @@ public Task Handle(PaymentSucceededEvent message, IMessageHandlerContext context
}
```

## Exercise 3.3: map incoming messages to the saga

In this exercise you will map incoming messages to the saga so that NServiceBus knows which property to use to find and retrieve the correct saga instance from the saga data storage.

### Step 1

Open the `ShippingSaga` class and locate the `ConfigureHowToFindSaga` method.

### Step 2

Map the `OrderSubmittedEvent` property `OrderId` to the sagas `OrderId` property. Do this by overriding The `ConfigureHowToFindSaga` method on the saga. It provides a `mapper` object as an argument. The mapper object exposes a `ConfigureMapping` method, which takes the event type and property to match, as well as the saga property you want to map it to. Map the `PaymentSucceededEvent` property `OrderId` to the sagas `OrderId` property.:

```c#
mapper.MapSaga(saga => saga.OrderId)
.ToMessage<OrderSubmittedEvent>(p => p.OrderId)
.ToMessage<PaymentSucceededEvent>(p => p.OrderId);
```

Note that this mapping also tells NServiceBus how to set the value of `Data.OrderId`. This is why we did not have to set `Data.OrderId` ourselves in exercise 2.2.

## Exercise 3.4 - deal with out-of-order delivery
## Exercise 3.3 - deal with out-of-order delivery

In this exercise you will process the messages coming in and make sure the messages can arrive in any order, by verifying if all expected messages have been received.

Expand Down Expand Up @@ -234,8 +202,9 @@ In the saga add a new async method called `ProcessOrder` that you will call from
}
```

Note that NServiceBus also has Roslyn analyzers to verify if you're not forgetting the `await` keyword on the call to `ProcessOrder`.

## Advanced Exercise 3.5
## Advanced Exercise 3.4

Sagas are excellent for coordinating a business process. In the current saga we're only able to execute the happy path of the business process, where everything works. Payment succeeds and acknowledgement of this arrives at our Shipping service within a short time. But what if the `PaymentSucceededEvent` never arrives? Our Finance service is the authority which should decide when a payment takes too long, but it has no knowledge of how to contact customers. The Customers service however, would probably have details of how every customer would like to be contacted. This is just an example, but you can probably get an idea of how important it is to properly define your boundaries with real projects.

Expand All @@ -251,7 +220,7 @@ To complete this exercise, take the following steps:

Bear in mind that in production, a small business requirement like this could spawn many more messages. Imagine what should happen after the customer has been contacted. Should we retry the payment? Should we remember how many times the customer has been contacted because of failed payments? Could there be an alternative path for a payment? Should the order be cancelled? Should we let Shipping know what's happening? Importantly, these are _business decisions_. It is not the use of messaging that requires us to write all the additional logic. Rather, messaging allows us to represent these business decisions and actions as explicit messages, handlers and sagas. We now have the opportunity to implement the requirements transparently, loosely coupled and via autonomous services. Using other approaches, this can easily become very messy, very quickly, perhaps requiring several batch jobs running continuously.

## Advanced Exercise 3.6 : alternative payment providers
## Advanced Exercise 3.5 : alternative payment providers

The Finance bounded context contacts a payment provider to execute payments. The `Divergent.Finance` project does this in the `InitiatePaymentProcessCommandHandler` class, by calling the `ReliablePaymentClient` class and executing the payment using the `ProcessPayment` method.

Expand Down
13 changes: 5 additions & 8 deletions exercises/04-integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ In this exercise, we'll have the saga in `Divergent.Shipping` service tell IT/Op

### Step 1

In the `Divergent.ItOps.Messages` project, create a new class `ShipWithFedexCommand` in the Commands folder. It should contain the order Id, customer Id, and a list of product Ids.
In the `Divergent.ItOps.Messages` project, create a folder `Commands` and add a new class `ShipWithFedexCommand` in the Commands folder. It should contain the order Id, customer Id, and a list of product Ids.

```c#
using System.Collections.Generic;
Expand Down Expand Up @@ -69,7 +69,7 @@ private async Task ProcessOrder(IMessageHandlerContext context)
{
OrderId = Data.OrderId,
CustomerId = Data.CustomerId,
Products = Data.Products.Select(s => s.Identifier).ToList(),
Products = Data.Products
});

MarkAsComplete();
Expand All @@ -79,7 +79,7 @@ private async Task ProcessOrder(IMessageHandlerContext context)

### Step 3

In the `Divergent.Shipping` project, configure the destination endpoint for the `ShipWithFedexCommand`. To do this use the `routing` object obtained when configuring the transport and add the following statement
In the `Divergent.Shipping` project, configure the destination endpoint for the `ShipWithFedexCommand`. To do this use the `endpoint` object for configuring NServiceBus and add the following statement

```
endpoint.Configure(routing =>
Expand All @@ -90,7 +90,7 @@ endpoint.Configure(routing =>

### Step 4

In the `Divergent.ITOps` project, add a class under Handlers called `ShipWithFedexCommandHandler`. It should contain a message handler for `ShipWithFedexCommand` that calls a fake FedEx Web Service. Hard-code the customer information for now.
In the `Divergent.ITOps` project, create a folder `Handlers` and add a class called `ShipWithFedexCommandHandler`. It should contain a message handler for `ShipWithFedexCommand` that calls a fake FedEx Web Service. Hard-code the customer information for now.

```c#
public class ShipWithFedexCommandHandler : IHandleMessages<ShipWithFedexCommand>
Expand Down Expand Up @@ -201,7 +201,7 @@ public class ShipWithFedexCommandHandler : IHandleMessages<ShipWithFedexCommand>
{
Log.Info("Handle ShipWithFedexCommand");

var customerInfo = await _customerProvider.GetCustomerInfo(message.CustomerId);
var customerInfo = _customerProvider.GetCustomerInfo(message.CustomerId);

var fedExRequest = CreateFedexRequest(customerInfo);
await CallFedexWebService(fedExRequest);
Expand Down Expand Up @@ -235,11 +235,8 @@ public class ShipWithFedexCommandHandler : IHandleMessages<ShipWithFedexCommand>

Run the solution and verify that the ITOps message handler fetches the customer information using the supplied provider.

NOTE: For your convenience IT/Ops is already configured with the required connection strings to allow providers to function properly.

Check out `ReflectionHelper` and look at how the container is created in the `Host` in `Divergent.ITOps` to learn more about how IT/Ops loads and co-hosts the providers.


## Exercise 4.3: implement shipping provider

In this exercise, you'll implement the shipping provider in the Shipping service. ITOps defines an `IProvideShippingInfo` interface. The provider will be implemented in the Shipping service using the two helpers `VolumeEstimator` and `WeightCalculator` already present there.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
{
"ConnectionStrings": {
"Divergent.ITOps": "Data Source=(localdb)\\particular-workshop;Initial Catalog=Exercise-04-after-itops;Integrated Security=True;MultipleActiveResultSets=true",
"Divergent.Customers": "Data Source=(localdb)\\particular-workshop;Initial Catalog=Exercise-04-after-customers;Integrated Security=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ public Task Handle(OrderSubmittedEvent message, IMessageHandlerContext context)

Data.IsOrderSubmitted = true;
Data.CustomerId = message.CustomerId;

var projection = message.Products.Select(p => new ShippingSagaData.Product { Identifier = p });
Data.Products = projection.ToList();
Data.Products = message.Products;

return ProcessOrder(context);
}
Expand All @@ -48,7 +46,7 @@ await context.Send(new ShipWithFedexCommand
{
OrderId = Data.OrderId,
CustomerId = Data.CustomerId,
Products = Data.Products.Select(s => s.Identifier).ToList(),
Products = Data.Products,
});

MarkAsComplete();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class ShippingSagaData : ContainSagaData
public int CustomerId { get; set; }
public bool IsOrderSubmitted { get; set; }
public bool IsPaymentProcessed { get; set; }
public ICollection<Product> Products { get; set; }
public List<int> Products { get; set; }

public class Product
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Folder Include="Commands" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,4 @@
<ProjectReference Include="..\Divergent.Shipping.Data\Divergent.Shipping.Data.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Handlers" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
{
"ConnectionStrings": {
"Divergent.ITOps": "Data Source=(localdb)\\particular-workshop;Initial Catalog=Exercise-04-after-itops;Integrated Security=True;MultipleActiveResultSets=true",
"Divergent.Customers": "Data Source=(localdb)\\particular-workshop;Initial Catalog=Exercise-04-after-customers;Integrated Security=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,4 @@
<ProjectReference Include="..\Divergent.ITOps.Interfaces\Divergent.ITOps.Interfaces.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="ITOps" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@ public Task Handle(OrderSubmittedEvent message, IMessageHandlerContext context)
Log.Info("Handle OrderSubmittedEvent");

Data.IsOrderSubmitted = true;
Data.Products = message.Products;
Data.CustomerId = message.CustomerId;

var projection = message.Products.Select(p => new ShippingSagaData.Product { Identifier = p });
Data.Products = projection.ToList();

return ProcessOrder(context);
}

Expand All @@ -39,8 +37,12 @@ public Task Handle(PaymentSucceededEvent message, IMessageHandlerContext context
return ProcessOrder(context);
}

private Task ProcessOrder(IMessageHandlerContext context)
private async Task ProcessOrder(IMessageHandlerContext context)
{
return Task.CompletedTask;
if (Data.IsOrderSubmitted && Data.IsPaymentProcessed)
{
await Task.CompletedTask; // Send a message to execute shipment
MarkAsComplete();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,5 @@ public class ShippingSagaData : ContainSagaData
public int CustomerId { get; set; }
public bool IsOrderSubmitted { get; set; }
public bool IsPaymentProcessed { get; set; }
public ICollection<Product> Products { get; set; }

public class Product
{
public int Identifier { get; set; }
}
public List<int> Products { get; set; }
}

0 comments on commit 03bb803

Please sign in to comment.