Simple Saga Usage

Component: NServiceBus
NuGet Package: NServiceBus (7.x)

Introduction

This sample shows a simple saga to manage an order.

Once the project is started, press Enter to send a StartOrder message with a randomly assigned OrderId that starts a new saga instance. This is a result of as configuration specified with the IAmStartedByMessages interface and mapping that correlates StartOrder.OrderId property on the message with OrderSagaData.OrderId property on the saga data.

When processing the StartOrder message each saga instance requests a 30-second CancelOrder timeout that will mark the saga as complete if the saga is not already complete.

The output to the console will be

Storage locations:
Learning Persister: SolutionDir\Sample\bin\Debug\.sagas
Learning Transport: SolutionDir\

Press 'Enter' to send a StartOrder message
Press any other key to exit

Sent StartOrder with OrderId 8d80b684-cc77-4ec6-867b-090bc38d914c.

OrderSaga StartOrder received with OrderId 8d80b684-cc77-4ec6-867b-090bc38d914c
OrderSaga Sending a CompleteOrder that will be delayed by 10 seconds
Stop the endpoint now to see the saga data in:
SolutionDir\Sample\bin\Debug\.sagas\OrderSaga\f5e7ea90-b866-16a0-c911-452f954b95da.json
OrderSaga Requesting a CancelOrder that will be executed in 30 seconds.
Stop the endpoint now to see the timeout data in the delayed directory
SolutionDir\.delayed\20170510054055

Endpoint configuration

endpointConfiguration.UsePersistence<LearningPersistence>();
endpointConfiguration.UseTransport<LearningTransport>();

The Saga

public class OrderSaga :
    Saga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>,
    IHandleMessages<CompleteOrder>,
    IHandleTimeouts<CancelOrder>
{
    static ILog log = LogManager.GetLogger<OrderSaga>();

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
    {
        mapper.ConfigureMapping<StartOrder>(message => message.OrderId)
                .ToSaga(sagaData => sagaData.OrderId);
        mapper.ConfigureMapping<CompleteOrder>(message => message.OrderId)
                .ToSaga(sagaData => sagaData.OrderId);
    }

    public async Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        // Correlation property Data.OrderId is automatically assigned with the value from message.OrderId;
        log.Info($"StartOrder received with OrderId {message.OrderId}");

        log.Info($@"Sending a CompleteOrder that will be delayed by 10 seconds
Stop the endpoint now to see the saga data in:
{LearningLocationHelper.GetSagaLocation<OrderSaga>(Data.Id)}");
        var completeOrder = new CompleteOrder
        {
            OrderId = Data.OrderId
        };
        var sendOptions = new SendOptions();
        sendOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10));
        sendOptions.RouteToThisEndpoint();
        await context.Send(completeOrder, sendOptions)
            .ConfigureAwait(false);

        var timeout = DateTime.UtcNow.AddSeconds(30);
        log.Info($@"Requesting a CancelOrder that will be executed in 30 seconds.
Stop the endpoint now to see the timeout data in the delayed directory
{LearningLocationHelper.TransportDelayedDirectory(timeout)}");
        await RequestTimeout<CancelOrder>(context, timeout)
            .ConfigureAwait(false);
    }

    public Task Handle(CompleteOrder message, IMessageHandlerContext context)
    {
        log.Info($"CompleteOrder received with OrderId {message.OrderId}");
        MarkAsComplete();
        return Task.CompletedTask;
    }

    public Task Timeout(CancelOrder state, IMessageHandlerContext context)
    {
        log.Info($"CompleteOrder not received soon enough OrderId {Data.OrderId}. Calling MarkAsComplete");
        MarkAsComplete();
        return Task.CompletedTask;
    }
}

Location Helper

This is a helper class used by the sample to derive the storage locations of the Learning Transport and the Learning Persistence.

public static class LearningLocationHelper
{
    public static string TransportDirectory;
    public static string SagaDirectory;

    static LearningLocationHelper()
    {
        var location = Assembly.GetExecutingAssembly().Location;
        var runningDirectory = Directory.GetParent(location).FullName;
        SagaDirectory = Path.Combine(runningDirectory, ".sagas");
        TransportDirectory = Path.GetFullPath(Path.Combine(runningDirectory, @"..\..\..\"));
    }

    public static string TransportDelayedDirectory(DateTime dateTime)
    {
        return Path.Combine(TransportDirectory, ".delayed", dateTime.ToString("yyyyMMddHHmmss"));
    }

    public static string GetSagaLocation<T>(Guid sagaId)
        where T : Saga
    {
        return Path.Combine(SagaDirectory, typeof(T).Name, $"{sagaId}.json");
    }
}

Related Articles


Last modified