Integration with Sagas
Http endpoints can start Wolverine sagas by just using a return value for a Saga value.
Let's say that we have a stateful saga type for making online reservations like this:
public class Reservation : Saga
{
public string? Id { get; set; }
// Apply the CompleteReservation to the saga
public void Handle(BookReservation book, ILogger<Reservation> logger)
{
logger.LogInformation("Completing Reservation {Id}", book.Id);
// That's it, we're done. Delete the saga state after the message is done.
MarkCompleted();
}
// Delete this Reservation if it has not already been deleted to enforce a "timeout"
// condition
public void Handle(ReservationTimeout timeout, ILogger<Reservation> logger)
{
logger.LogInformation("Applying timeout to Reservation {Id}", timeout.Id);
// That's it, we're done. Delete the saga state after the message is done.
MarkCompleted();
}
}To start the Reservation saga, you could use an HTTP endpoint method like this one:
[WolverinePost("/reservation")]
public static (
// The first return value would be written out as the HTTP response body
ReservationBooked,
// Because this subclasses from Saga, Wolverine will persist this entity
// with saga persistence
Reservation,
// Other return values that trigger no special handling will be treated
// as cascading messages
ReservationTimeout) Post(StartReservation start)
{
return (new ReservationBooked(start.ReservationId, DateTimeOffset.UtcNow), new Reservation { Id = start.ReservationId }, new ReservationTimeout(start.ReservationId));
}Remember in Wolverine.HTTP that the first return value of an endpoint is assumed to be the response body by Wolverine, so if you are wanting to start a new saga from an HTTP endpoint, the Saga return value either has to be a secondary value in a tuple to opt into the Saga mechanics. Alternatively, if all you want to do is create a new saga, but nothing else, you can return the Saga type and force Wolverine to use the return value as a new Saga as shown in the snippet below. Please note that when creating a Saga entity in this manner, if it has a static Start() method, it will not be invoked. Other message handlers in the Saga will behave as usual.
[WolverinePost("/reservation2")]
// This directs Wolverine to disregard the Reservation return value
// as the response body, and allow Wolverine to use the Reservation
// return as a new saga
[EmptyResponse]
public static Reservation Post2(StartReservation start)
{
return new Reservation { Id = start.ReservationId };
}Returning a Saga as the Response Only
Wolverine treats any returned Saga as a new saga to insert. If the endpoint has already started the saga (say through IMessageBus.InvokeAsync()) and only wants to hand the saga back as the response body, the second insert fails. Mark the endpoint with [SagaIsResponseOnly] and Wolverine will skip the saga persistence for that method's return values:
[WolverinePost("/reservation3")]
[SagaIsResponseOnly]
public static async Task<Reservation> Post3(StartReservation start, IMessageBus bus) { ... }
