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
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
. A Docker image can be used to accomplish this by running docker run -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d mcr.
in a terminal.
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
Ensure an instance of MySQL (Version 5.7 or above) is installed and accessible on localhost
and port 3306
. A Docker image can be used to accomplish this by running docker run --name mysql -e 'MYSQL_ROOT_PASSWORD=yourStrong(!)Password' -p 3306:3306 -d mysql:latest
in a terminal.
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
Ensure an instance of Oracle (Version 11g or later) is installed and accessible on localhost
and port 1521
. A Docker image can be used to accomplish this by running docker run --name oracle -e 'ORACLE_PASSWORD=yourStrong(!)Password' -p 1521:1521 -d gvenzl/
in a terminal.
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
Ensure an instance of PostgreSQL (Version 10 or later) is installed and accessible on localhost
and port 5432
. A Docker image can be used to accomplish this by running docker run --name postgres -e 'POSTGRES_PASSWORD=yourStrong(!)Password' -p 5432:5432 -d postgres:latest
in a terminal.
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.
from the incoming message to OrderSagaData.
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.
to OrderSagaData.
in the ConfigureHowToFindSaga
method. However it also introduces a correlation to OrderSagaData.
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.
to OrderSagaData.
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; }
}