Marten as Saga Storage
Marten is an easy option for persistent sagas with Wolverine. Yet again, to opt into using Marten as your saga storage mechanism in Wolverine, you just need to add the IntegrateWithWolverine() option to your Marten configuration as shown in the Marten Integration Getting Started section.
When using the Wolverine + Marten integration, your stateful saga classes should be valid Marten document types that inherit from Wolverine's Saga type, which generally means being a public class with a valid Marten identity member. Remember that your handler methods in Wolverine can accept "method injected" dependencies from your underlying IoC container.
See the Saga with Marten sample project.
Strong-Typed Identifiers 5.14
Wolverine supports using strong-typed identifiers (e.g., OrderId, InvoiceId) as the identity of a Marten saga document. This works the same way that strong-typed identifiers are supported for Marten aggregate types. As long as Marten can resolve the identity type, Wolverine will correctly extract the saga identity from your messages.
Here's an example using the StronglyTypedId library:
[StronglyTypedId(Template.Guid)]
public readonly partial struct OrderSagaId;
public class OrderSagaWorkflow : Wolverine.Saga
{
public OrderSagaId Id { get; set; }
public string CustomerName { get; set; } = null!;
public bool ItemsPicked { get; set; }
public bool PaymentProcessed { get; set; }
public bool Shipped { get; set; }
public static OrderSagaWorkflow Start(StartOrderSaga command)
{
return new OrderSagaWorkflow
{
Id = command.OrderId,
CustomerName = command.CustomerName
};
}
public void Handle(PickOrderItems command)
{
ItemsPicked = true;
checkForCompletion();
}
public void Handle(ProcessOrderPayment command)
{
PaymentProcessed = true;
checkForCompletion();
}
public void Handle(ShipOrder command)
{
Shipped = true;
checkForCompletion();
}
public void Handle(CancelOrderSaga command)
{
MarkCompleted();
}
private void checkForCompletion()
{
if (ItemsPicked && PaymentProcessed && Shipped)
{
MarkCompleted();
}
}
}
// Messages using the strong-typed identifier
public record StartOrderSaga(OrderSagaId OrderId, string CustomerName);
public record PickOrderItems(OrderSagaId OrderSagaWorkflowId);
public record ProcessOrderPayment(OrderSagaId OrderSagaWorkflowId);
public record ShipOrder(OrderSagaId OrderSagaWorkflowId);
public record CancelOrderSaga(OrderSagaId OrderSagaWorkflowId);
// Message using [SagaIdentity] attribute with strong-typed ID
public class CompleteOrderStep
{
[SagaIdentity] public OrderSagaId TheOrderId { get; set; }
}The standard saga identity resolution conventions still apply:
- Properties decorated with
[SagaIdentity] - A property named
{SagaTypeName}Id(e.g.,OrderSagaWorkflowId) - A property named
SagaId - A property named
Id
Any strong-typed identifier type that Marten can resolve will work, including types generated by StronglyTypedId, Vogen, or hand-crafted value types.
Soft-Deleted Sagas
By default, when a saga calls MarkCompleted(), Wolverine deletes the saga document from Marten via IDocumentSession.Delete(). If your saga type is configured for soft-deletes, the document will be soft-deleted rather than hard-deleted, allowing you to keep a history of completed sagas using Marten's soft-delete feature.
WARNING
When using soft-deleted sagas, Wolverine will still load soft-deleted saga documents via IDocumentSession.LoadAsync(), which does not filter out soft-deleted documents. You must explicitly handle the case where a saga has been marked as deleted in your handler code.
The recommended approach is to implement Marten's ISoftDeleted interface on your saga class. This gives your handlers access to the Deleted and DeletedAt properties so they can check whether the saga has already been completed:
[SoftDeleted]
public class SubscriptionSaga : Saga, ISoftDeleted
{
public Guid Id { get; set; }
public string PlanName { get; set; } = string.Empty;
public bool IsActive { get; set; }
// ISoftDeleted members — Marten populates these automatically
public bool Deleted { get; set; }
public DateTimeOffset? DeletedAt { get; set; }
public static SubscriptionSaga Start(StartSubscription command)
{
return new SubscriptionSaga
{
Id = command.SubscriptionSagaId,
PlanName = command.PlanName,
IsActive = true
};
}
public void Handle(CancelSubscription command)
{
IsActive = false;
MarkCompleted(); // Marten will soft-delete instead of hard-delete
}
// Because soft-deleted sagas are still loaded by Wolverine,
// you must guard against processing messages for completed sagas
public void Handle(UpgradeSubscription command)
{
if (Deleted)
{
// Saga was already completed — do nothing, or log, or throw
return;
}
PlanName = command.NewPlanName;
}
}
public record StartSubscription(Guid SubscriptionSagaId, string PlanName);
public record CancelSubscription(Guid SubscriptionSagaId);
public record UpgradeSubscription(Guid SubscriptionSagaId, string NewPlanName);Note that with soft-deletes, you retain the full saga document in the database after completion, and you can query completed sagas using Marten's MaybeDeleted() or IsDeleted() LINQ filters:
// Query only deleted (completed) sagas
var completedSagas = await session
.Query<SubscriptionSaga>()
.Where(x => x.IsDeleted())
.ToListAsync();
// Query all sagas including deleted
var allSagas = await session
.Query<SubscriptionSaga>()
.Where(x => x.MaybeDeleted())
.ToListAsync();Optimistic Concurrency 3.0
Marten will automatically apply numeric revisioning to Wolverine Saga storage, and will increment the Version while handling Saga commands to use Marten's native optimistic concurrency protection.

