This sample shows a client/server scenario using non-transactional saga persistence.
Prerequisites
Ensure that an instance of the latest Azure Cosmos DB Emulator is running.
Sample structure
This sample contains three projects, SharedMessages, Client and Server.
SharedMessages
The shared message contracts used by all endpoints.
Client
- Sends the
StartOrdermessage toServer. - Receives and handles the
OrderCompletedevent.
Server
- Receive the
StartOrdermessage and initiate anOrderSaga. OrderSagarequests a timeout with an instance ofCompleteOrderwith the saga data.OrderSagapublishes anOrderCompletedevent when theCompleteOrdertimeout fires.
Implementation highlights
Persistence config
In Program.cs of the Server project, the endpoint is configured to use Cosmos DB Persistence:
var endpointConfiguration = new EndpointConfiguration("Samples.CosmosDB.Simple.Server");
var persistence = endpointConfiguration.UsePersistence<CosmosPersistence>();
var connection = Environment.GetEnvironmentVariable("COSMOS_CONNECTION_STRING")
?? """AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==""";
persistence.DatabaseName("Samples.CosmosDB.Simple");
persistence.CosmosClient(new(connection));
persistence.DefaultContainer("Server", "/id");
In the non-transactional mode, the saga id is used as a partition key, and thus, the container needs to use / as the partition key path.
Order saga data
The data stored on the saga is defined on the OrderSagaData. file inside the Server project:
public class OrderSagaData :
ContainSagaData
{
public Guid OrderId { get; set; }
public string OrderDescription { get; set; }
}
Order saga
The handler for this data is on the OrderSaga. file on the Server project:
public class OrderSaga(ILogger<OrderSaga> logger) :
Saga<OrderSagaData>,
IAmStartedByMessages<StartOrder>,
IHandleTimeouts<CompleteOrder>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
{
mapper.MapSaga(saga => saga.OrderId).ToMessage<StartOrder>(msg => msg.OrderId);
}
public Task Handle(StartOrder message, IMessageHandlerContext context)
{
var orderDescription = $"The saga for order {message.OrderId}";
Data.OrderDescription = orderDescription;
logger.LogInformation($"Received StartOrder message {Data.OrderId}. Starting Saga");
var shipOrder = new ShipOrder
{
OrderId = message.OrderId
};
logger.LogInformation("Order will complete in 5 seconds");
CompleteOrder timeoutData = new()
{
OrderDescription = orderDescription,
};
return Task.WhenAll([
context.SendLocal(shipOrder),
RequestTimeout(context, TimeSpan.FromSeconds(5), timeoutData)
]);
}
public Task Timeout(CompleteOrder state, IMessageHandlerContext context)
{
logger.LogInformation($"Saga with OrderId {Data.OrderId} completed");
MarkAsComplete();
OrderCompleted orderCompleted = new()
{
OrderId = Data.OrderId
};
return context.Publish(orderCompleted);
}
}