diff --git a/docs/guide/http/marten.md b/docs/guide/http/marten.md
index f6fa925fd..56645eab0 100644
--- a/docs/guide/http/marten.md
+++ b/docs/guide/http/marten.md
@@ -1,7 +1,136 @@
# Integration with Marten
+New in Wolverine 1.10.0 is the `Wolverine.Http.Marten` library that adds the ability to more deeply integrate Marten
+into Wolverine.HTTP by utilizing information from route arguments.
+
+To install that library, use:
+
+```bash
+dotnet add package WolverineFx.Http.Marten
+```
+
+## Passing Marten Documents to Endpoint Parameters
+
+Consider this very common use case, you have an HTTP endpoint that needs to work on a Marten document that will
+be loaded using the value of one of the route arguments as that document's identity. In a long hand way, that could
+look like this:
+
+
+
+```cs
+{
+ [WolverineGet("/invoices/longhand/id")]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(200, Type = typeof(Invoice))]
+ public static async Task GetInvoice(
+ Guid id,
+ IQuerySession session,
+ CancellationToken cancellationToken)
+ {
+ var invoice = await session.LoadAsync(id, cancellationToken);
+ if (invoice == null) return Results.NotFound();
+
+ return Results.Ok(invoice);
+ }
+```
+snippet source | anchor
+
+
+Pretty straightforward, but it's a little annoying to have to scatter in all the attributes for OpenAPI and there's definitely
+some repetitive code. So let's introduce the new `[Document]` parameter and look at an exact equivalent for both the
+actual functionality and for the OpenAPI metadata:
+
+
+
+```cs
+[WolverineGet("/invoices/{id}")]
+public static Invoice Get([Document] Invoice invoice)
+{
+ return invoice;
+}
+```
+snippet source | anchor
+
+
+Notice that the `[Document]` attribute was able to use the "id" route parameter. By default, Wolverine is looking first
+for a route variable named "invoiceId" (the document type name + "Id"), then falling back to looking for "id". You can
+of course explicitly override the matching of route argument like so:
+
+
+
+```cs
+[WolverinePost("/invoices/{number}/approve")]
+public static IMartenOp Approve([Document("number")] Invoice invoice)
+{
+ invoice.Approved = true;
+ return MartenOps.Store(invoice);
+}
+```
+snippet source | anchor
+
+
+
+## Marten Aggregate Workflow
+
The http endpoints can play inside the full "critter stack" combination with [Marten](https://martendb.io) with Wolverine's [specific
-support for Event Sourcing and CQRS](/guide/durability/marten/event-sourcing).
+support for Event Sourcing and CQRS](/guide/durability/marten/event-sourcing). Originally this has been done
+by just mimicking the command handler mechanism and having all the inputs come in through the request body (aggregate id, version).
+Wolverine 1.10 added a more HTTP-centric approach using route arguments.
+
+### Using Route Arguments
+
+To opt into the Wolverine + Marten "aggregate workflow", but use data from route arguments for the aggregate id,
+use the new `[Aggregate]` attribute from Wolverine.Http.Marten on endpoint method parameters like shown below:
+
+
+
+```cs
+[WolverinePost("/orders/{orderId}/ship2"), EmptyResponse]
+// The OrderShipped return value is treated as an event being posted
+// to a Marten even stream
+// instead of as the HTTP response body because of the presence of
+// the [EmptyResponse] attribute
+public static OrderShipped Ship(ShipOrder2 command, [Aggregate] Order order)
+{
+ return new OrderShipped();
+}
+```
+snippet source | anchor
+
+
+Using this version of the "aggregate workflow", you no longer have to supply a command in the request body, so you could
+have an endpoint signature like this:
+
+
+
+```cs
+[WolverinePost("/orders/{orderId}/ship3"), EmptyResponse]
+// The OrderShipped return value is treated as an event being posted
+// to a Marten even stream
+// instead of as the HTTP response body because of the presence of
+// the [EmptyResponse] attribute
+public static OrderShipped Ship3([Aggregate] Order order)
+{
+ return new OrderShipped();
+}
+```
+snippet source | anchor
+
+
+A couple other notes:
+
+* The return value handling for events follows the same rules as shown in the next section
+* The endpoints will return a 404 response code if the aggregate in question does not exist
+* The aggregate id can be set explicitly like `[Aggregate("number")]` to match against a route argument named "number", or by default
+ the behavior will try to match first on "{camel case name of aggregate type}Id", then a route argument named "id"
+* This usage will automatically apply the transactional middleware for Marten
+
+### Using Request Body
+
+::: tip
+This usage only requires Wolverine.Marten and does not require the Wolverine.Http.Marten library because
+there's nothing happening here in regards to Marten that is using AspNetCore
+:::
For some context, let's say that we have the following events and [Marten aggregate](https://martendb.io/events/projections/aggregate-projections.html#aggregate-by-stream) to model the workflow of an `Order`:
@@ -116,5 +245,6 @@ public static (OrderStatus, Events) Post(MarkItemReady command, Order order)
return (new OrderStatus(order.Id, order.IsReadyToShip()), events);
}
```
-snippet source | anchor
+snippet source | anchor
+
diff --git a/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs b/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs
index 7ad9b1323..eace80de1 100644
--- a/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs
+++ b/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs
@@ -56,6 +56,8 @@ public AggregateAttribute(string routeOrParameterName)
public override Variable Modify(HttpChain chain, ParameterInfo parameter, IContainer container)
{
+ chain.Metadata.Produces(404);
+
AggregateType = parameter.ParameterType;
var store = container.GetInstance();
var idType = store.Options.Events.StreamIdentity == StreamIdentity.AsGuid ? typeof(Guid) : typeof(string);
diff --git a/src/Http/Wolverine.Http.Marten/DocumentAttribute.cs b/src/Http/Wolverine.Http.Marten/DocumentAttribute.cs
index a3f557bf3..642f55c15 100644
--- a/src/Http/Wolverine.Http.Marten/DocumentAttribute.cs
+++ b/src/Http/Wolverine.Http.Marten/DocumentAttribute.cs
@@ -4,6 +4,7 @@
using JasperFx.Core;
using Lamar;
using Marten;
+using Microsoft.AspNetCore.Http;
namespace Wolverine.Http.Marten;
@@ -28,6 +29,8 @@ public DocumentAttribute(string routeArgumentName)
public override Variable Modify(HttpChain chain, ParameterInfo parameter, IContainer container)
{
+ chain.Metadata.Produces(404);
+
var store = container.GetInstance();
var documentType = parameter.ParameterType;
var mapping = store.Options.FindOrResolveDocumentType(documentType);
diff --git a/src/Http/WolverineWebApi/Marten/Documents.cs b/src/Http/WolverineWebApi/Marten/Documents.cs
index 93887e77f..14dd65fca 100644
--- a/src/Http/WolverineWebApi/Marten/Documents.cs
+++ b/src/Http/WolverineWebApi/Marten/Documents.cs
@@ -1,3 +1,5 @@
+using Marten;
+using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;
using Wolverine.Http.Marten;
using Wolverine.Marten;
@@ -5,26 +7,53 @@
namespace WolverineWebApi.Marten;
public class InvoicesEndpoint
+
+ #region sample_get_invoice_longhand
+
{
+ [WolverineGet("/invoices/longhand/id")]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(200, Type = typeof(Invoice))]
+ public static async Task GetInvoice(
+ Guid id,
+ IQuerySession session,
+ CancellationToken cancellationToken)
+ {
+ var invoice = await session.LoadAsync(id, cancellationToken);
+ if (invoice == null) return Results.NotFound();
+
+ return Results.Ok(invoice);
+ }
+
+ #endregion
+
+ #region sample_using_document_attribute
+
[WolverineGet("/invoices/{id}")]
public static Invoice Get([Document] Invoice invoice)
{
return invoice;
}
+ #endregion
+
[WolverinePost("/invoices/{invoiceId}/pay")]
public static IMartenOp Pay([Document] Invoice invoice)
{
invoice.Paid = true;
return MartenOps.Store(invoice);
}
-
+
+ #region sample_overriding_route_argument_with_document_attribute
+
[WolverinePost("/invoices/{number}/approve")]
public static IMartenOp Approve([Document("number")] Invoice invoice)
{
invoice.Approved = true;
return MartenOps.Store(invoice);
}
+
+ #endregion
}
public class Invoice
diff --git a/src/Http/WolverineWebApi/Marten/Orders.cs b/src/Http/WolverineWebApi/Marten/Orders.cs
index 8c47b7fe0..80821f124 100644
--- a/src/Http/WolverineWebApi/Marten/Orders.cs
+++ b/src/Http/WolverineWebApi/Marten/Orders.cs
@@ -90,7 +90,9 @@ public static OrderShipped Ship(ShipOrder command, Order order)
}
#endregion
-
+
+ #region sample_using_aggregate_attribute_1
+
[WolverinePost("/orders/{orderId}/ship2"), EmptyResponse]
// The OrderShipped return value is treated as an event being posted
// to a Marten even stream
@@ -100,7 +102,11 @@ public static OrderShipped Ship(ShipOrder2 command, [Aggregate] Order order)
{
return new OrderShipped();
}
-
+
+ #endregion
+
+ #region sample_using_aggregate_attribute_2
+
[WolverinePost("/orders/{orderId}/ship3"), EmptyResponse]
// The OrderShipped return value is treated as an event being posted
// to a Marten even stream
@@ -110,6 +116,8 @@ public static OrderShipped Ship3([Aggregate] Order order)
{
return new OrderShipped();
}
+
+ #endregion
[WolverinePost("/orders/{orderId}/ship4"), EmptyResponse]
// The OrderShipped return value is treated as an event being posted