# Azure Table Persistence Using Saga IDs as Partition Keys This sample demonstrates a client/server scenario using saga and outbox persistences to store records atomically by leveraging transactions. The Saga ID is used as a partition key. ## Prerequisites 1. Install Docker. 1. If running Docker on Windows, set Docker to use Linux containers. 1. In the sample directory, execute the following to set up the Azurite Azure Storage emulator instance: > docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite ## Projects ### SharedMessages The shared message contracts used by all endpoints. ### Client * Sends the `StartOrder` message to `Server`. * Receives and handles the `OrderCompleted` event. ### Server projects * Receive the `StartOrder` message and initiate an `OrderSaga`. * `OrderSaga` requests a timeout with an instance of `CompleteOrder` with the saga data. * Receive the `OrderShipped` message with a custom header. * `OrderSaga` publishes an `OrderCompleted` event when the `CompleteOrder` timeout fires. ### Persistence config Configure the endpoint to use Azure Table Persistence. ```cs var endpointConfiguration = new EndpointConfiguration("Samples.AzureTable.Transactions.Server"); endpointConfiguration.EnableOutbox(); var useStorageTable = true; var persistence = endpointConfiguration.UsePersistence(); var connection = useStorageTable ? "UseDevelopmentStorage=true" : "TableEndpoint=https://localhost:8081/;AccountName=AzureTableSamples;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; var account = CloudStorageAccount.Parse(connection); var cloudTableClient = account.CreateCloudTableClient(); persistence.UseCloudTableClient(cloudTableClient); persistence.DefaultTable("Server"); ``` ## Using Behaviors Most messages implement `IProvideOrderId`. Since Saga IDs are deterministically derived from saga data (the correlation property name and value), they can be used as a partition key. Then, `IProvidePartitionKeyFromSagaId` can be injected into behaviors in the logical pipeline stage. ```cs class OrderIdAsPartitionKeyBehavior : Behavior { public OrderIdAsPartitionKeyBehavior(IProvidePartitionKeyFromSagaId partitionKeyFromSagaId) { partitionKeyFromSagaId1 = partitionKeyFromSagaId; } public override async Task Invoke(IIncomingLogicalMessageContext context, Func next) { var correlationProperty = SagaCorrelationProperty.None; if (context.Message.Instance is IProvideOrderId provideOrderId) { var partitionKeyValue = provideOrderId.OrderId; correlationProperty = new SagaCorrelationProperty("OrderId", partitionKeyValue); } await partitionKeyFromSagaId1.SetPartitionKey(context, correlationProperty); if (context.Headers.TryGetValue(Headers.SagaId, out var sagaIdHeader)) { Log.Info($"Saga Id Header: {sagaIdHeader}"); } if (context.Extensions.TryGet(out var tableInformation)) { Log.Info($"Table Information: {tableInformation.TableName}"); } Log.Info($"Found partition key '{context.Extensions.Get().PartitionKey}' from '{nameof(IProvideOrderId)}'"); await next(); } public class Registration : RegisterStep { public Registration() : base(nameof(OrderIdAsPartitionKeyBehavior), typeof(OrderIdAsPartitionKeyBehavior), "Determines the PartitionKey from the logical message", provider => new OrderIdAsPartitionKeyBehavior(provider.GetRequiredService())) { InsertBefore(nameof(LogicalOutboxBehavior)); } } IProvidePartitionKeyFromSagaId partitionKeyFromSagaId1; static readonly ILog Log = LogManager.GetLogger(); } ``` Even though one of the handlers replies with a message that does not implement `IProvideOrderId`, transactionality is still maintained because messages that are part of a saga conversation flow automatically have the Saga ID set as a header. In these cases, there is no need to extract correlation property information to derive the Saga ID. Finally the above behavior is registered in the pipeline. ```cs endpointConfiguration.Pipeline.Register(new OrderIdAsPartitionKeyBehavior.Registration()); ``` ## Order saga data ```cs public class OrderSagaData : ContainSagaData { public Guid OrderId { get; set; } public string OrderDescription { get; set; } } ``` ## Order saga ```cs public class OrderSaga : Saga, IAmStartedByMessages, IHandleMessages, IHandleTimeouts { static readonly ILog Log = LogManager.GetLogger(); protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { mapper.MapSaga(saga => saga.OrderId) .ToMessage(msg => msg.OrderId); } public Task Handle(StartOrder message, IMessageHandlerContext context) { var orderDescription = $"The saga for order {message.OrderId}"; Data.OrderDescription = orderDescription; Log.Info($"Received StartOrder message {Data.OrderId}. Starting Saga"); var shipOrder = new ShipOrder { OrderId = message.OrderId }; Log.Info("Order will complete in 5 seconds"); var timeoutData = new CompleteOrder { OrderDescription = orderDescription, OrderId = Data.OrderId, }; return Task.WhenAll( context.SendLocal(shipOrder), RequestTimeout(context, TimeSpan.FromSeconds(5), timeoutData) ); } public Task Handle(OrderShipped message, IMessageHandlerContext context) { Log.Info($"Order with OrderId {Data.OrderId} shipped on {message.ShippingDate}"); return Task.CompletedTask; } public Task Timeout(CompleteOrder state, IMessageHandlerContext context) { Log.Info($"Saga with OrderId {Data.OrderId} completed"); MarkAsComplete(); var orderCompleted = new OrderCompleted { OrderId = Data.OrderId }; return context.Publish(orderCompleted); } } ```