Orchestration code must be deterministic—it must produce the same sequence of operations every time it runs with the same history. This is required because orchestrations are replayed to rebuild state after interruptions.
The same input must always produce the same sequence of durable operations.
Durable operations include:
ScheduleTask/ScheduleWithRetryCreateTimerWaitForExternalEventCreateSubOrchestrationInstanceContinueAsNew
// ❌ WRONG - Non-deterministic
if (DateTime.UtcNow > deadline)
{
await context.ScheduleTask<string>(typeof(ExpiredActivity), input);
}
// ✅ CORRECT - Use orchestration time
if (context.CurrentUtcDateTime > deadline)
{
await context.ScheduleTask<string>(typeof(ExpiredActivity), input);
}// ❌ WRONG - Different on replay
var random = new Random();
if (random.Next(100) > 50)
{
await context.ScheduleTask<string>(typeof(ActivityA), input);
}
// ✅ CORRECT - Get random value from activity
var randomValue = await context.ScheduleTask<int>(typeof(GetRandomNumberActivity), 100);
if (randomValue > 50)
{
await context.ScheduleTask<string>(typeof(ActivityA), input);
}
// ✅ OR use a fixed seed
var random = new Random(42); // Fixed seed
if (random.Next(100) > 50)
{
await context.ScheduleTask<string>(typeof(ActivityA), input);
}// ❌ WRONG - Different GUID on replay
var id = Guid.NewGuid().ToString();
await context.ScheduleTask<string>(typeof(ProcessActivity), id);
// ✅ CORRECT - Use orchestration's NewGuid
var id = context.NewGuid().ToString();
await context.ScheduleTask<string>(typeof(ProcessActivity), id);
// ✅ Also correct - Get from activity
var id = await context.ScheduleTask<string>(typeof(GenerateIdActivity), null);// ❌ WRONG - May change between replays
var endpoint = Environment.GetEnvironmentVariable("API_ENDPOINT");
await context.ScheduleTask<string>(typeof(CallApiActivity), endpoint);
// ✅ CORRECT - Pass as input or read in activity
// Option 1: Pass as orchestration input
await context.ScheduleTask<string>(typeof(CallApiActivity), input.ApiEndpoint);
// Option 2: Read in activity
await context.ScheduleTask<string>(typeof(CallApiWithConfigActivity), input);// ❌ WRONG - Side effect, non-deterministic
var response = await httpClient.GetAsync("https://api.example.com/data");
var data = await response.Content.ReadAsStringAsync();
// ✅ CORRECT - Use activity for network calls
var data = await context.ScheduleTask<string>(typeof(FetchDataActivity), "https://api.example.com/data");Note
Awaiting a non-durable task like httpClient.GetAsync may cause the orchestration to hang indefinitely.
// ❌ WRONG - Data may change between replays
var user = await dbContext.Users.FindAsync(userId);
// ✅ CORRECT - Use activity
var user = await context.ScheduleTask<User>(typeof(GetUserActivity), userId);Note
Awaiting a non-durable task like dbContext.Users.FindAsync may cause the orchestration to hang indefinitely.
// ❌ WRONG - Blocks thread, doesn't persist
Thread.Sleep(TimeSpan.FromMinutes(5));
await Task.Delay(TimeSpan.FromMinutes(5));
// ✅ CORRECT - Use durable timer
await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(5), true);Note
Awaiting a non-durable task like Task.Delay may cause the orchestration to hang indefinitely.
// ❌ WRONG - State not preserved across replays
static int counter = 0;
counter++;
if (counter > 5) { ... }
// ✅ CORRECT - Use orchestration input/output for state
public override async Task<int> RunTask(OrchestrationContext context, int currentCount)
{
currentCount++;
if (currentCount > 5) { ... }
}// ❌ WRONG - HashSet and Dictionary iteration order is not guaranteed
var items = new HashSet<string> { "a", "b", "c" };
foreach (var item in items)
{
await context.ScheduleTask<string>(typeof(ProcessActivity), item);
}
// ✅ CORRECT - Use ordered collection
var items = new List<string> { "a", "b", "c" };
foreach (var item in items)
{
await context.ScheduleTask<string>(typeof(ProcessActivity), item);
}// ❌ WRONG - Background tasks are non-deterministic and may not complete before replay
await Task.Run(() => ProcessData(input));
// ❌ WRONG - Manual thread creation is non-deterministic
var thread = new Thread(() => DoWork());
thread.Start();
// ❌ WRONG - ThreadPool work is non-deterministic
ThreadPool.QueueUserWorkItem(_ => ProcessItem(input));
// ✅ CORRECT - Use activities for background work
var result = await context.ScheduleTask<string>(typeof(ProcessDataActivity), input);
// ✅ CORRECT - Use fan-out pattern for parallel work
var tasks = input.Items.Select(item =>
context.ScheduleTask<string>(typeof(ProcessItemActivity), item));
var results = await Task.WhenAll(tasks);Note
Task.Run, ThreadPool.QueueUserWorkItem, and manual thread creation introduce non-determinism because:
- The work may complete at different times during replay
- Background threads don't participate in orchestration checkpointing
- Results are not captured in the orchestration history
// ✅ Safe - deterministic computation
var sum = input.Values.Sum();
var filtered = input.Items.Where(x => x.IsActive).ToList();
var formatted = $"Order {input.OrderId}: {input.Description}";// ✅ Safe - consistent across replays
var instanceId = context.OrchestrationInstance.InstanceId;
var currentTime = context.CurrentUtcDateTime;
var newId = context.NewGuid();// ✅ Safe - result comes from history during replay
var status = await context.ScheduleTask<OrderStatus>(typeof(GetStatusActivity), orderId);
if (status == OrderStatus.Approved)
{
await context.ScheduleTask<string>(typeof(ProcessOrderActivity), orderId);
}// ✅ Safe - loop bounds are deterministic
for (int i = 0; i < input.Items.Count; i++)
{
await context.ScheduleTask<string>(typeof(ProcessItemActivity), input.Items[i]);
}// ✅ Safe - Task.WhenAll is deterministic
var tasks = input.Items.Select(item =>
context.ScheduleTask<string>(typeof(ProcessItemActivity), item));
var results = await Task.WhenAll(tasks);| Operation | Allowed in Orchestration? | Alternative |
|---|---|---|
DateTime.UtcNow |
❌ No | context.CurrentUtcDateTime |
Guid.NewGuid() |
❌ No | context.NewGuid() |
Random.Next() |
❌ No | Get from activity |
Thread.Sleep() / Task.Delay() |
❌ No | context.CreateTimer() |
Task.Run() |
❌ No | Use activity or fan-out |
ThreadPool.QueueUserWorkItem() |
❌ No | Use activity |
| Manual thread creation | ❌ No | Use activity |
| HTTP calls | ❌ No | Use activity |
| Database queries | ❌ No | Use activity |
| File I/O | ❌ No | Use activity |
| Environment variables | Pass as input or read in activity | |
| Static mutable state | ❌ No | Use orchestration state |
HashSet or Dictionary iteration |
Use List or sorted collection |
|
| Local computation | ✅ Yes | — |
| String manipulation | ✅ Yes | — |
| LINQ queries (on local data) | ✅ Yes | — |
Some non-deterministic issues cause runtime errors:
NonDeterministicOrchestrationException: The orchestration 'MyOrchestration'
has a non-deterministic replay detected. The history expected 'TaskScheduled'
for 'ActivityA' but got 'TaskScheduled' for 'ActivityB'.
Consider using analyzers or code reviews to catch issues:
- Review all
DateTime,Guid,Randomusage - Search for HTTP client usage
- Check for
Thread.SleeporTask.Delay - Check for
Task.Run,ThreadPool, ornew Thread
- Replay and Durability — Why determinism matters
- Versioning — Safely updating orchestration code
- Error Handling — Handling failures deterministically