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 port1433
.
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
- Ensure an instance of MySQL (Version 5.7 or above) is installed and accessible on
localhost
and port3306
. - Add the username to access the instance to an environment variable named
MySqlUserName
. - 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
- Ensure an instance of Oracle Database (Version 11g or later) is installed and accessible on
localhost
on port1521
with service nameXE
. - Add the username to access the instance to an environment variable named
OracleUserName
. - 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
- Ensure an instance of PostgreSQL (Version 10 or later) is installed and accessible on
localhost
. - Add the username to access the instance to an environment variable named
PostgreSqlUserName
. - 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.
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; }
}