This is the multi-page printable view of this section. Click here to print.
Dapr Workflow .NET SDK
- 1: DaprWorkflowClient lifetime management and registration
- 2: Workflow serialization in the .NET SDK
- 3: Multi-application workflows in the .NET SDK
- 4: Workflow history propagation in the .NET SDK
- 5: Workflow management operations with DaprWorkflowClient
- 6: Workflow versioning in the .NET SDK
- 7: .NET Workflow Examples
1 - DaprWorkflowClient lifetime management and registration
DaprWorkflowClient lifetime management and dependency injectionLifetime management
A DaprWorkflowClient holds access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar as well as other types used in the management and operation of Workflows. DaprWorkflowClient implements IAsyncDisposable to support eager cleanup of resources.
Dependency Injection
The AddDaprWorkflow() method will register the Dapr workflow services with ASP.NET Core dependency injection. This method requires an options delegate that defines each of the workflows and activities you wish to register and use in your application.
Change gRPC Message Size Limits
You can also configure gRPC message size limits for the workflow client during registration. This is useful when workflow payloads are larger than the default gRPC limits.
services
.AddDaprWorkflowClient()
.WithGrpcMessageSizeLimits(
maxReceiveMessageSize: 16 * 1024 * 1024,
maxSendMessageSize: 16 * 1024 * 1024);
Change gRPC Message Size Limits
You can also configure gRPC message size limits for the workflow client during registration. This is useful when workflow payloads are larger than the default gRPC limits.
services
.AddDaprWorkflowClient()
.WithGrpcMessageSizeLimits(
maxReceiveMessageSize: 16 * 1024 * 1024,
maxSendMessageSize: 16 * 1024 * 1024);
Singleton Registration
By default, the AddDaprWorkflow method registers the DaprWorkflowClient and associated services using a singleton lifetime. This means that the services are instantiated only a single time.
The following is an example of how registration of the DaprWorkflowClient as it would appear in a typical Program.cs file:
builder.Services.AddDaprWorkflow();
var app = builder.Build();
await app.RunAsync();
Note
Starting with Dapr .NET SDK 1.18.x a build-time source generator automatically discovers and registers all workflow and activity types. You no longer need to call RegisterWorkflow<T> or ReigsterActivity<T> inside the AddDaprWorkflow options delegate, though doing so is still fully supported.
If your workflows or activities live in a referenced assembly (not the entry-point project), add the following to the executing application’s .csproj to enable cross-assembly discovery:
<ItemGroup>
<CompilerVisibleProperty Include="DaprWorkflowVersioningScanReferences" />
</ItemGroup>
This is disabled by default for build-performance reasons.
Scoped Registration
While this may generally be acceptable in your use case, you may instead wish to override the lifetime specified. This is done by passing a ServiceLifetime argument in AddDaprWorkflow. For example, you may wish to inject another scoped service into your ASP.NET Core processing pipeline that needs context used by the DaprWorkflowClient that wouldn’t be available if the former service were registered as a singleton.
This is demonstrated in the following example:
builder.Services.AddDaprWorkflow(serviceLifetime: ServiceLifecycle.Scoped);
var app = builder.Build();
await app.RunAsync();
Transient Registration
Finally, Dapr services can also be registered using a transient lifetime meaning that they will be initialized every time they’re injected. This is demonstrated in the following example:
builder.Services.AddDaprWorkflow(serviceLifetime: ServiceLifecycle.Transient);
var app = builder.Build();
await app.RunAsync();
Using a DaprWorkflowClient instance
In an ASP.Net Core application, you can inject the DaprWorkflowClient into methods or controllers via method or constructor injection. This example demonstrates method injection in a minimal API scenario:
app.MapPost("/start", async (
[FromServices] DaprWorkflowClient daprWorkflowClient,
Order order
) => {
var instanceId = await daprWorkflowClient.ScheduleNewWorkflowAsync(
nameof(OrderProcessingWorkflow),
input: order);
return Results.Accepted(instanceId);
});
To create a DaprWorkflowClient instance in a console app, retrieve it from the ServiceProvider:
using var scope = host.Services.CreateAsyncScope();
var daprWorkflowClient = scope.ServiceProvider.GetRequiredService<DaprWorkflowClient>();
Now, you can use this client to perform workflow management operations such as starting, pausing, resuming, and terminating a workflow instance. See Workflow management operations with DaprWorkflowClient for more information on these operations.
Injecting Services into Workflow Activities
Workflow activities support the same dependency injection that developers have come to expect of modern C# applications. Assuming a proper registration at startup, any such type can be injected into the constructor of the workflow activity and available to utilize during the execution of the workflow. This makes it simple to add logging via an injected ILogger or access to other Dapr building blocks by injecting DaprClient or DaprJobsClient, for example.
internal sealed class SquareNumberActivity(ILogger logger) : WorkflowActivity<int, int>
{
public override Task<int> RunAsync(WorkflowActivityContext context, int input)
{
this.logger.LogInformation("Squaring the value {number}", input);
var result = input * input;
this.logger.LogInformation("Got a result of {squareResult}", result);
return Task.FromResult(result);
}
}
Activity task execution identifiers
Starting with Dapr .NET SDK v1.17.0, WorkflowActivityContext exposes a task execution identifier that is:
- Unique per activity task
- Stable across retries
This makes it useful for idempotency keys, task-level state tracking, and correlating logs.
internal sealed class IdempotentActivity : WorkflowActivity<int, int>
{
public override Task<int> RunAsync(WorkflowActivityContext context, int input)
{
var executionId = context.TaskExecutionId;
// Use executionId as your idempotency key or task state key.
return Task.FromResult(input * input);
}
}
Using ILogger in Workflow
Because workflows must be deterministic, it is not possible to inject arbitrary services into them. For example, if you were able to inject a standard ILogger into a workflow and it needed to be replayed because of an error, subsequent replay from the event source log would result in the log recording additional operations that didn’t actually take place a second or third time because their results were sourced from the log. This has the potential to introduce a significant amount of confusion. Rather, a replay-safe logger is made available for use within workflows. It will only log events the first time the workflow runs and will not log anything whenever the workflow is being replayed.
This logger can be retrieved from a method present on the WorkflowContext available on your workflow instance and otherwise used precisely as you might otherwise use an ILogger instance.
An end-to-end sample demonstrating this can be seen in the .NET SDK repository but a brief extraction of this sample is available below.
public sealed class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
string orderId = context.InstanceId;
var logger = context.CreateReplaySafeLogger<OrderProcessingWorkflow>(); //Use this method to access the logger instance
logger.LogInformation("Received order {orderId} for {quantity} {name} at ${totalCost}", orderId, order.Quantity, order.Name, order.TotalCost);
//...
}
}
Next steps
2 - Workflow serialization in the .NET SDK
Overview
Starting with Dapr .NET SDK v1.17.0, Dapr.Workflow supports pluggable serialization. The SDK continues to use
System.Text.Json by default, but you can now:
- Override the default
System.Text.Jsonsettings. - Register a custom serializer (for example, MessagePack or BSON).
Serialization configuration is entirely client-side and does not require a specific Dapr runtime version.
Note
This feature requires Dapr .NET SDK v1.17.0 or later.Compatibility and breaking changes
Warning
Changing serialization can be a breaking change for existing workflows. There is no supported migration path between serialization implementations.
All Dapr SDKs use a standard JSON convention by default. If you change the serialization settings or switch to a custom serializer in your .NET workflows and activities, cross-SDK workflows may fail because other SDKs might not support your custom serialization format.
Default JSON serialization
By default, the .NET SDK uses System.Text.Json with JsonSerializerDefaults.Web (see the
JsonSerializerDefaults.Web reference).
This means:
- Property names are case-insensitive.
- “camelCase” formatting is used for property names.
- Quoted numbers (JSON strings for number properties) are allowed when reading.
This default convention is designed to be compatible with other Dapr language SDKs for multi-app workflows.
Override System.Text.Json defaults
To override the default JSON settings, register the workflow client using the workflow builder so you can provide custom JsonSerializerOptions:
builder.Services
.AddDaprWorkflowBuilder(options =>
{
// Explicit registration is operation - the source generator discovers types automatically
options.RegisterWorkflow<MyWorkflow>();
options.RegisterActivity<MyActivity>();
})
.WithJsonSerializer(new JsonSerializerOptions { PropertyNamingPolicy = null });
All DaprWorkflowClient instances resolved from DI will use the provided JsonSerializerOptions for workflow and activity payloads.
Custom serialization providers
Custom serializers must implement the IWorkflowSerializer interface. The following example shows a
MessagePack-based implementation that encodes data as Base64 strings for transport:
public sealed class MessagePackWorkflowSerializer : IWorkflowSerializer
{
private readonly MessagePackSerializerOptions _options;
public MessagePackWorkflowSerializer(MessagePackSerializerOptions options)
{
_options = options;
}
/// <inheritdoc/>
public string Serialize(object? value, Type? inputType = null)
{
if (value == null)
{
return string.Empty;
}
var targetType = inputType ?? value.GetType();
var bytes = MessagePackSerializer.Serialize(targetType, value, _options);
return Convert.ToBase64String(bytes);
}
/// <inheritdoc/>
public T? Deserialize<T>(string? data)
{
return (T?)Deserialize(data, typeof(T));
}
/// <inheritdoc/>
public object? Deserialize(string? data, Type returnType)
{
if (returnType == null)
{
throw new ArgumentNullException(nameof(returnType));
}
if (string.IsNullOrEmpty(data))
{
return default;
}
try
{
var bytes = Convert.FromBase64String(data);
return MessagePackSerializer.Deserialize(returnType, bytes, _options);
}
catch (FormatException ex)
{
throw new InvalidOperationException(
"Failed to decode Base64 data. The input may not be valid MessagePack-serialized data.",
ex);
}
catch (MessagePackSerializationException ex)
{
throw new InvalidOperationException(
$"Failed to deserialize data to type {returnType.FullName}.",
ex);
}
}
}
Register a custom serializer
Register the serializer with the workflow builder:
builder.Services
.AddDaprWorkflowBuilder(options =>
{
// Explicit registration is operation - the source generator discovers types automatically
options.RegisterWorkflow<MyWorkflow>();
options.RegisterActivity<MyActivity>();
})
.WithSerializer(new MessagePackWorkflowSerializer(MessagePackSerializerOptions.Standard));
If you need DI-provided configuration, use the overload that receives an IServiceProvider:
builder.Services
.AddDaprWorkflowBuilder(options =>
{
// Explicit registration is operation - the source generator discovers types automatically
options.RegisterWorkflow<MyWorkflow>();
options.RegisterActivity<MyActivity>();
})
.WithSerializer(serviceProvider =>
{
var options = serviceProvider
.GetRequiredService<IOptions<MessagePackSerializerOptions>>()
.Value;
return new MessagePackWorkflowSerializer(options);
});
3 - Multi-application workflows in the .NET SDK
Overview
Dapr workflows can call activities or child workflows that are hosted in a different Dapr application. In .NET, multi-application workflows are supported starting with:
- Dapr runtime v1.16.0+
- Dapr .NET SDK v1.17.0+
Conceptual guidance and constraints are covered in Multi Application Workflows.
Requirements
Multi-application workflow calls require:
- The target app ID must exist and must register the activity or workflow you invoke.
- All participating app IDs must be in the same namespace.
- All participating app IDs must use the same workflow (actor) state store.
Call an activity in another application
Set TargetAppId on WorkflowTaskOptions when calling an activity to execute it in another app:
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new WorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallActivityAsync<string>(nameof(ActivityA), input, options);
return output;
}
}
The parent workflow continues to orchestrate locally and receives the activity result.
Call a child workflow in another application
Set TargetAppId on ChildWorkflowTaskOptions when calling a child workflow to execute it in another app:
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new ChildWorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallChildWorkflowAsync<string>(nameof(Workflow2), input, options);
return output;
}
}
Note
When calling a workflow in another application, you are responsible for using the workflow name expected by that app. If the target application is a .NET app that uses named workflow versioning, you can call it by its canonical (unversioned) workflow name and the target app will route it to the latest version. Named workflow versioning requires Dapr runtime v1.17.0 or later (multi-app workflows only require v1.16.0+).Next steps
4 - Workflow history propagation in the .NET SDK
Overview
Workflow history propagation allows a parent workflow to share its execution history — and optionally its full ancestor chain — with the child workflows and activities it calls. The child can then inspect those upstream events at runtime.
Common use cases include:
- Audit trails: Verifying a chain of custody across a multi-step workflow
- Fraud detection: Inspecting upstream decisions before committing a transaction
- AI agent orchestration: Passing context through hierarchical agent workflows
Conceptual guidance is covered in Workflow history propagation.
Note
This feature requires Dapr .NET SDK v1.18.0 or later and Dapr runtime v1.18.0 or later.Propagation scopes
Propagation is opt-in and per-call. Each call to CallActivityAsync or CallChildWorkflowAsync can independently specify a HistoryPropagationScope:
| Scope | Description |
|---|---|
None | Default. No history is propagated to the callee. |
OwnHistory | Propagates the calling workflow’s own events only. Ancestor history is dropped, acting as a trust boundary. |
Lineage | Propagates the calling workflow’s events plus the full ancestor chain it inherited from its own parent. |
Propagate history to a child workflow
Use WithHistoryPropagation on ChildWorkflowTaskOptions to opt a child workflow into receiving the parent’s history:
public sealed class MerchantCheckoutWorkflow : Workflow<Order, CheckoutResult>
{
public override async Task<CheckoutResult> RunAsync(WorkflowContext context, Order order)
{
// Activity without propagation — default behavior, no opt-in
await context.CallActivityAsync(nameof(ValidateMerchantActivity), order.MerchantId);
// Child workflow with full lineage propagation
var options = new ChildWorkflowTaskOptions()
.WithHistoryPropagation(HistoryPropagationScope.Lineage);
var result = await context.CallChildWorkflowAsync<PaymentResult>(
nameof(ProcessPaymentWorkflow), order, options);
return new CheckoutResult(result);
}
}
Calls that do not specify a propagation scope receive no history — other calls in the same workflow are unaffected by opt-ins.
Propagate history to an activity
The same WithHistoryPropagation extension is available on WorkflowTaskOptions for activity calls:
var options = new WorkflowTaskOptions()
.WithHistoryPropagation(HistoryPropagationScope.OwnHistory);
var auditResult = await context.CallActivityAsync<AuditResult>(
nameof(WriteAuditTrailActivity), payload, options);
Read propagated history
Inside a child workflow, call GetPropagatedHistory() on WorkflowContext to retrieve the history passed by the parent. The method returns null if propagation was not requested for this invocation.
public sealed class ProcessPaymentWorkflow : Workflow<Order, PaymentResult>
{
public override async Task<PaymentResult> RunAsync(WorkflowContext context, Order order)
{
var history = context.GetPropagatedHistory();
if (history != null)
{
foreach (var evt in history.Events)
{
// evt.Name, evt.InstanceId, evt.AppId
// evt.Activities — activity results for this ancestor
// evt.Workflows — child workflow results for this ancestor
}
}
return await context.CallActivityAsync<PaymentResult>(
nameof(ChargeCardActivity), order);
}
}
PropagatedHistory type
GetPropagatedHistory() returns a PropagatedHistory object (or null). Its Events property is an ordered list of PropagatedHistoryEvent values — one per ancestor workflow, in execution order (oldest ancestor first, immediate parent last).
Each PropagatedHistoryEvent represents a single ancestor workflow’s contribution to the propagated history:
| Member | Type | Description |
|---|---|---|
AppId | string | Dapr app ID that ran this workflow |
InstanceId | string | Workflow instance ID |
Name | string | The name of the workflow |
Activities | IReadOnlyList<PropagatedHistoryActivityResult> | Activity results for this workflow, in execution order |
Workflows | IReadOnlyList<PropagatedHistoryWorkflowResult> | Child workflow results for this workflow, in execution order |
PropagatedHistoryActivityResult type
Each PropagatedHistoryActivityResult is a sealed record describing a single activity invocation:
| Member | Type | Description |
|---|---|---|
Name | string | The scheduled name of the activity |
Status | PropagatedHistoryStatus | Lifecycle status — Pending, Completed, or Failed |
Input | string? | JSON-encoded input payload, or null when unset |
Output | string? | JSON-encoded output payload, or null when the activity has not completed |
FailureDetails | WorkflowTaskFailureDetails? | Failure details when Status is Failed, otherwise null |
PropagatedHistoryWorkflowResult type
Each PropagatedHistoryWorkflowResult is a sealed record describing a single child workflow invocation:
| Member | Type | Description |
|---|---|---|
Name | string | The scheduled name of the child workflow |
Status | PropagatedHistoryStatus | Lifecycle status — Pending, Completed, or Failed |
Output | string? | JSON-encoded output payload, or null when the workflow has not completed |
FailureDetails | WorkflowTaskFailureDetails? | Failure details when Status is Failed, otherwise null |
PropagatedHistoryStatus enum
PropagatedHistoryStatus reflects how far a task progressed past scheduling:
| Value | Description |
|---|---|
Pending | The task was scheduled but has not yet completed or failed |
Completed | The task completed successfully |
Failed | The task failed |
Query propagated history
PropagatedHistory query methods
PropagatedHistory provides Get methods that return lists and TryGet methods that return a single match (the most recent) via an out parameter. All Get methods return an empty list when no match is found.
| Method | Return type | Description |
|---|---|---|
GetByAppId(string) | IReadOnlyList<PropagatedHistoryEvent> | All events from the given Dapr app ID |
GetByInstanceId(string) | IReadOnlyList<PropagatedHistoryEvent> | All events from the given workflow instance ID |
GetEventsByWorkflowName(string) | IReadOnlyList<PropagatedHistoryEvent> | All events with the given workflow name |
TryGetLastWorkflowEventByName(string, out PropagatedHistoryEvent?) | bool | Gets the most recent event matching the workflow name |
GetAppIds() | IReadOnlyList<string> | Ordered, deduplicated list of app IDs in the history |
PropagatedHistoryEvent query methods
Each PropagatedHistoryEvent also provides query methods to inspect the activities and child workflows within that ancestor:
| Method | Return type | Description |
|---|---|---|
GetActivitiesByName(string) | IReadOnlyList<PropagatedHistoryActivityResult> | All activities matching the given name |
TryGetLastActivityByName(string, out PropagatedHistoryActivityResult?) | bool | Gets the most recent activity matching the name |
GetWorkflowsByName(string) | IReadOnlyList<PropagatedHistoryWorkflowResult> | All child workflows matching the given name |
TryGetLastWorkflowByName(string, out PropagatedHistoryWorkflowResult?) | bool | Gets the most recent child workflow matching the name |
Example
var history = context.GetPropagatedHistory();
if (history != null)
{
// By app ID — useful in multi-app workflows
var fromOrderApp = history.GetByAppId("order-app");
// By workflow instance ID
var fromSpecificRun = history.GetByInstanceId("checkout-abc123");
// By workflow name — returns all matches (e.g. recursion or ContinueAsNew)
var checkoutEvents = history.GetEventsByWorkflowName(nameof(MerchantCheckoutWorkflow));
// TryGet for a single match — avoids null ambiguity
if (history.TryGetLastWorkflowEventByName(nameof(MerchantCheckoutWorkflow), out var parentEvent))
{
// Inspect the parent's activities
var failedActivities = parentEvent.Activities
.Where(a => a.Status == PropagatedHistoryStatus.Failed)
.ToList();
// Or look up a specific activity by name
if (parentEvent.TryGetLastActivityByName(nameof(ValidateMerchantActivity), out var validation))
{
// validation.Status, validation.Output, validation.FailureDetails
}
}
}
Security considerations
By default, Dapr uses mutual TLS (mTLS) between sidecars for all cross-app communication, providing transport-layer protection for propagated history in multi-app workflow scenarios.
For stronger guarantees in production, enable WorkflowHistorySigning. This feature uses SPIFFE identity to cryptographically sign each history chunk, so the receiving workflow can verify the integrity and origin of the propagated history. Without signing enabled, Dapr emits a warning that propagated chunks lack cryptographic verification.
See Workflow history propagation for details on configuring WorkflowHistorySigning.
Next steps
5 - Workflow management operations with DaprWorkflowClient
DaprWorkflowClient to manage workflowsWorkflow management operations with DaprWorkflowClient
The DaprWorkflowClient class provides methods to manage workflow instances. Below are the operations you can perform using the DaprWorkflowClient.
Schedule a new workflow instance
To start a new workflow instance, use the ScheduleNewWorkflowAsync method. This method requires the workflow type name and an input required by the workflow. The workflow instancedId is an optional argument; if not provided, a new GUID is generated by the DaprWorkflowClient. The final optional argument is a startTime of type DateTimeOffset which can be used to define when the workflow instance should start. The method returns the instanceId of the scheduled workflow which is used for other workflow management operations.
var instanceId = $"order-workflow-{Guid.NewGuid().ToString()[..8]}";
var input = new Order("Paperclips", 1000, 9.95);
await daprWorkflowClient.ScheduleNewWorkflowAsync(
nameof(OrderProcessingWorkflow),
instanceId,
input);
Retrieve the status of a workflow instance
To get the current status of a workflow instance, use the GetWorkflowStateAsync method. This method requires the instance ID of the workflow and returns a WorkflowStatus object containing details about the workflow’s current state.
var workflowStatus = await daprWorkflowClient.GetWorkflowStateAsync(instanceId);
Raise an event to a running workflow instance
To send an event to a running workflow instance that is waiting for an external event, use the RaiseEventAsync method. This method requires the instance ID of the workflow, the name of the event, and optionally the event payload.
await daprWorkflowClient.RaiseEventAsync(instanceId, "Approval", true);
Suspend a running workflow instance
A running workflow instance can be paused using the SuspendWorkflowAsync method. This method requires the instance ID of the workflow. You can optionally provide a reason for suspending the workflow.
await daprWorkflowClient.SuspendWorkflowAsync(instanceId);
Resume a suspended workflow instance
A suspended workflow instance can be resumed using the ResumeWorkflowAsync method. This method requires the instance ID of the workflow. You can optionally provide a reason for resuming the workflow.
await daprWorkflowClient.ResumeWorkflowAsync(instanceId);
Terminate a workflow instance
To terminate a workflow instance, use the TerminateWorkflowAsync method. This method requires the instance ID of the workflow. You can optionally provide an output argument of type string. Terminating a workflow instance will also terminal all child workflow instances but it has no impact on in-flight activity executions.
await daprWorkflowClient.TerminateWorkflowAsync(instanceId);
Purge a workflow instance
To remove the workflow instance history from the Dapr Workflow state store, use the PurgeWorkflowAsync method. This method requires the instance ID of the workflow. Only completed, failed, or terminated workflow instances can be purged.
await daprWorkflowClient.PurgeWorkflowAsync(instanceId);
Next steps
6 - Workflow versioning in the .NET SDK
Overview
Dapr Workflow versioning lets you evolve workflows without breaking deterministic execution for in-flight instances. The .NET SDK supports two approaches:
- Patch-based versioning: introduce conditional branches guarded by
context.IsPatched("patch-name"). - Name-based versioning: create a new workflow type name and let a versioning strategy select the newest version.
Use patch-based versioning for small, in-place changes. Use name-based versioning for larger refactors where you want a clean new workflow type.
Note
Workflow versioning requires Dapr .NET SDK v1.17.0 or later and Dapr runtime v1.17.0 or later.When to use each approach
Patch-based versioning is a good fit when:
- You need small, incremental changes in an existing workflow.
- You want existing instances to keep deterministic behavior after deployment.
- You want to avoid introducing a new workflow type yet.
Name-based versioning is a good fit when:
- You want a clean, new workflow type without accumulated patches.
- You are ready to remove old patch blocks and refactor more freely.
- You want version selection to be automatic based on naming conventions.
Patch-based versioning
Patch-based versioning relies on a deterministic switch inside the workflow. Use WorkflowContext.IsPatched to guard
new behavior:
public override async Task RunAsync(WorkflowContext context, OrderPayload input)
{
await context.CallActivityAsync(nameof(ReserveInventoryActivity), input);
if (context.IsPatched("v2"))
{
await context.CallActivityAsync(nameof(ChargePaymentActivityV2), input);
}
else
{
await context.CallActivityAsync(nameof(ChargePaymentActivity), input);
}
}
Patch rules
- Patch names can appear multiple times in the same workflow and can be nested.
- Patch names must be unique across deployments. For example, if you deployed a workflow with a patch name
"v1", - you must not reuse
"v1"in later edits. Use a new identifier such as"v2"to avoid non-deterministic behavior. IsPatchedis available onWorkflowContext; no additional setup is required.
Tip
Keep patch names simple and monotonic (for example,"v2", "v3") so it is clear which deployment introduced each change.Name-based versioning
Name-based versioning lets you create a new workflow version by changing the workflow type name. The recommended pattern is to copy the existing workflow to a new file, rename the class, refactor as needed, and then start patching again if necessary.
For example, if you had OrderWorkflow, create OrderWorkflowV2 and refactor it. Older versions can remain for in-flight instances while new instances use the latest version.
Default naming behavior
By default, name-based versioning uses the built-in NumericVersionStrategy with a numeric suffix. The following are all valid examples:
MyWorkflow(treated as version0)MyWorkflow2MyWorkflowV2
The default strategy assumes higher numeric values are newer (for example, MyWorkflowV10 is newer than MyWorkflowV2). The .NET SDK also includes other built-in strategies (Date, SemVer, and Numeric) plus support for custom strategies.
Built-in strategies and options
The .NET SDK ships with several built-in name-based strategies. Each strategy supports options that let you tune how the suffix is parsed and what to do when no suffix is present.
- DateVersionStrategy: Derives a date-based version from a trailing suffix (for example,
MyWorkflow20220611). Options include:- Date format: Uses standard C# date formatting rules; defaults to
yyyyMMdd. - Default version: Used when no suffix is provided; defaults to
0. - Prefix: Optional prefix to match before the date suffix, with optional case-sensitivity.
- Date format: Uses standard C# date formatting rules; defaults to
- SemVerVersionStrategy: Derives a SemVer version from a trailing suffix (for example,
MyWorkflow1.2.3). Options include:- Prefix: Optional prefix to match before the SemVer suffix, with optional case-sensitivity.
- Prerelease/build support: Can parse prerelease annotations and build metadata.
- Default version: Optional default when no suffix is provided, if configured to allow missing suffixes.
- NumericVersionStrategy: Derives a numeric version from a trailing suffix (for example,
MyWorkflow42orMyWorkflowV42). Options include:- Prefix: Optional prefix to match before the numeric suffix, with optional case-sensitivity.
- Zero-padding width: Optional width to allow fixed-width numbers with leading zeroes.
- Default version: Used when no suffix is provided.
Configure name-based versioning
1. Install the versioning package
If you haven’t already added it, you need to add the Dapr.Workflow package to your project. No additional packages are required.
2. Register workflow versioning
Add versioning to DI during startup:
builder.Services.AddDaprWorkflowVersioning();
3. Choose a strategy (optional)
You can select a strategy by registering it in DI after calling AddDaprWorkflowVersioning:
builder.Services.UseDefaultWorkflowStrategy<NumericVersionStrategy>("workflow-versioning-options");
The optional string key is used to locate strategy options.
4. Configure strategy options (optional)
Register strategy options using the same key:
builder.Services.ConfigureStrategyOptions<NumericVersionStrategyOptions>("workflow-versioning-options", o =>
{
o.SuffixPrefix = "V";
});
Note
The option key strings must match exactly or the options will not be applied.5. Register workflows and activities
With the source generator included in Dapr.Workflow, both workflows and activities are discovered and registered automatically at build time. You do not need to opt into named workflow versioning to benefit from this automatic registration.
The AddDaprWorkflow() call is still required to wire up Dapr workflow services, but the options delegate is now optional:
builder.Services.AddDaprWorkflow();
Explicit registrations remain supported if you prefer them:
builder.Services.AddDaprWorkflow(w =>
{
w.RegisterActivity<SendEmailActivity>();
});
Once configured, named workflow versioning is applied automatically at runtime.
Cross-assembly workflow discovery
By default, the workflow versioning source generator only scans the executing assembly. If you keep workflows in a separate referenced assembly, those implementations are not discovered unless you opt in to reference scanning.
Reference scanning is disabled by default because it can increase build times (the generator must inspect all referenced assemblies for workflow and activity implementations). To enable it, add the following to the executing application’s .csproj file:
<ItemGroup>
<CompilerVisibleProperty Include="DaprWorkflowVersioningScanReferences" />
</ItemGroup>
When enabled, the source generator adds any discovered workflow and activity types from all referenced assemblies and registers them automatically. This applies regardless of whether you used named workflow versioning - it is a general-purpose discovery mechanism.
Override name and version
If you need to override the canonical name or version detected from the workflow type, apply the [WorkflowVersion]
attribute to the class implementing your workflow and specify values explicitly.
Best practices
- Schedule by canonical name: When scheduling a new workflow instance, use the canonical workflow name (the unversioned name). The SDK discovers versioned types and maps the canonical name to the latest version automatically. Avoid scheduling with a specific versioned name.
- Keep old versions: Preserve older workflow types indefinitely. You can remove them only after you are certain no in-flight instances reference them. Removing old versions too early can stall long-running workflows.
- Version in one direction: Always move versions forward. Avoid renaming or reusing older version identifiers.
Current limitations
- A single project cannot mix multiple versioning strategies for different workflows.
- There is no automatic migration from one versioning strategy to another.
- Workflow versioning doesn’t work across application boundaries. If you use multi-app workflows, you must use the type name expected in the target app based on any versioning strategy applied there. Other applications calling into this .NET application can simply use the canonical name if set up to use name-based workflow versioning.
Example project
An end-to-end example is available in the Dapr .NET SDK repository at examples/Workflow/WorkflowVersioning.
Next steps
7 - .NET Workflow Examples
Workflow tutorials in the Dapr Quickstarts repository
The Dapr Quickstarts repository on GitHub includes many workflow tutorials that showcase the various workflow patterns and how to use the workflow management operations. You can find these tutorials in the quickstarts/tutorials/workflow/csharp folder.
Workflow examples in the .NET SDK repository
The Dapr .NET SDK repository on GitHub contains several examples demonstrating how to use Dapr Workflows with .NET. You can find these examples in the examples/Workflow folder.