Skip to content

Commit

Permalink
Vertical slice architecture awesomeness through storage side effects …
Browse files Browse the repository at this point in the history
…and the [Entity] attribute

Closes GH-1073

Docs for the [Entity] attribute usage.

Docs on the new storage action side effect return values

Docs for using an enumerable of IMartenOp

Can now use IEnumerable<IMartenOp> as a return type

New UnitOfWork<T> side effect type

More tests on using IStorageAction within a tuple

Tests on EF Core + HTTP + [Edit]

Working functionality for [Entity] plus HTTP

Basic usage of [Entity] and IStorageAction w/ HTTP endpoints

Enforcing required on [Entity], enabling the usage of [Entity] for Before/Load methods

EntityAttribute has Required / Not Required semantics for message handlers

Exploded the little types into their own files, added Xml API comments

Storage action return values are working with EF Core!

Boom! Can use [Edit] and IStorageAction types with RavenDb

Storage action compliance tests are all green for Marten!

Built new compliance test for the storage action usage for different persistence mechanisms

Can handle nulls with the storage action returns

Happy path tests using InMemorySagaPersistor for all storage actions

First working version of the new [Entity] attribute against the in memory saga persistor

Spiked in the HttpChain.TryFindVariable implementation, partial though

New IChain member for finding value variable, implemented on HandlerChain

Quite a bit of preliminary work on the new Storage / IStorageAction model
  • Loading branch information
