Skip to content

Commit

Permalink
documentation on using LatestAggregate. Closes GH-1161
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydmiller committed Dec 13, 2024
1 parent 85ab976 commit 4d49c71
Show file tree
Hide file tree
Showing 20 changed files with 316 additions and 34 deletions.
2 changes: 1 addition & 1 deletion docs/guide/durability/dead-letter-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ app.MapDeadLettersEndpoints()
;
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L141-L151' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_dead_letter_endpoints' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L145-L155' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_dead_letter_endpoints' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### Using the Dead Letters REST API
Expand Down
111 changes: 109 additions & 2 deletions docs/guide/durability/marten/event-sourcing.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ public static void Handle(OrderEventSourcingSample.MarkItemReady command, IEvent
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/OrderEventSourcingSample/Alternatives/Signatures.cs#L25-L54' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_markitemreadyhandler_with_explicit_stream' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/OrderEventSourcingSample/Alternatives/Signatures.cs#L26-L55' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_markitemreadyhandler_with_explicit_stream' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Just as in other Wolverine [message handlers](/guide/handlers/), you can use
Expand Down Expand Up @@ -365,9 +365,116 @@ public class MarkItemReady
public string ItemName { get; init; }
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/OrderEventSourcingSample/Alternatives/Signatures.cs#L8-L19' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_markitemready_with_explicit_identity' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/OrderEventSourcingSample/Alternatives/Signatures.cs#L9-L20' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_markitemready_with_explicit_identity' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Forwarding Events

See [Event Forwarding](./event-forwarding) for more information.

## Returning the Updated Aggregate <Badge type="tip" text="3.5" />

A common use case for the "aggregate handler workflow" has been to respond with the now updated state of the projected
aggregate that has just been updated by appending new events. Until now, that's effectively meant making a completely separate
call to the database through Marten to retrieve the latest updates.

::: info
To understand more about the inner workings of the next section, see the Marten documentation on its [FetchLatest](https://martendb.io/events/projections/read-aggregates.html#fetchlatest)
API.
:::

As a quick tip for performance, assuming that you are *not* mutating the projected documents within your command
handlers, you can opt for this significant Marten optimization to eliminate extra database round trips while
using the aggregate handler workflow:

```csharp
builder.Services.AddMarten(opts =>
{
// Other Marten configuration
// Use this setting to get the very best performance out
// of the UpdatedAggregate workflow and aggregate handler
// workflow over all
opts.Events.UseIdentityMapForAggregates = true;
}).IntegrateWithWolverine();
```

::: info
The setting above cannot be a default in Marten because it can break some existing code with a very different
workflow that what the Critter Stack team recommends for the aggregate handler workflow.
:::

Wolverine.Marten has a special response type for message handlers or HTTP endpoints we can use as a directive to tell Wolverine
to respond with the latest state of a projected aggregate as part of the command execution. Let's make this concrete by
taking the `MarkItemReady` command handler we've used earlier in this guide and building a slightly new version that
produces a response of the latest aggregate:

<!-- snippet: sample_MarkItemReadyHandler_with_response_for_updated_aggregate -->
<a id='snippet-sample_markitemreadyhandler_with_response_for_updated_aggregate'></a>
```cs
[AggregateHandler]
public static (
// Just tells Wolverine to use Marten's FetchLatest API to respond with
// the updated version of Order that reflects whatever events were appended
// in this command
UpdatedAggregate,

// The events that should be appended to the event stream for this order
Events) Handle(OrderEventSourcingSample.MarkItemReady command, Order order)
{
var events = new Events();

if (order.Items.TryGetValue(command.ItemName, out var item))
{
// Not doing this in a purist way here, but just
// trying to illustrate the Wolverine mechanics
item.Ready = true;

// Mark that the this item is ready
events.Add(new ItemReady(command.ItemName));
}
else
{
// Some crude validation
throw new InvalidOperationException($"Item {command.ItemName} does not exist in this order");
}

// If the order is ready to ship, also emit an OrderReady event
if (order.IsReadyToShip())
{
events.Add(new OrderReady());
}

return (new UpdatedAggregate(), events);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/OrderEventSourcingSample/Alternatives/Signatures.cs#L63-L101' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_markitemreadyhandler_with_response_for_updated_aggregate' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Note the usage of the `Wolverine.Marten.UpdatedAggregate` response in the handler. That type by itself is just a directive
to Wolverine to generate the necessary code to call `FetchLatest` and respond with that. The command handler above allows
us to use the command in a mediator usage like so:

<!-- snippet: sample_using_UpdatedAggregate_with_invoke_async -->
<a id='snippet-sample_using_updatedaggregate_with_invoke_async'></a>
```cs
public static Task<Order> update_and_get_latest(IMessageBus bus, MarkItemReady command)
{
// This will return the updated version of the Order
// aggregate that incorporates whatever events were appended
// in the course of processing the command
return bus.InvokeAsync<Order>(command);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/OrderEventSourcingSample/Alternatives/Signatures.cs#L103-L113' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_updatedaggregate_with_invoke_async' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Likewise, you can use `UpdatedAggregate` as the response body of an HTTP endpoint with Wolverine.HTTP [as shown here](/guide/http/marten.html#responding-with-the-updated-aggregate~~~~).

::: info
This feature has been more or less requested several times, but was finally brought about because of the need
to consume Wolverine + Marten commands within Hot Chocolate mutations and always return the current state of
the projected aggregate being updated to the user interface.
:::


2 changes: 1 addition & 1 deletion docs/guide/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ var app = builder.Build();
// you will need to explicitly call this *before* MapWolverineEndpoints()
await app.Services.ApplyAsyncWolverineExtensions();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L103-L111' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_calling_applyasyncwolverineextensions' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L107-L115' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_calling_applyasyncwolverineextensions' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Wolverine Plugin Modules
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/http/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public static OrderShipped Ship(ShipOrder command, Order order)
return new OrderShipped();
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L106-L119' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_emptyresponse' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L116-L129' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_emptyresponse' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## JSON Handling
Expand Down Expand Up @@ -312,7 +312,7 @@ and register that strategy within our `MapWolverineEndpoints()` set up like so:
// Customizing parameter handling
opts.AddParameterHandlingStrategy<NowParameterStrategy>();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L203-L208' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_adding_custom_parameter_handling' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L207-L212' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_adding_custom_parameter_handling' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And lastly, here's the application within an HTTP endpoint for extra context:
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/http/fluentvalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ app.MapWolverineEndpoints(opts =>
// Wolverine.Http.FluentValidation
opts.UseFluentValidationProblemDetailMiddleware();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L153-L174' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_configure_endpoints' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L157-L178' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_configure_endpoints' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
48 changes: 42 additions & 6 deletions docs/guide/http/marten.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public static OrderShipped Ship(ShipOrder2 command, [Aggregate] Order order)
return new OrderShipped();
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L121-L136' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_aggregate_attribute_1' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L131-L146' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_aggregate_attribute_1' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Using this version of the "aggregate workflow", you no longer have to supply a command in the request body, so you could
Expand All @@ -143,7 +143,7 @@ public static OrderShipped Ship3([Aggregate] Order order)
return new OrderShipped();
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L138-L150' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_aggregate_attribute_2' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L148-L160' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_aggregate_attribute_2' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

A couple other notes:
Expand Down Expand Up @@ -189,6 +189,9 @@ public class Item

public class Order
{
// For JSON serialization
public Order(){}

public Order(OrderCreated created)
{
foreach (var item in created.Items) Items[item.Name] = item;
Expand Down Expand Up @@ -218,6 +221,13 @@ public class Order
Items[ready.Name].Ready = true;
}

public void Apply(OrderConfirmed confirmed)
{
IsConfirmed = true;
}

public bool IsConfirmed { get; set; }

public bool IsReadyToShip()
{
return Shipped == null && Items.Values.All(x => x.Ready);
Expand All @@ -226,7 +236,7 @@ public class Order
public bool IsShipped() => Shipped.HasValue;
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L11-L73' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_order_aggregate_for_http' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L11-L83' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_order_aggregate_for_http' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

To append a single event to an event stream from an HTTP endpoint, you can use a return value like so:
Expand All @@ -245,7 +255,7 @@ public static OrderShipped Ship(ShipOrder command, Order order)
return new OrderShipped();
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L106-L119' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_emptyresponse' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L116-L129' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_emptyresponse' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Or potentially append multiple events using the `Events` type as a return value like this sample:
Expand Down Expand Up @@ -281,7 +291,33 @@ public static (OrderStatus, Events) Post(MarkItemReady command, Order order)
return (new OrderStatus(order.Id, order.IsReadyToShip()), events);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L200-L230' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_returning_multiple_events_from_http_endpoint' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L210-L240' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_returning_multiple_events_from_http_endpoint' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### Responding with the Updated Aggregate

See the documentation from the message handlers on using [UpdatedAggregate](/guide/durability/marten/event-sourcing.html#returning-the-updated-aggregate) for more background on this topic.

To return the updated state of a projected aggregate from Marten as the HTTP response from an endpoint using
the aggregate handler workflow, return the `UpdatedAggregate` marker type as the first "response value" of
your HTTP endpoint like so:

<!-- snippet: sample_returning_updated_aggregate_as_response_from_http_endpoint -->
<a id='snippet-sample_returning_updated_aggregate_as_response_from_http_endpoint'></a>
```cs
[AggregateHandler]
[WolverinePost("/orders/{id}/confirm2")]
// The updated version of the Order aggregate will be returned as the response body
// from requesting this endpoint at runtime
public static (UpdatedAggregate, Events) ConfirmDifferent(ConfirmOrder command, Order order)
{
return (
new UpdatedAggregate(),
[new OrderConfirmed()]
);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L268-L282' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_returning_updated_aggregate_as_response_from_http_endpoint' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### Compiled Query Resource Writer Policy
Expand All @@ -294,7 +330,7 @@ Register it in `WolverineHttpOptions` like this:
```cs
opts.UseMartenCompiledQueryResultPolicy();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L180-L182' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_user_marten_compiled_query_policy' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L184-L186' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_user_marten_compiled_query_policy' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

If you now return a compiled query from an Endpoint the result will get directly streamed to the client as JSON. Short circuiting JSON deserialization.
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/http/mediator.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ app.MapPostToWolverine<CustomRequest, CustomResponse>("/wolverine/request");
app.MapDeleteToWolverine<CustomRequest, CustomResponse>("/wolverine/request");
app.MapPutToWolverine<CustomRequest, CustomResponse>("/wolverine/request");
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L214-L226' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_optimized_mediator_usage' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L218-L230' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_optimized_mediator_usage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

With this mechanism, Wolverine is able to optimize the runtime function for Minimal API by eliminating IoC service locations
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/http/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ builder.Services.AddSwaggerGen(x =>
x.OperationFilter<WolverineOperationFilter>();
});
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L43-L50' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_custom_swashbuckle_filter' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L42-L49' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_custom_swashbuckle_filter' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Operation Id
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/http/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Which is registered like this (or as described in [`Registering Middleware by Me
opts.AddMiddlewareByMessageType(typeof(FakeAuthenticationMiddleware));
opts.AddMiddlewareByMessageType(typeof(CanShipOrderMiddleWare));
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L184-L187' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_http_middleware_by_type' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L188-L191' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_http_middleware_by_type' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The key point to notice there is that `IResult` is a "return value" of the middleware. In the case of an HTTP endpoint,
Expand Down
Loading

0 comments on commit 4d49c71

Please sign in to comment.