.NET: [Feature Branch] Adding support for events & shared state in durable workflows (#4020)

* Adding support for events & shared state in durable workflows.

* PR feedback fixes

* PR feedback fixes.

* Add YieldOutputAsync calls to 05_WorkflowEvents sample executors

The integration test asserts that WorkflowOutputEvent is found in the
stream, but the sample executors only used AddEventAsync for custom
events and never called YieldOutputAsync. Since WorkflowOutputEvent is
only emitted via explicit YieldOutputAsync calls, the assertion would
fail. Added YieldOutputAsync to each executor to match the test
expectation and demonstrate the API in the sample.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix deserialization to use shared serializer options.

* PR feedback updates.

* Sample cleanup

* PR feedback fixes

* Addressing PR review feedback for DurableStreamingWorkflowRun

   - Use -1 instead of 0 for taskId in TaskFailedException when task ID is not relevant.
   - Add [NotNullWhen(true)] to TryParseWorkflowResult out parameter following .NET TryXXX conventions.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Shyju Krishnankutty
2026-02-20 16:49:47 -08:00
committed by GitHub
co-authored by Copilot
parent b62b1f2191
commit 3256baa8b6
37 changed files with 3498 additions and 215 deletions
@@ -15,15 +15,6 @@ namespace Microsoft.Agents.AI.DurableTask.Workflows;
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Workflow and executor types are registered at startup.")]
internal static class DurableActivityExecutor
{
/// <summary>
/// Shared JSON options that match the DurableDataConverter settings.
/// </summary>
private static readonly JsonSerializerOptions s_jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
/// <summary>
/// Executes an activity using the provided executor binding.
/// </summary>
@@ -53,7 +44,7 @@ internal static class DurableActivityExecutor
Type inputType = ResolveInputType(inputWithState?.InputTypeName, executor.InputTypes);
object typedInput = DeserializeInput(executorInput, inputType);
DurableActivityContext workflowContext = new(sharedState, executor);
DurableWorkflowContext workflowContext = new(sharedState, executor);
object? result = await executor.ExecuteAsync(
typedInput,
new TypeId(inputType),
@@ -63,19 +54,34 @@ internal static class DurableActivityExecutor
return SerializeActivityOutput(result, workflowContext);
}
private static string SerializeActivityOutput(object? result, DurableActivityContext context)
private static string SerializeActivityOutput(object? result, DurableWorkflowContext context)
{
DurableActivityOutput output = new()
DurableExecutorOutput output = new()
{
Result = SerializeResult(result),
SentMessages = context.SentMessages.ConvertAll(m => new SentMessageInfo
{
Message = m.Message,
TypeName = m.TypeName
})
StateUpdates = context.StateUpdates,
ClearedScopes = [.. context.ClearedScopes],
Events = context.OutboundEvents.ConvertAll(SerializeEvent),
SentMessages = context.SentMessages,
HaltRequested = context.HaltRequested
};
return JsonSerializer.Serialize(output, DurableWorkflowJsonContext.Default.DurableActivityOutput);
return JsonSerializer.Serialize(output, DurableWorkflowJsonContext.Default.DurableExecutorOutput);
}
/// <summary>
/// Serializes a workflow event with type information for proper deserialization.
/// </summary>
private static string SerializeEvent(WorkflowEvent evt)
{
Type eventType = evt.GetType();
TypedPayload wrapper = new()
{
TypeName = eventType.AssemblyQualifiedName,
Data = JsonSerializer.Serialize(evt, eventType, DurableSerialization.Options)
};
return JsonSerializer.Serialize(wrapper, DurableWorkflowJsonContext.Default.TypedPayload);
}
private static string SerializeResult(object? result)
@@ -90,7 +96,7 @@ internal static class DurableActivityExecutor
return str;
}
return JsonSerializer.Serialize(result, result.GetType(), s_jsonOptions);
return JsonSerializer.Serialize(result, result.GetType(), DurableSerialization.Options);
}
private static DurableActivityInput? TryDeserializeActivityInput(string input)
@@ -112,7 +118,7 @@ internal static class DurableActivityExecutor
return input;
}
return JsonSerializer.Deserialize(input, targetType, s_jsonOptions)
return JsonSerializer.Deserialize(input, targetType, DurableSerialization.Options)
?? throw new InvalidOperationException($"Failed to deserialize input to type '{targetType.Name}'.");
}