Versioning - .NET SDK
This page shows how to do the following:
- Use the .NET SDK Patching API
- Patching in new code
- Understanding deprecated Patches in the .NET SDK
- Safe Deployment of PostPatchActivity
Introduction to Versioning
Because we design for potentially long running Workflows at scale, versioning with Temporal works differently. We explain more in this optional 30 minute introduction:
Use the .NET SDK Patching API
How to use the .NET SDK Patching API using the Temporal .NET SDK
In principle, the .NET SDK's patching mechanism operates similarly to other SDKs in a "feature-flag" fashion. However, the "versioning" API now uses the concept of "patching in" code.
To understand this, you can break it down into three steps, which reflect three stages of migration:
- Running
PrePatchActivity
code while concurrently patching inPostPatchActivity
. - Running
PostPatchActivity
code with deprecation markers formy-patch
patches. - Running only the
PostPatchActivity
code.
Let's walk through this process in sequence.
Suppose you have an initial Workflow version called PrePatchActivity
:
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PrePatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
Now, you want to update your code to run PostPatchActivity
instead. This represents your desired end state.
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
Problem: You cannot deploy PostPatchActivity
directly until you're certain there are no more running Workflows created using the PrePatchActivity
code, otherwise you are likely to cause a nondeterminism error.
Instead, you'll need to deploy PostPatchActivity
and use the Patched method to determine which version of the code to execute.
Implementing patching involves three steps:
- Use Patched to patch in new code and run it alongside the old code.
- Remove the old code and apply DeprecatePatch.
- Once you're confident that all old Workflows have finished executing, remove
DeprecatePatch
.
Overview
The following sample shows how the patched()
function behaves, providing explanations at each stage of the patching flow:
if (patched('v3')) {
// This is the newest version of the code.
// The above patched statement following will do
// one of the following three things:
// 1. If the execution is not Replaying, it will evaluate
// to true and write a Marker Event to the history
// with a patch id v3. This code block will run.
// 2. If the execution is Replaying, and the original
// run put a Patch ID v3 at this location in the event
// history, it will evaluate to True, and this code block
// will run.
// 3. If the execution is Replaying, and the original
// run has a Patch ID other than v3 at this location in the event
// history, it will evaluate to False, and this code block won't
// run.
} else if (patched('v2')) {
// This is the second version of the code.
// The above patched statement following will do
// one of the following three things:
// 1. If the execution is not Replaying, the execution
// won't get here because the first patched statement
// will be True.
// 2. If the execution is Replaying, and the original
// run put a Patch ID v2 marker at this location in the event
// history, it will evaluate to True, and this code block
// will run.
// 3. If the execution is Replaying, and the original
// run has a Patch ID other than v2 at this location in the event
// history, or doesn't have a patch marker at this location in the event
// history, it will evaluate to False, and this code block won't
// run.
} else {
// This is the original version of the code.
//
// The above patched statement following will do
// one of the following three things:
//
// 1. If the execution is not Replaying, the execution
// won't get here because the first patched statement
// will be True.
// 2. If the execution is Replaying, and the original
// run had a patch marker v3 or v2 at this location in the event
// history, the execution
// won't get here because the first or second patched statement
// will be True (respectively).
// 3. If the execution is Replaying, and condition 2
// doesn't hold, then it will run this code.
}
To add more clarity, the following sample shows how patched()
will behave in a different conditional block.
In this case, the code's conditional block doesn't have the newest code at the top.
Because patched()
will always return true
when not Replaying, this snippet will run the v2
branch instead of v3
in new executions.
if (patched('v2')) {
// This is bad because when doing an original execution (i.e. not replaying),
// all patched statements evaluate to True (and put a marker
// in the event history), which means that new executions
// will use v2, and miss v3 below
}
else if (patched('v3')) {}
else {}
The moral of the story is that when not Replaying, patched()
will return true and write the patch ID to the Event History.
And when Replaying, it will only return true if the patch ID matches that in the Event History.
Patching in new code
Using Patched
inserts a marker into the Workflow History.
During replay, if a Worker encounters a history with that marker, it will fail the Workflow task when the Workflow code doesn't produce the same patch marker (in this case, my-patch
). This ensures you can safely deploy code from PostPatchActivity
as a "feature flag" alongside the original version (PrePatchActivity
).
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
if (Workflow.Patched("my-patch"))
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
else
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PrePatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
// ...
}
}
Understanding deprecated Patches in the .NET SDK
After ensuring that all Workflows started with PrePatchActivity
code have finished, you can deprecate the patch.
Deprecated patches serve as a bridge between PrePatchActivity
and PostPatchActivity
. They function similarly to regular patches by adding a marker to the Workflow History. However, this marker won't cause a replay failure when the Workflow code doesn't produce it.
If, during the deployment of PostPatchActivity
, there are still live Workers running PrePatchActivity
code and these Workers pick up Workflow histories generated by PostPatchActivity
, they will safely use the patched branch.
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
Workflow.DeprecatePatch("my-patch")
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
Safe Deployment of PostPatchActivity
You can safely deploy PostPatchActivity
once all Workflows labeled my-patch or earlier are finished, based on the previously mentioned assertion.
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
Best Practice of Using Classes as Arguments and Returns
As a side note on the Patching API, its behavior is why Temporal recommends using a single object as arguments and returns from Signals, Queries, Updates, and Activities, rather than using multiple arguments/returns.
The Patching API's main use case is to support branching in an if
block of a method body.
It is not designed to be used to set different methods or method signatures for different Workflow Versions.
Because of this, Temporal recommends that each Signal, Activity, etc, accepts a single object and returns a single object, so the method signature can stay constant, and you can do your versioning logic using patched()
within the method body.