jeremydmiller committed Jan 7, 2025
1 parent b61c26c commit db46888
Show file tree
Hide file tree
Showing 213 changed files with 10,264 additions and 134 deletions.
3 changes: 2 additions & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ const config: UserConfig<DefaultTheme.Config> = {
{text: 'Execution Timeouts', link: '/guide/handlers/timeout'},
{text: 'Fluent Validation Middleware', link: '/guide/handlers/fluent-validation'},
{text: 'Sticky Handler to Endpoint Assignments', link: '/guide/handlers/sticky'},
{text: 'Message Batching', link: '/guide/handlers/batching'}
{text: 'Message Batching', link: '/guide/handlers/batching'},
{text: 'Persistence Helpers', link: '/guide/handlers/persistence'}
]
},
]
Expand Down
14 changes: 12 additions & 2 deletions docs/guide/durability/efcore.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ public async Task Post(
await outbox.SaveChangesAndFlushMessagesAsync();
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/EFCoreSample/ItemService/CreateItemController.cs#L10-L40' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_dbcontext_outbox_1' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/EFCoreSample/ItemService/CreateItemController.cs#L12-L42' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_dbcontext_outbox_1' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Or use the `IDbContextOutbox` as shown below, but in this case you will need to explicitly call `Enroll()` on
Expand Down Expand Up @@ -291,10 +291,20 @@ public async Task Post3(
await outbox.SaveChangesAndFlushMessagesAsync();
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/EFCoreSample/ItemService/CreateItemController.cs#L43-L78' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_dbcontext_outbox_2' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/EFCoreSample/ItemService/CreateItemController.cs#L45-L80' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_dbcontext_outbox_2' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## As Saga Storage

There's actually nothing to do other than to make a mapping of the `Saga` subclass that's your stateful saga inside
a registered `DbContext`.

## Storage Side Effects

This integration includes full support for the [storage action side effects](/guide/handlers/side-effects.html#storage-side-effects)
model when using EF Core with Wolverine. The only exception is that the `Store` "upsert" operation from the
side effects translates to an EF Core `DbContext` update.

## Entity Attribute Loading

The EF Core integration is able to completely support the [Entity attribute usage](/guide/handlers/persistence.html#automatically-loading-entities-to-method-parameters).
3 changes: 3 additions & 0 deletions docs/guide/durability/marten/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ Using the `IntegrateWithWolverine()` extension method behind your call to `AddMa
* Makes Marten the active [saga storage](/guide/durability/sagas) for Wolverine
* Adds transactional middleware using Marten to your Wolverine application

## Entity Attribute Loading

The EF Core integration is able to completely support the [Entity attribute usage](/guide/handlers/persistence.html#automatically-loading-entities-to-method-parameters).

## Marten as Outbox

Expand Down
45 changes: 44 additions & 1 deletion docs/guide/durability/marten/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ You can certainly write your own `IMartenOp` implementations and use them as ret
handlers
:::

::: info
This integration includes full support for the [storage action side effects](/guide/handlers/side-effects.html#storage-side-effects)
model when using Marten with Wolverine.
:::

::: tip
This integration also includes full support for the [storage action side effects](/guide/handlers/side-effects.html#storage-side-effects)
model when using Marten~~~~ with Wolverine.
:::

The `Wolverine.Marten` library includes some helpers for Wolverine [side effects](/guide/handlers/side-effects) using
Marten with the `IMartenOp` interface:

Expand All @@ -19,7 +29,7 @@ public interface IMartenOp : ISideEffect
void Execute(IDocumentSession session);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/Wolverine.Marten/IMartenOp.cs#L7-L17' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_imartenop' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/Wolverine.Marten/IMartenOp.cs#L13-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_imartenop' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The built in side effects can all be used from the `MartenOps` static class like this HTTP endpoint example:
Expand Down Expand Up @@ -66,5 +76,38 @@ be a pure function that can be easily unit tested through measuring the expected
helps you utilize synchronous methods for your logic, even though at runtime Wolverine itself will be wrapping asynchronous
code about your simpler, synchronous code.

## Returning Multiple Marten Side Effects <Badge type="tip" text="3.6" />

Due to (somewhat) popular demand, Wolverine lets you return zero to many `IMartenOp` operations as side effects
from a message handler or HTTP endpoint method like so:

<!-- snippet: sample_using_ienumerable_of_martenop_as_side_effect -->
<a id='snippet-sample_using_ienumerable_of_martenop_as_side_effect'></a>
```cs
// Just keep in mind that this "example" was rigged up for test coverage
public static IEnumerable<IMartenOp> Handle(AppendManyNamedDocuments command)
{
var number = 1;
foreach (var name in command.Names)
{
yield return MartenOps.Store(new NamedDocument{Id = name, Number = number++});
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/MartenTests/handler_actions_with_implied_marten_operations.cs#L169-L181' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_ienumerable_of_martenop_as_side_effect' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Wolverine will pick up on any return type that can be cast to `IEnumerable<IMartenOp>`, so for example:

* `IEnumerable<IMartenOp>`
* `IMartenOp[]`
* `List<IMartenOp>`

And you get the point. Wolverine is not (yet) smart enough to know that an array or enumerable of a concrete
type of `IMartenOp` is a side effect.

Like any other "side effect", you could technically return this as the main return type of a method or as part of a
tuple.



2 changes: 1 addition & 1 deletion docs/guide/durability/marten/transactional-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public class CommandsAreTransactional : IHandlerPolicy
// for each chain
chains
.Where(chain => chain.MessageType.Name.EndsWith("Command"))
.Each(chain => chain.Middleware.Add(new TransactionalFrame(chain)));
.Each(chain => chain.Middleware.Add(new CreateDocumentSessionFrame(chain)));
}
}
```
Expand Down
9 changes: 8 additions & 1 deletion docs/guide/durability/ravendb.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,14 @@ public static class RavenOps
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/Wolverine.RavenDb/IRavenDbOp.cs#L36-L66' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_ravenops' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

See the Wolverine [side effects](/guide/handlers/side-effects) model for more information.
See the Wolverine [side effects](/guide/handlers/side-effects) model for more information.

This integration also includes full support for the [storage action side effects](/guide/handlers/side-effects.html#storage-side-effects)
model when using RavenDb with Wolverine.

## Entity Attribute Loading

The RavenDb integration is able to completely support the [Entity attribute usage](/guide/handlers/persistence.html#automatically-loading-entities-to-method-parameters).



Expand Down
151 changes: 151 additions & 0 deletions docs/guide/handlers/persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Persistence Helpers

Philosophically, Wolverine is trying to enable you to write the message handlers or HTTP endpoint
methods with low ceremony code that's easy to test and easy to reason about. To that end, Wolverine
has quite a few tricks to utilize your persistence tooling from your handler or HTTP endpoint code
without having to directly couple your behavioral code to persistence infrastructure:

* The [storage action side effect model](/guide/handlers/side-effects.html#storage-side-effects) for pure function handlers that involve database "writes"
* The [aggregate handler workflow](/guide/durability/marten/event-sourcing) with Marten for highly testable CQRS + Event Sourcing systems
* Specific [integration with Marten and Wolverine.HTTP](/guide/http/marten)

## Automatically Loading Entities to Method Parameters <Badge type="tip" text="3.6" />

A common need when building Wolverine message handlers or HTTP endpoints is to need to load
an entity object based on an identity value in either the message itself, the HTTP request body, or
an HTTP route argument. In these cases, you'll generally pluck the correct value out of the
message or route arguments, then call into an EF Core `DbContext` or a Marten/RavenDb `IDocumentSession`
to load the entity for you before proceeding on with your work. Since this usage is so common,
Wolverine has the `[Wolverine.Persistence.Entity]` attribute to just do that for you and have the right entity "pushed" into
your message handler.

Here's a simple example of a message handler that's also a valid Wolverine.HTTP endpoint using this attribute. First though,
the message type and/or HTTP request body:

<!-- snippet: sample_rename_todo -->
<a id='snippet-sample_rename_todo'></a>
```cs
public record RenameTodo(string Id, string Name);
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Todos/Todo2.cs#L23-L27' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_rename_todo' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

and the handler & endpoint code handling that message type:

<!-- snippet: sample_using_entity_attribute -->
<a id='snippet-sample_using_entity_attribute'></a>
```cs
// Use "Id" as the default member
[WolverinePost("/api/todo/update")]
public static Update<Todo2> Handle(
// The first argument is always the incoming message
RenameTodo command,

// By using this attribute, we're telling Wolverine
// to load the Todo entity from the configured
// persistence of the app using a member on the
// incoming message type
[Entity] Todo2 todo)
{
// Do your actual business logic
todo.Name = command.Name;

// Tell Wolverine that you want this entity
// updated in persistence
return Storage.Update(todo);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Todos/Todo2.cs#L55-L77' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_entity_attribute' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

In the code above, the `Todo2` argument would be filled by trying to load that `Todo2` entity
from persistence using the value of `RenameTodo.Id`. If you were using Marten as your persistence
mechanism, this would be using `IDocumentSession.LoadAsync<Todo2>(id)` to load the entity with the RavenDb usage being similar. If
you were using EF Core and had an `Todo2DbContext` service registered in your system, it would
be using `Todo2DbContext.FindAsync<Todo2>(id)`.

By default, Wolverine is assuming that any parameter value marked with `[Entity]` is required, so if the `Todo2` entity was not found in the database, then:

* As a message handler, it will just log that the entity could not be found and otherwise exit cleanly without doing any further processing
* As an HTTP endpoint, the handler would write out a status code of 404 (not found) and exit otherwise

If you need or want any other kind of failure handling on the entity not being found, you'll need to
use explicit code instead, maybe with a `LoadAsync()` "before" method to still keep your main
handler or endpoint method a *pure function*.

If you genuinely don't need the `[Entity]` value to be required, you can do this instead:

<!-- snippet: sample_using_not_required_entity_attribute -->
<a id='snippet-sample_using_not_required_entity_attribute'></a>
```cs
[WolverinePost("/api/todo/maybecomplete")]
public static IStorageAction<Todo2> Handle(MaybeCompleteTodo command, [Entity(Required = false)] Todo2? todo)
{
if (todo == null) return Storage.Nothing<Todo2>();
todo.IsComplete = true;
return Storage.Update(todo);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Todos/Todo2.cs#L144-L154' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_not_required_entity_attribute' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

So far, all of the examples have depended on a fall back to looking for either a case insensitive match "id"
match on the message members for message handlers or the route arguments, then request input members
for HTTP endpoints. Wolverine will also look for "[Entity Type Name]Id", so in the case of `Todo2`, it would
look as well for a more specific `Todo2Id` member or route argument for the identity value.

You can of course override this by just telling Wolverine what member name or route argument name
should have the identity like this:

<!-- snippet: sample_specifying_the_exact_route_argument -->
<a id='snippet-sample_specifying_the_exact_route_argument'></a>
```cs
// Okay, I still used "id", but it *could* be something different here!
[WolverineGet("/api/todo/{id}")]
public static Todo2 Get([Entity("id")] Todo2 todo) => todo;
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Todos/Todo2.cs#L156-L162' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_specifying_the_exact_route_argument' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

If you have any conflict between whether the identity should be found on either the route arguments
or request body, you can specify the identity value source through the `EntityAttribute.ValueSource` property
to one of these values:

<!-- snippet: sample_ValueSource -->
<a id='snippet-sample_valuesource'></a>
```cs
public enum ValueSource
{
/// <summary>
/// This value can be sourced by any mechanism that matches the name. This is the default.
/// </summary>
Anything,

/// <summary>
/// The value should be sourced by a property or field on the message type or HTTP request type
/// </summary>
InputMember,

/// <summary>
/// The value should be sourced by a route argument of an HTTP request
/// </summary>
RouteValue
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/Attributes/ModifyChainAttribute.cs#L17-L37' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_valuesource' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Some other facts to know about `[Entity]` usage:

* Supported by the Marten, EF Core, and RavenDb integration
* For EF Core usage, Wolverine has to be able to figure out which `DbContext` type persists the entity type of the parameter
* In all cases, Wolverine is trying to "know" what the identity type for the entity type is (`Guid`? `int`? Something else?) from the underlying persistence tooling and use that to help parse route arguments as needed
* `[Entity]` cannot support any kind of composite key or identity
* `[Entity]` can be used for both HTTP endpoints and message handler methods
* `[Entity]` can be used for `Before` / `Validate` methods in compound handlers
* If an `[Entity]` attribute is used in the main handler or endpoint method, you can still resolve the same entity type as a parameter to a `Before` method without needing to use the attribute again

::: tip
As with other kinds of Wolverine "magic", lean on the [pre-generated code](/guide/codegen) to let Wolverine explain
what it's doing with your method signatures.
:::
Loading

0 comments on commit db46888

Please sign in to comment.