9.0 KiB
.NET SDK Versioning
For conceptual overview and guidance on choosing an approach, see references/core/versioning.md.
Patching API
The Patched() Method
The Workflow.Patched() method checks whether a Workflow should run new or old code:
[Workflow]
public class ShippingWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
if (Workflow.Patched("send-email-instead-of-fax"))
{
// New code path
await Workflow.ExecuteActivityAsync(
(ShippingActivities a) => a.SendEmailAsync(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
else
{
// Old code path (for replay of existing workflows)
await Workflow.ExecuteActivityAsync(
(ShippingActivities a) => a.SendFaxAsync(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
}
}
How it works:
- For new executions:
Patched()returnstrueand records a marker in the Workflow history - For replay with the marker:
Patched()returnstrue(history includes this patch) - For replay without the marker:
Patched()returnsfalse(history predates this patch)
Three-Step Patching Process
Warning: Failing to follow this process correctly will result in non-determinism errors for in-flight workflows.
Step 1: Patch in New Code
[Workflow]
public class OrderWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(Order order)
{
if (Workflow.Patched("add-fraud-check"))
{
await Workflow.ExecuteActivityAsync(
(OrderActivities a) => a.CheckFraudAsync(order),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(2) });
}
return await Workflow.ExecuteActivityAsync(
(OrderActivities a) => a.ProcessPaymentAsync(order),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
}
Step 2: Deprecate the Patch
Once all pre-patch Workflow Executions have completed:
[Workflow]
public class OrderWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(Order order)
{
Workflow.DeprecatePatch("add-fraud-check");
await Workflow.ExecuteActivityAsync(
(OrderActivities a) => a.CheckFraudAsync(order),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(2) });
return await Workflow.ExecuteActivityAsync(
(OrderActivities a) => a.ProcessPaymentAsync(order),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
}
Step 3: Remove the Patch
After all workflows with the deprecated patch marker have completed, remove the DeprecatePatch() call entirely:
[Workflow]
public class OrderWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(Order order)
{
await Workflow.ExecuteActivityAsync(
(OrderActivities a) => a.CheckFraudAsync(order),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(2) });
return await Workflow.ExecuteActivityAsync(
(OrderActivities a) => a.ProcessPaymentAsync(order),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
}
Query Filters for Finding Workflows by Version
Use List Filters to find workflows with specific patch versions:
# Find running workflows with a specific patch
temporal workflow list --query \
'WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running" AND TemporalChangeVersion = "add-fraud-check"'
# Find running workflows without any patch (pre-patch versions)
temporal workflow list --query \
'WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running" AND TemporalChangeVersion IS NULL'
Workflow Type Versioning
For incompatible changes, create a new Workflow Type instead of using patches:
[Workflow("PizzaWorkflow")]
public class PizzaWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(PizzaOrder order)
{
return await ProcessOrderV1Async(order);
}
}
[Workflow("PizzaWorkflowV2")]
public class PizzaWorkflowV2
{
[WorkflowRun]
public async Task<string> RunAsync(PizzaOrder order)
{
return await ProcessOrderV2Async(order);
}
}
Register both with the Worker:
var worker = new TemporalWorker(
client,
new TemporalWorkerOptions("pizza-task-queue")
.AddWorkflow<PizzaWorkflow>()
.AddWorkflow<PizzaWorkflowV2>()
.AddAllActivities(new PizzaActivities()));
Update client code to start new workflows with the new type:
// Old workflows continue on PizzaWorkflow
// New workflows use PizzaWorkflowV2
var handle = await client.StartWorkflowAsync(
(PizzaWorkflowV2 wf) => wf.RunAsync(order),
new(id: $"pizza-{order.Id}", taskQueue: "pizza-task-queue"));
Check for open executions before removing the old type:
temporal workflow list --query 'WorkflowType = "PizzaWorkflow" AND ExecutionStatus = "Running"'
Worker Versioning
Worker Versioning manages versions at the deployment level, allowing multiple Worker versions to run simultaneously.
Key Concepts
Worker Deployment: A logical service grouping similar Workers together (e.g., "loan-processor"). All versions of your code live under this umbrella.
Worker Deployment Version: A specific snapshot of your code identified by a deployment name and Build ID (e.g., "loan-processor:v1.0" or "loan-processor:abc123").
Configuring Workers for Versioning
using Temporalio.Worker;
var worker = new TemporalWorker(
client,
new TemporalWorkerOptions("my-task-queue")
{
DeploymentOptions = new WorkerDeploymentOptions(
DeploymentName: "my-service",
BuildId: Environment.GetEnvironmentVariable("BUILD_ID") ?? "dev"),
UseWorkerVersioning = true,
}
.AddWorkflow<MyWorkflow>()
.AddAllActivities(new MyActivities()));
Configuration parameters:
UseWorkerVersioning: Enables Worker VersioningDeploymentOptions: Identifies the Worker Deployment Version (deployment name + build ID)- Build ID: Typically a git commit hash, version number, or timestamp
PINNED vs AUTO_UPGRADE Behaviors
PINNED Behavior
Workflows stay locked to their original Worker version:
[Workflow(VersioningBehavior = VersioningBehavior.Pinned)]
public class StableWorkflow { /* ... */ }
When to use PINNED:
- Short-running workflows (minutes to hours)
- Consistency is critical (e.g., financial transactions)
- You want to eliminate version compatibility complexity
- Building new applications and want simplest development experience
AUTO_UPGRADE Behavior
Workflows can move to newer versions:
[Workflow(VersioningBehavior = VersioningBehavior.AutoUpgrade)]
public class UpgradableWorkflow { /* ... */ }
When to use AUTO_UPGRADE:
- Long-running workflows (weeks or months)
- Workflows need to benefit from bug fixes during execution
- Migrating from traditional rolling deployments
- You are already using patching APIs for version transitions
Important: AUTO_UPGRADE workflows still need patching to handle version transitions safely since they can move between Worker versions.
Worker Configuration with Default Behavior
var worker = new TemporalWorker(
client,
new TemporalWorkerOptions("my-task-queue")
{
DeploymentOptions = new WorkerDeploymentOptions(
DeploymentName: "order-service",
BuildId: Environment.GetEnvironmentVariable("BUILD_ID") ?? "dev")
{
DefaultVersioningBehavior = VersioningBehavior.Pinned,
},
UseWorkerVersioning = true,
}
.AddWorkflow<OrderWorkflow>()
.AddAllActivities(new OrderActivities()));
Deployment Strategies
Blue-Green Deployments
Maintain two environments and switch traffic between them:
- Deploy new code to idle environment
- Run tests and validation
- Switch traffic to new environment
- Keep old environment for instant rollback
Rainbow Deployments
Multiple versions run simultaneously:
- New workflows use latest version
- Existing workflows complete on their original version
- Add new versions alongside existing ones
- Gradually sunset old versions as workflows complete
Querying Workflows by Worker Version
# Find workflows on a specific Worker version
temporal workflow list --query \
'TemporalWorkerDeploymentVersion = "my-service:v1.0.0" AND ExecutionStatus = "Running"'
Best Practices
- Check for open executions before removing old code paths
- Use descriptive patch IDs that explain the change (e.g., "add-fraud-check" not "patch-1")
- Deploy patches incrementally: patch, deprecate, remove
- Use PINNED for short workflows to simplify version management
- Use AUTO_UPGRADE with patching for long-running workflows that need updates
- Generate Build IDs from code (git hash) to ensure changes produce new versions
- Avoid rolling deployments for high-availability services with long-running workflows