Transitioning Saga Correlation Ids

Component: Sql Persistence
NuGet Package NServiceBus.Persistence.Sql (3-pre)
Target NServiceBus Version: 7.x
This page targets a pre-release version and is subject to change prior to the final release.

This sample illustrates an approach for transitioning between different Correlation Ids in way that requires no endpoint downtime and no migration of saga data stored in sql.

This sample uses 3 "Phase" Endpoint Projects to illustrate the iterations of a single endpoint in one solution.

Prerequisites

MS SQL Server

  1. Ensure an instance of SQL Server Express (Version 2016 or above) is installed and accessible as .\SqlExpress.

Or, alternatively, change the connection string to point to different SQL Server instance.

At startup each endpoint will create its required SQL assets including databases, tables and schemas.

MySQL

  1. Ensure an instance of MySQL (Version 5.7 or above) is installed and accessible as on localhost and port 3306.
  2. Add the username to access the instance to an environment variable named MySqlUserName.
  3. Add the password to access the instance to an environment variable named MySqlPassword.

Or, alternatively, change the connection string to point to different MySQL instance.

Oracle

  1. Ensure an instance of Oracle Database (11g or later) is installed and accessible as on localhost on port 1521 with service name XE.
  2. Add the username to access the instance to an environment variable named OracleUserName.
  3. Add the password to access the instance to an environment variable named OraclePassword.

Or, alternatively, change the connection string to point to different Oracle instance.

Scenario

This samples uses a hypothetical "Order" scenario where the requirement is to transition from an an int correlation id OrderNumber to a guid correlation id OrderId.

Phases

Phase 1

In the initial phase an int OrderNumber is used. The saga maps to StartOrder.OrderNumber in the ConfigureMapping and correlates to OrderSagaData.OrderNumber via a SqlSagaAttribute with correlationProperty of OrderNumber.

Message

public class StartOrder :
    IMessage
{
    public int OrderNumber { get; set; }
}

Saga

public class OrderSaga :
    SqlSaga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>
{
    static ILog log = LogManager.GetLogger<OrderSaga>();

    protected override void ConfigureMapping(IMessagePropertyMapper mapper)
    {
        mapper.ConfigureMapping<StartOrder>(_ => _.OrderNumber);
    }

    protected override string CorrelationPropertyName => nameof(OrderSagaData.OrderNumber);

    public Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        Data.OrderNumber = message.OrderNumber;
        log.Info($"Received StartOrder message. Data.OrderNumber={Data.OrderNumber}");
        return Task.CompletedTask;
    }
}

SagaData

public class OrderSagaData :
    ContainSagaData
{
    public int OrderNumber { get; set; }
}

Phase 2

In the second phase a guid OrderId is added. The saga still maps to StartOrder.OrderNumber in the ConfigureMapping and correlates on to OrderSagaData.OrderNumber. However it also correlates to OrderSagaData.OrderId via a transitionalCorrelationProperty.

Message

public class StartOrder :
    IMessage
{
    public int OrderNumber { get; set; }
    public Guid OrderId { get; set; }
}

Saga

public class OrderSaga :
    SqlSaga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>
{
    static ILog log = LogManager.GetLogger<OrderSaga>();

    protected override void ConfigureMapping(IMessagePropertyMapper mapper)
    {
        mapper.ConfigureMapping<StartOrder>(_ => _.OrderNumber);
    }

    protected override string CorrelationPropertyName => nameof(OrderSagaData.OrderNumber);
    protected override string TransitionalCorrelationPropertyName => nameof(OrderSagaData.OrderId);

    public Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        Data.OrderId = message.OrderId;
        Data.OrderNumber = message.OrderNumber;
        log.Info($"Received StartOrder message. OrderId={Data.OrderId}. OrderNumber={Data.OrderNumber}");
        return Task.CompletedTask;
    }
}

SagaData

public class OrderSagaData :
    ContainSagaData
{
    public int OrderNumber { get; set; }
    public Guid OrderId { get; set; }
}
Prior to moving to Phase 3 it is necessary to verify that all existing sagas have the Correlation_OrderId column populated. This can either be inferred by the business knowledge (i.e. certain saga may have a know and constrained lifetime) or by querying the database.

Phase 3

In the third phase the int OrderNumber is removed leaving only the OrderId. The saga now maps to StartOrder.OrderId in the ConfigureMapping and correlates to OrderSagaData.OrderId via a SqlSagaAttribute with correlationProperty of OrderNumber.

Message

public class StartOrder :
    IMessage
{
    public Guid OrderId { get; set; }
}

Saga

public class OrderSaga :
    SqlSaga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>
{
    static ILog log = LogManager.GetLogger<OrderSaga>();

    protected override void ConfigureMapping(IMessagePropertyMapper mapper)
    {
        mapper.ConfigureMapping<StartOrder>(_ => _.OrderId);
    }

    protected override string CorrelationPropertyName => nameof(OrderSagaData.OrderId);

    public Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        Data.OrderId = message.OrderId;
        log.Info($"Received StartOrder message. OrderId={Data.OrderId}.");
        return Task.CompletedTask;
    }
}

SagaData

public class OrderSagaData :
    ContainSagaData
{
    public Guid OrderId { get; set; }
}

Related Articles

  • Sagas
    NServiceBus uses event-driven architecture to include fault-tolerance and scalability in long-term business processes.

Last modified