Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

Transitioning Saga Correlation IDs

Component: Sql Persistence
NuGet Package: NServiceBus.Persistence.Sql (7.x)
Target Version: NServiceBus 8.x

This sample illustrates an approach for transitioning between different correlation IDs in a way that requires no endpoint downtime or migration of saga data stored in sql.

The sample uses three "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 (Version 2016 or above for custom saga finders sample, or Version 2012 or above for other samples) is installed and accessible on localhost and port 1433.

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

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

MySQL

  1. Ensure an instance of MySQL (Version 5.7 or above) is installed and accessible 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.

Alternatively, change the connection string to point to different MySQL instance.

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

Oracle

  1. Ensure an instance of Oracle Database (Version 11g or later) is installed and accessible 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.

Alternatively, change the connection string to point to different Oracle instance.

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

PostgreSQL

  1. Ensure an instance of PostgreSQL (Version 10 or later) is installed and accessible on localhost.
  2. Add the username to access the instance to an environment variable named PostgreSqlUserName.
  3. Add the password to access the instance to an environment variable named PostgreSqlPassword.

Alternatively, change the connection string to point to different PostgreSQL instance.

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

Scenario

The sample uses a hypothetical "Order" scenario where the requirement is to transition from an an integer correlation ID OrderNumber to a GUID correlation ID OrderId.

To move between phases, after running each phase adjust the startup project list in solution properties. E.g. after phase 1, disable endpoints that contain "Phase1" in the name, and enable endpoints that contain "Phase2".

Phases

Phase 1

In the initial phase, an integer OrderNumber is used for the correlation ID. In the ConfigureHowToFindSaga method, the saga maps StartOrder.OrderNumber from the incoming message to OrderSagaData.OrderNumber in the saga data.

Message

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

Saga

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

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
    {
        mapper.MapSaga(saga => saga.OrderNumber)
            .ToMessage<StartOrder>(msg => msg.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 StartOrder.OrderNumber to OrderSagaData.OrderNumber in the ConfigureHowToFindSaga method. However it also introduces a correlation to OrderSagaData.OrderId via a transitionalCorrelationProperty in the [SqlSaga] attribute.

Message

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

Saga

[SqlSaga(transitionalCorrelationProperty: nameof(OrderSagaData.OrderId))]
public class OrderSaga :
    Saga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>
{
    readonly static ILog log = LogManager.GetLogger<OrderSaga>();

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
    {
        mapper.MapSaga(saga => saga.OrderNumber)
            .ToMessage<StartOrder>(msg => msg.OrderNumber);
    }

    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 known and constrained lifetime) or by querying the database.

Phase 3

In the third phase, the integer OrderNumber is removed leaving only the OrderId. The saga now maps StartOrder.OrderId to OrderSagaData.OrderId in the ConfigureHowToFindSaga method.

Message

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

Saga

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

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
    {
        mapper.MapSaga(saga => saga.OrderId)
            .ToMessage<StartOrder>(msg =>msg.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
    Maintain statefulness in distributed systems with the saga pattern and NServiceBus' event-driven architecture with built-in fault-tolerance and scalability.