From ab590723a665d875eefc9f0223c521b00404377a Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Sun, 1 Mar 2026 08:22:13 -0800 Subject: [PATCH 1/4] Propagate trace context for entity operations Entity operations triggered from orchestrations don't propagate distributed trace context because SendEntityMessageAction lacks a parentTraceContext field. This causes entity operations to appear disconnected from their parent orchestration traces. Changes: - Vendor updated proto with parentTraceContext field on SendEntityMessageAction (see https://github.com/microsoft/durabletask-protobuf/pull/64) - Set ParentTraceContext in ProtoUtils.ConstructOrchestratorResponse for entity message actions (matching existing behavior for tasks and sub-orchestrations) - Add unit tests for entity trace context propagation Note: A corresponding server-side change is also needed in the Durable Task Scheduler backend to propagate SendEntityMessageAction.ParentTraceContext to OperationRequest.traceContext on entity work items. Fixes #653 --- src/Grpc/orchestrator_service.proto | 2 + src/Shared/Grpc/ProtoUtils.cs | 6 +- .../Grpc.Tests/ProtoUtilsTraceContextTests.cs | 271 ++++++++++++++++++ 3 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 0c34d986..2ba4bc5b 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -318,6 +318,8 @@ message SendEntityMessageAction { EntityLockRequestedEvent entityLockRequested = 3; EntityUnlockSentEvent entityUnlockSent = 4; } + + TraceContext parentTraceContext = 5; } message OrchestratorAction { diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index 2412fac3..c7fe00f3 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -414,6 +414,8 @@ internal static P.OrchestratorResponse ConstructOrchestratorResponse( sendAction, out string requestId); + sendAction.ParentTraceContext = CreateTraceContext(); + entityConversionState.EntityRequestIds.Add(requestId); switch (sendAction.EntityMessageTypeCase) @@ -1054,7 +1056,7 @@ internal static T Base64Decode(this MessageParser parser, string encodedMessa case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StringValue: string stringValue = value.StringValue; - // If the value starts with the 'dt:' prefix, it may represent a DateTime value — attempt to parse it. + // If the value starts with the 'dt:' prefix, it may represent a DateTime value � attempt to parse it. if (stringValue.StartsWith("dt:", StringComparison.Ordinal)) { if (DateTime.TryParse(stringValue[3..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime date)) @@ -1063,7 +1065,7 @@ internal static T Base64Decode(this MessageParser parser, string encodedMessa } } - // If the value starts with the 'dto:' prefix, it may represent a DateTime value — attempt to parse it. + // If the value starts with the 'dto:' prefix, it may represent a DateTime value � attempt to parse it. if (stringValue.StartsWith("dto:", StringComparison.Ordinal)) { if (DateTimeOffset.TryParse(stringValue[4..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset date)) diff --git a/test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs b/test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs new file mode 100644 index 00000000..a4c49ffa --- /dev/null +++ b/test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using DurableTask.Core; +using DurableTask.Core.Command; +using Newtonsoft.Json; +using P = Microsoft.DurableTask.Protobuf; + +namespace Microsoft.DurableTask.Worker.Grpc.Tests; + +public class ProtoUtilsTraceContextTests +{ + static readonly ActivitySource TestSource = new(nameof(ProtoUtilsTraceContextTests)); + + [Fact] + public void SendEntityMessage_SignalEntity_SetsParentTraceContext() + { + // Arrange + using ActivityListener listener = CreateListener(); + using Activity? orchestrationActivity = TestSource.StartActivity("TestOrchestration"); + orchestrationActivity.Should().NotBeNull(); + + string requestId = Guid.NewGuid().ToString(); + string entityInstanceId = "@counter@myKey"; + string eventData = JsonConvert.SerializeObject(new + { + op = "increment", + signal = true, + id = requestId, + }); + + SendEventOrchestratorAction sendEventAction = new() + { + Id = 1, + Instance = new OrchestrationInstance { InstanceId = entityInstanceId }, + EventName = "op", + EventData = eventData, + }; + + ProtoUtils.EntityConversionState entityConversionState = new(insertMissingEntityUnlocks: false); + + // Act + P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( + instanceId: "test-orchestration", + executionId: "exec-1", + customStatus: null, + actions: [sendEventAction], + completionToken: "token", + entityConversionState: entityConversionState, + orchestrationActivity: orchestrationActivity); + + // Assert + response.Actions.Should().ContainSingle(); + P.OrchestratorAction action = response.Actions[0]; + action.SendEntityMessage.Should().NotBeNull(); + action.SendEntityMessage.EntityOperationSignaled.Should().NotBeNull(); + action.SendEntityMessage.ParentTraceContext.Should().NotBeNull(); + action.SendEntityMessage.ParentTraceContext.TraceParent.Should().NotBeNullOrEmpty(); + action.SendEntityMessage.ParentTraceContext.TraceParent.Should().Contain( + orchestrationActivity!.TraceId.ToString()); + } + + [Fact] + public void SendEntityMessage_CallEntity_SetsParentTraceContext() + { + // Arrange + using ActivityListener listener = CreateListener(); + using Activity? orchestrationActivity = TestSource.StartActivity("TestOrchestration"); + orchestrationActivity.Should().NotBeNull(); + + string requestId = Guid.NewGuid().ToString(); + string entityInstanceId = "@counter@myKey"; + string eventData = JsonConvert.SerializeObject(new + { + op = "get", + signal = false, + id = requestId, + parent = "parent-instance", + }); + + SendEventOrchestratorAction sendEventAction = new() + { + Id = 1, + Instance = new OrchestrationInstance { InstanceId = entityInstanceId }, + EventName = "op", + EventData = eventData, + }; + + ProtoUtils.EntityConversionState entityConversionState = new(insertMissingEntityUnlocks: false); + + // Act + P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( + instanceId: "test-orchestration", + executionId: "exec-1", + customStatus: null, + actions: [sendEventAction], + completionToken: "token", + entityConversionState: entityConversionState, + orchestrationActivity: orchestrationActivity); + + // Assert + response.Actions.Should().ContainSingle(); + P.OrchestratorAction action = response.Actions[0]; + action.SendEntityMessage.Should().NotBeNull(); + action.SendEntityMessage.EntityOperationCalled.Should().NotBeNull(); + action.SendEntityMessage.ParentTraceContext.Should().NotBeNull(); + action.SendEntityMessage.ParentTraceContext.TraceParent.Should().NotBeNullOrEmpty(); + action.SendEntityMessage.ParentTraceContext.TraceParent.Should().Contain( + orchestrationActivity!.TraceId.ToString()); + } + + [Fact] + public void SendEntityMessage_NoOrchestrationActivity_DoesNotSetParentTraceContext() + { + // Arrange + string requestId = Guid.NewGuid().ToString(); + string entityInstanceId = "@counter@myKey"; + string eventData = JsonConvert.SerializeObject(new + { + op = "increment", + signal = true, + id = requestId, + }); + + SendEventOrchestratorAction sendEventAction = new() + { + Id = 1, + Instance = new OrchestrationInstance { InstanceId = entityInstanceId }, + EventName = "op", + EventData = eventData, + }; + + ProtoUtils.EntityConversionState entityConversionState = new(insertMissingEntityUnlocks: false); + + // Act + P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( + instanceId: "test-orchestration", + executionId: "exec-1", + customStatus: null, + actions: [sendEventAction], + completionToken: "token", + entityConversionState: entityConversionState, + orchestrationActivity: null); + + // Assert + response.Actions.Should().ContainSingle(); + P.OrchestratorAction action = response.Actions[0]; + action.SendEntityMessage.Should().NotBeNull(); + action.SendEntityMessage.ParentTraceContext.Should().BeNull(); + } + + [Fact] + public void SendEntityMessage_NoEntityConversionState_SendsAsSendEvent() + { + // Arrange + using ActivityListener listener = CreateListener(); + using Activity? orchestrationActivity = TestSource.StartActivity("TestOrchestration"); + + string requestId = Guid.NewGuid().ToString(); + string entityInstanceId = "@counter@myKey"; + string eventData = JsonConvert.SerializeObject(new + { + op = "increment", + signal = true, + id = requestId, + }); + + SendEventOrchestratorAction sendEventAction = new() + { + Id = 1, + Instance = new OrchestrationInstance { InstanceId = entityInstanceId }, + EventName = "op", + EventData = eventData, + }; + + // Act - no entityConversionState means entity events are NOT converted + P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( + instanceId: "test-orchestration", + executionId: "exec-1", + customStatus: null, + actions: [sendEventAction], + completionToken: "token", + entityConversionState: null, + orchestrationActivity: orchestrationActivity); + + // Assert - should be a SendEvent, not SendEntityMessage + response.Actions.Should().ContainSingle(); + P.OrchestratorAction action = response.Actions[0]; + action.SendEvent.Should().NotBeNull(); + action.SendEntityMessage.Should().BeNull(); + } + + [Fact] + public void SendEntityMessage_TraceContextHasUniqueSpanId() + { + // Arrange + using ActivityListener listener = CreateListener(); + using Activity? orchestrationActivity = TestSource.StartActivity("TestOrchestration"); + orchestrationActivity.Should().NotBeNull(); + + string entityInstanceId = "@counter@myKey"; + string eventData1 = JsonConvert.SerializeObject(new + { + op = "increment", + signal = true, + id = Guid.NewGuid().ToString(), + }); + + string eventData2 = JsonConvert.SerializeObject(new + { + op = "increment", + signal = true, + id = Guid.NewGuid().ToString(), + }); + + SendEventOrchestratorAction action1 = new() + { + Id = 1, + Instance = new OrchestrationInstance { InstanceId = entityInstanceId }, + EventName = "op", + EventData = eventData1, + }; + + SendEventOrchestratorAction action2 = new() + { + Id = 2, + Instance = new OrchestrationInstance { InstanceId = entityInstanceId }, + EventName = "op", + EventData = eventData2, + }; + + ProtoUtils.EntityConversionState entityConversionState = new(insertMissingEntityUnlocks: false); + + // Act + P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( + instanceId: "test-orchestration", + executionId: "exec-1", + customStatus: null, + actions: [action1, action2], + completionToken: "token", + entityConversionState: entityConversionState, + orchestrationActivity: orchestrationActivity); + + // Assert - each entity message should get a unique span ID + response.Actions.Should().HaveCount(2); + string traceParent1 = response.Actions[0].SendEntityMessage.ParentTraceContext.TraceParent; + string traceParent2 = response.Actions[1].SendEntityMessage.ParentTraceContext.TraceParent; + traceParent1.Should().NotBeNullOrEmpty(); + traceParent2.Should().NotBeNullOrEmpty(); + + // Same trace ID (from orchestration activity) + traceParent1.Should().Contain(orchestrationActivity!.TraceId.ToString()); + traceParent2.Should().Contain(orchestrationActivity.TraceId.ToString()); + + // Different span IDs + traceParent1.Should().NotBe(traceParent2); + } + + static ActivityListener CreateListener() + { + ActivityListener listener = new() + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + }; + + ActivitySource.AddActivityListener(listener); + return listener; + } +} From d2057e0199051d238695de9b701f99e8a2512ff8 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Sun, 1 Mar 2026 22:05:40 -0800 Subject: [PATCH 2/4] Extract trace context from entity operation events in ToEntityBatchRequest Add parentTraceContext fields to EntityOperationSignaledEvent and EntityOperationCalledEvent proto messages, and extract them in ToEntityBatchRequest to populate OperationRequest.TraceContext. This is the receiving side of the trace context propagation: once the DTS backend populates parentTraceContext on entity operation events (from SendEntityMessageAction.ParentTraceContext), the SDK will correctly extract and use the trace context for entity operations. --- src/Grpc/orchestrator_service.proto | 2 + src/Shared/Grpc/ProtoUtils.cs | 10 ++ .../Grpc.Tests/ProtoUtilsTraceContextTests.cs | 107 ++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 2ba4bc5b..cbcd648d 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -182,6 +182,7 @@ message EntityOperationSignaledEvent { google.protobuf.Timestamp scheduledTime = 3; google.protobuf.StringValue input = 4; google.protobuf.StringValue targetInstanceId = 5; // used only within histories, null in messages + TraceContext parentTraceContext = 6; } message EntityOperationCalledEvent { @@ -192,6 +193,7 @@ message EntityOperationCalledEvent { google.protobuf.StringValue parentInstanceId = 5; // used only within messages, null in histories google.protobuf.StringValue parentExecutionId = 6; // used only within messages, null in histories google.protobuf.StringValue targetInstanceId = 7; // used only within histories, null in messages + TraceContext parentTraceContext = 8; } message EntityLockRequestedEvent { diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index c7fe00f3..2e229d02 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -638,6 +638,11 @@ internal static void ToEntityBatchRequest( Id = Guid.Parse(op.EntityOperationSignaled.RequestId), Operation = op.EntityOperationSignaled.Operation, Input = op.EntityOperationSignaled.Input, + TraceContext = op.EntityOperationSignaled.ParentTraceContext != null + ? new DistributedTraceContext( + op.EntityOperationSignaled.ParentTraceContext.TraceParent, + op.EntityOperationSignaled.ParentTraceContext.TraceState) + : null, }); operationInfos.Add(new P.OperationInfo { @@ -652,6 +657,11 @@ internal static void ToEntityBatchRequest( Id = Guid.Parse(op.EntityOperationCalled.RequestId), Operation = op.EntityOperationCalled.Operation, Input = op.EntityOperationCalled.Input, + TraceContext = op.EntityOperationCalled.ParentTraceContext != null + ? new DistributedTraceContext( + op.EntityOperationCalled.ParentTraceContext.TraceParent, + op.EntityOperationCalled.ParentTraceContext.TraceState) + : null, }); operationInfos.Add(new P.OperationInfo { diff --git a/test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs b/test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs index a4c49ffa..d130c54a 100644 --- a/test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs +++ b/test/Worker/Grpc.Tests/ProtoUtilsTraceContextTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using DurableTask.Core; using DurableTask.Core.Command; +using DurableTask.Core.Entities.OperationFormat; using Newtonsoft.Json; using P = Microsoft.DurableTask.Protobuf; @@ -268,4 +269,110 @@ static ActivityListener CreateListener() ActivitySource.AddActivityListener(listener); return listener; } + + [Fact] + public void ToEntityBatchRequest_SignalEntity_ExtractsTraceContext() + { + // Arrange + string traceParent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"; + string traceState = "vendor=value"; + + P.EntityRequest entityRequest = new() + { + InstanceId = "@counter@myKey", + OperationRequests = + { + new P.HistoryEvent + { + EntityOperationSignaled = new P.EntityOperationSignaledEvent + { + RequestId = Guid.NewGuid().ToString(), + Operation = "increment", + ParentTraceContext = new P.TraceContext + { + TraceParent = traceParent, + TraceState = traceState, + }, + }, + }, + }, + }; + + // Act + entityRequest.ToEntityBatchRequest(out EntityBatchRequest batchRequest, out _); + + // Assert + batchRequest.Operations.Should().ContainSingle(); + batchRequest.Operations[0].TraceContext.Should().NotBeNull(); + batchRequest.Operations[0].TraceContext!.TraceParent.Should().Be(traceParent); + batchRequest.Operations[0].TraceContext!.TraceState.Should().Be(traceState); + } + + [Fact] + public void ToEntityBatchRequest_CallEntity_ExtractsTraceContext() + { + // Arrange + string traceParent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"; + string traceState = "vendor=value"; + + P.EntityRequest entityRequest = new() + { + InstanceId = "@counter@myKey", + OperationRequests = + { + new P.HistoryEvent + { + EntityOperationCalled = new P.EntityOperationCalledEvent + { + RequestId = Guid.NewGuid().ToString(), + Operation = "get", + ParentInstanceId = "parent-instance", + ParentExecutionId = "parent-exec", + ParentTraceContext = new P.TraceContext + { + TraceParent = traceParent, + TraceState = traceState, + }, + }, + }, + }, + }; + + // Act + entityRequest.ToEntityBatchRequest(out EntityBatchRequest batchRequest, out _); + + // Assert + batchRequest.Operations.Should().ContainSingle(); + batchRequest.Operations[0].TraceContext.Should().NotBeNull(); + batchRequest.Operations[0].TraceContext!.TraceParent.Should().Be(traceParent); + batchRequest.Operations[0].TraceContext!.TraceState.Should().Be(traceState); + } + + [Fact] + public void ToEntityBatchRequest_NoTraceContext_LeavesTraceContextNull() + { + // Arrange + P.EntityRequest entityRequest = new() + { + InstanceId = "@counter@myKey", + OperationRequests = + { + new P.HistoryEvent + { + EntityOperationSignaled = new P.EntityOperationSignaledEvent + { + RequestId = Guid.NewGuid().ToString(), + Operation = "increment", + }, + }, + }, + }; + + // Act + entityRequest.ToEntityBatchRequest(out EntityBatchRequest batchRequest, out _); + + // Assert + batchRequest.Operations.Should().ContainSingle(); + batchRequest.Operations[0].TraceContext.Should().BeNull(); + } } From 2df4cdd59adbcab677439677f4449ffc51c4d85c Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Tue, 3 Mar 2026 14:39:45 -0800 Subject: [PATCH 3/4] Fix unrelated character encoding issue --- src/Shared/Grpc/ProtoUtils.cs | 2506 ++++++++++++++++----------------- 1 file changed, 1253 insertions(+), 1253 deletions(-) diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index 2e229d02..bd0ccf2a 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -1,1072 +1,1072 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Buffers; -using System.Buffers.Text; -using System.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text; -using System.Text.Json; -using DurableTask.Core; -using DurableTask.Core.Command; -using DurableTask.Core.Entities; -using DurableTask.Core.Entities.OperationFormat; -using DurableTask.Core.History; -using DurableTask.Core.Tracing; -using Google.Protobuf; -using Google.Protobuf.Collections; -using Google.Protobuf.WellKnownTypes; -using DTCore = DurableTask.Core; -using P = Microsoft.DurableTask.Protobuf; -using TraceHelper = Microsoft.DurableTask.Tracing.TraceHelper; - -namespace Microsoft.DurableTask; - -/// -/// Protobuf utilities and helpers. -/// -static class ProtoUtils -{ - /// - /// Converts a history event from to . - /// - /// The proto history event to converter. - /// The converted history event. - /// When the provided history event type is not supported. - internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto) - { - return ConvertHistoryEvent(proto, conversionState: null); - } - - /// - /// Converts a history event from to , and performs - /// stateful conversions of entity-related events. - /// - /// The proto history event to converter. - /// State needed for converting entity-related history entries and actions. - /// The converted history event. - /// When the provided history event type is not supported. - internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto, EntityConversionState? conversionState) - { - Check.NotNull(proto); - HistoryEvent historyEvent; - switch (proto.EventTypeCase) - { - case P.HistoryEvent.EventTypeOneofCase.ContinueAsNew: - historyEvent = new ContinueAsNewEvent(proto.EventId, proto.ContinueAsNew.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionStarted: - OrchestrationInstance instance = proto.ExecutionStarted.OrchestrationInstance.ToCore(); - conversionState?.SetOrchestrationInstance(instance); - historyEvent = new ExecutionStartedEvent(proto.EventId, proto.ExecutionStarted.Input) - { - Name = proto.ExecutionStarted.Name, - Version = proto.ExecutionStarted.Version, - OrchestrationInstance = instance, - Tags = proto.ExecutionStarted.Tags, - ParentInstance = proto.ExecutionStarted.ParentInstance == null ? null : new ParentInstance - { - Name = proto.ExecutionStarted.ParentInstance.Name, - Version = proto.ExecutionStarted.ParentInstance.Version, - OrchestrationInstance = proto.ExecutionStarted.ParentInstance.OrchestrationInstance.ToCore(), - TaskScheduleId = proto.ExecutionStarted.ParentInstance.TaskScheduledId, - }, - ScheduledStartTime = proto.ExecutionStarted.ScheduledStartTimestamp?.ToDateTime(), - }; - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionCompleted: - historyEvent = new ExecutionCompletedEvent( - proto.EventId, - proto.ExecutionCompleted.Result, +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Buffers; +using System.Buffers.Text; +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using System.Text.Json; +using DurableTask.Core; +using DurableTask.Core.Command; +using DurableTask.Core.Entities; +using DurableTask.Core.Entities.OperationFormat; +using DurableTask.Core.History; +using DurableTask.Core.Tracing; +using Google.Protobuf; +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; +using DTCore = DurableTask.Core; +using P = Microsoft.DurableTask.Protobuf; +using TraceHelper = Microsoft.DurableTask.Tracing.TraceHelper; + +namespace Microsoft.DurableTask; + +/// +/// Protobuf utilities and helpers. +/// +static class ProtoUtils +{ + /// + /// Converts a history event from to . + /// + /// The proto history event to converter. + /// The converted history event. + /// When the provided history event type is not supported. + internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto) + { + return ConvertHistoryEvent(proto, conversionState: null); + } + + /// + /// Converts a history event from to , and performs + /// stateful conversions of entity-related events. + /// + /// The proto history event to converter. + /// State needed for converting entity-related history entries and actions. + /// The converted history event. + /// When the provided history event type is not supported. + internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto, EntityConversionState? conversionState) + { + Check.NotNull(proto); + HistoryEvent historyEvent; + switch (proto.EventTypeCase) + { + case P.HistoryEvent.EventTypeOneofCase.ContinueAsNew: + historyEvent = new ContinueAsNewEvent(proto.EventId, proto.ContinueAsNew.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionStarted: + OrchestrationInstance instance = proto.ExecutionStarted.OrchestrationInstance.ToCore(); + conversionState?.SetOrchestrationInstance(instance); + historyEvent = new ExecutionStartedEvent(proto.EventId, proto.ExecutionStarted.Input) + { + Name = proto.ExecutionStarted.Name, + Version = proto.ExecutionStarted.Version, + OrchestrationInstance = instance, + Tags = proto.ExecutionStarted.Tags, + ParentInstance = proto.ExecutionStarted.ParentInstance == null ? null : new ParentInstance + { + Name = proto.ExecutionStarted.ParentInstance.Name, + Version = proto.ExecutionStarted.ParentInstance.Version, + OrchestrationInstance = proto.ExecutionStarted.ParentInstance.OrchestrationInstance.ToCore(), + TaskScheduleId = proto.ExecutionStarted.ParentInstance.TaskScheduledId, + }, + ScheduledStartTime = proto.ExecutionStarted.ScheduledStartTimestamp?.ToDateTime(), + }; + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionCompleted: + historyEvent = new ExecutionCompletedEvent( + proto.EventId, + proto.ExecutionCompleted.Result, proto.ExecutionCompleted.OrchestrationStatus.ToCore(), - proto.ExecutionCompleted.FailureDetails.ToCore()); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionTerminated: - historyEvent = new ExecutionTerminatedEvent(proto.EventId, proto.ExecutionTerminated.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionSuspended: - historyEvent = new ExecutionSuspendedEvent(proto.EventId, proto.ExecutionSuspended.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionResumed: - historyEvent = new ExecutionResumedEvent(proto.EventId, proto.ExecutionResumed.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.TaskScheduled: - historyEvent = new TaskScheduledEvent( - proto.EventId, - proto.TaskScheduled.Name, - proto.TaskScheduled.Version, - proto.TaskScheduled.Input) - { - Tags = proto.TaskScheduled.Tags, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.TaskCompleted: - historyEvent = new TaskCompletedEvent( - proto.EventId, - proto.TaskCompleted.TaskScheduledId, - proto.TaskCompleted.Result); - break; - case P.HistoryEvent.EventTypeOneofCase.TaskFailed: - historyEvent = new TaskFailedEvent( - proto.EventId, - proto.TaskFailed.TaskScheduledId, - reason: null, /* not supported */ - details: null, /* not supported */ - proto.TaskFailed.FailureDetails.ToCore()); - break; - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated: - historyEvent = new SubOrchestrationInstanceCreatedEvent(proto.EventId) - { - Input = proto.SubOrchestrationInstanceCreated.Input, - InstanceId = proto.SubOrchestrationInstanceCreated.InstanceId, - Name = proto.SubOrchestrationInstanceCreated.Name, - Version = proto.SubOrchestrationInstanceCreated.Version, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted: - historyEvent = new SubOrchestrationInstanceCompletedEvent( - proto.EventId, - proto.SubOrchestrationInstanceCompleted.TaskScheduledId, - proto.SubOrchestrationInstanceCompleted.Result); - break; - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed: - historyEvent = new SubOrchestrationInstanceFailedEvent( - proto.EventId, - proto.SubOrchestrationInstanceFailed.TaskScheduledId, - reason: null /* not supported */, - details: null /* not supported */, - proto.SubOrchestrationInstanceFailed.FailureDetails.ToCore()); - break; - case P.HistoryEvent.EventTypeOneofCase.TimerCreated: - historyEvent = new TimerCreatedEvent( - proto.EventId, - proto.TimerCreated.FireAt.ToDateTime()); - break; - case P.HistoryEvent.EventTypeOneofCase.TimerFired: - historyEvent = new TimerFiredEvent( - eventId: -1, - proto.TimerFired.FireAt.ToDateTime()) - { - TimerId = proto.TimerFired.TimerId, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.OrchestratorStarted: - historyEvent = new OrchestratorStartedEvent(proto.EventId); - break; - case P.HistoryEvent.EventTypeOneofCase.OrchestratorCompleted: - historyEvent = new OrchestratorCompletedEvent(proto.EventId); - break; - case P.HistoryEvent.EventTypeOneofCase.EventSent: - historyEvent = new EventSentEvent(proto.EventId) - { - InstanceId = proto.EventSent.InstanceId, - Name = proto.EventSent.Name, - Input = proto.EventSent.Input, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.EventRaised: - historyEvent = new EventRaisedEvent(proto.EventId, proto.EventRaised.Input) - { - Name = proto.EventRaised.Name, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationCalled: - historyEvent = EntityConversions.EncodeOperationCalled(proto, conversionState!.CurrentInstance); - conversionState?.EntityRequestIds.Add(proto.EntityOperationCalled.RequestId); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationSignaled: - historyEvent = EntityConversions.EncodeOperationSignaled(proto); - conversionState?.EntityRequestIds.Add(proto.EntityOperationSignaled.RequestId); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityLockRequested: - historyEvent = EntityConversions.EncodeLockRequested(proto, conversionState!.CurrentInstance); - conversionState?.AddUnlockObligations(proto.EntityLockRequested); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityUnlockSent: - historyEvent = EntityConversions.EncodeUnlockSent(proto, conversionState!.CurrentInstance); - conversionState?.RemoveUnlockObligation(proto.EntityUnlockSent.TargetInstanceId); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityLockGranted: - historyEvent = EntityConversions.EncodeLockGranted(proto); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationCompleted: - historyEvent = EntityConversions.EncodeOperationCompleted(proto); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationFailed: - historyEvent = EntityConversions.EncodeOperationFailed(proto); - break; - case P.HistoryEvent.EventTypeOneofCase.GenericEvent: - historyEvent = new GenericEvent(proto.EventId, proto.GenericEvent.Data); - break; - case P.HistoryEvent.EventTypeOneofCase.HistoryState: - historyEvent = new HistoryStateEvent( - proto.EventId, - new OrchestrationState - { - OrchestrationInstance = new OrchestrationInstance - { - InstanceId = proto.HistoryState.OrchestrationState.InstanceId, - }, - Name = proto.HistoryState.OrchestrationState.Name, - Version = proto.HistoryState.OrchestrationState.Version, - ScheduledStartTime = proto.HistoryState.OrchestrationState.ScheduledStartTimestamp.ToDateTime(), - CreatedTime = proto.HistoryState.OrchestrationState.CreatedTimestamp.ToDateTime(), - LastUpdatedTime = proto.HistoryState.OrchestrationState.LastUpdatedTimestamp.ToDateTime(), - Input = proto.HistoryState.OrchestrationState.Input, - Output = proto.HistoryState.OrchestrationState.Output, - Status = proto.HistoryState.OrchestrationState.CustomStatus, - Tags = proto.HistoryState.OrchestrationState.Tags, - }); - break; + proto.ExecutionCompleted.FailureDetails.ToCore()); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionTerminated: + historyEvent = new ExecutionTerminatedEvent(proto.EventId, proto.ExecutionTerminated.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionSuspended: + historyEvent = new ExecutionSuspendedEvent(proto.EventId, proto.ExecutionSuspended.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionResumed: + historyEvent = new ExecutionResumedEvent(proto.EventId, proto.ExecutionResumed.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.TaskScheduled: + historyEvent = new TaskScheduledEvent( + proto.EventId, + proto.TaskScheduled.Name, + proto.TaskScheduled.Version, + proto.TaskScheduled.Input) + { + Tags = proto.TaskScheduled.Tags, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.TaskCompleted: + historyEvent = new TaskCompletedEvent( + proto.EventId, + proto.TaskCompleted.TaskScheduledId, + proto.TaskCompleted.Result); + break; + case P.HistoryEvent.EventTypeOneofCase.TaskFailed: + historyEvent = new TaskFailedEvent( + proto.EventId, + proto.TaskFailed.TaskScheduledId, + reason: null, /* not supported */ + details: null, /* not supported */ + proto.TaskFailed.FailureDetails.ToCore()); + break; + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated: + historyEvent = new SubOrchestrationInstanceCreatedEvent(proto.EventId) + { + Input = proto.SubOrchestrationInstanceCreated.Input, + InstanceId = proto.SubOrchestrationInstanceCreated.InstanceId, + Name = proto.SubOrchestrationInstanceCreated.Name, + Version = proto.SubOrchestrationInstanceCreated.Version, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted: + historyEvent = new SubOrchestrationInstanceCompletedEvent( + proto.EventId, + proto.SubOrchestrationInstanceCompleted.TaskScheduledId, + proto.SubOrchestrationInstanceCompleted.Result); + break; + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed: + historyEvent = new SubOrchestrationInstanceFailedEvent( + proto.EventId, + proto.SubOrchestrationInstanceFailed.TaskScheduledId, + reason: null /* not supported */, + details: null /* not supported */, + proto.SubOrchestrationInstanceFailed.FailureDetails.ToCore()); + break; + case P.HistoryEvent.EventTypeOneofCase.TimerCreated: + historyEvent = new TimerCreatedEvent( + proto.EventId, + proto.TimerCreated.FireAt.ToDateTime()); + break; + case P.HistoryEvent.EventTypeOneofCase.TimerFired: + historyEvent = new TimerFiredEvent( + eventId: -1, + proto.TimerFired.FireAt.ToDateTime()) + { + TimerId = proto.TimerFired.TimerId, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.OrchestratorStarted: + historyEvent = new OrchestratorStartedEvent(proto.EventId); + break; + case P.HistoryEvent.EventTypeOneofCase.OrchestratorCompleted: + historyEvent = new OrchestratorCompletedEvent(proto.EventId); + break; + case P.HistoryEvent.EventTypeOneofCase.EventSent: + historyEvent = new EventSentEvent(proto.EventId) + { + InstanceId = proto.EventSent.InstanceId, + Name = proto.EventSent.Name, + Input = proto.EventSent.Input, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.EventRaised: + historyEvent = new EventRaisedEvent(proto.EventId, proto.EventRaised.Input) + { + Name = proto.EventRaised.Name, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationCalled: + historyEvent = EntityConversions.EncodeOperationCalled(proto, conversionState!.CurrentInstance); + conversionState?.EntityRequestIds.Add(proto.EntityOperationCalled.RequestId); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationSignaled: + historyEvent = EntityConversions.EncodeOperationSignaled(proto); + conversionState?.EntityRequestIds.Add(proto.EntityOperationSignaled.RequestId); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityLockRequested: + historyEvent = EntityConversions.EncodeLockRequested(proto, conversionState!.CurrentInstance); + conversionState?.AddUnlockObligations(proto.EntityLockRequested); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityUnlockSent: + historyEvent = EntityConversions.EncodeUnlockSent(proto, conversionState!.CurrentInstance); + conversionState?.RemoveUnlockObligation(proto.EntityUnlockSent.TargetInstanceId); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityLockGranted: + historyEvent = EntityConversions.EncodeLockGranted(proto); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationCompleted: + historyEvent = EntityConversions.EncodeOperationCompleted(proto); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationFailed: + historyEvent = EntityConversions.EncodeOperationFailed(proto); + break; + case P.HistoryEvent.EventTypeOneofCase.GenericEvent: + historyEvent = new GenericEvent(proto.EventId, proto.GenericEvent.Data); + break; + case P.HistoryEvent.EventTypeOneofCase.HistoryState: + historyEvent = new HistoryStateEvent( + proto.EventId, + new OrchestrationState + { + OrchestrationInstance = new OrchestrationInstance + { + InstanceId = proto.HistoryState.OrchestrationState.InstanceId, + }, + Name = proto.HistoryState.OrchestrationState.Name, + Version = proto.HistoryState.OrchestrationState.Version, + ScheduledStartTime = proto.HistoryState.OrchestrationState.ScheduledStartTimestamp.ToDateTime(), + CreatedTime = proto.HistoryState.OrchestrationState.CreatedTimestamp.ToDateTime(), + LastUpdatedTime = proto.HistoryState.OrchestrationState.LastUpdatedTimestamp.ToDateTime(), + Input = proto.HistoryState.OrchestrationState.Input, + Output = proto.HistoryState.OrchestrationState.Output, + Status = proto.HistoryState.OrchestrationState.CustomStatus, + Tags = proto.HistoryState.OrchestrationState.Tags, + }); + break; case P.HistoryEvent.EventTypeOneofCase.ExecutionRewound: historyEvent = new ExecutionRewoundEvent(proto.EventId); break; - default: - throw new NotSupportedException($"Deserialization of {proto.EventTypeCase} is not supported."); - } - - historyEvent.Timestamp = proto.Timestamp.ToDateTime(); - return historyEvent; - } - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp ToTimestamp(this DateTime dateTime) - { - // The protobuf libraries require timestamps to be in UTC - if (dateTime.Kind == DateTimeKind.Unspecified) - { - dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); - } - else if (dateTime.Kind == DateTimeKind.Local) - { - dateTime = dateTime.ToUniversalTime(); - } - - return Timestamp.FromDateTime(dateTime); - } - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp? ToTimestamp(this DateTime? dateTime) - => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp ToTimestamp(this DateTimeOffset dateTime) => Timestamp.FromDateTimeOffset(dateTime); - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp? ToTimestamp(this DateTimeOffset? dateTime) - => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; - - /// - /// Constructs a . - /// - /// The orchestrator instance ID. - /// The orchestrator execution ID. - /// The orchestrator customer status or null if no custom status. - /// The orchestrator actions. - /// - /// The completion token for the work item. It must be the exact same - /// value that was provided by the corresponding that triggered the orchestrator execution. - /// - /// The entity conversion state, or null if no conversion is required. - /// The that represents orchestration execution. - /// Whether or not a history is required to complete the orchestration request and none was provided. - /// The orchestrator response. - /// When an orchestrator action is unknown. - internal static P.OrchestratorResponse ConstructOrchestratorResponse( - string instanceId, - string executionId, - string? customStatus, - IEnumerable? actions, - string completionToken, - EntityConversionState? entityConversionState, - Activity? orchestrationActivity, - bool requiresHistory = false) - { - var response = new P.OrchestratorResponse - { - InstanceId = instanceId, - CustomStatus = customStatus, - CompletionToken = completionToken, - OrchestrationTraceContext = - new() - { - SpanID = orchestrationActivity?.SpanId.ToString(), - SpanStartTime = orchestrationActivity?.StartTimeUtc.ToTimestamp(), - }, - RequiresHistory = requiresHistory, - }; - - // If a history is required and the orchestration request was not completed, then there is no list of actions. - if (requiresHistory) - { - return response; - } - - Check.NotNull(actions); - foreach (OrchestratorAction action in actions) - { - var protoAction = new P.OrchestratorAction { Id = action.Id }; - - P.TraceContext? CreateTraceContext() - { - if (orchestrationActivity is null) - { - return null; - } - - ActivitySpanId clientSpanId = ActivitySpanId.CreateRandom(); - ActivityContext clientActivityContext = new(orchestrationActivity.TraceId, clientSpanId, orchestrationActivity.ActivityTraceFlags, orchestrationActivity.TraceStateString); - - return new P.TraceContext - { - TraceParent = $"00-{clientActivityContext.TraceId}-{clientActivityContext.SpanId}-0{clientActivityContext.TraceFlags:d}", - TraceState = clientActivityContext.TraceState, - }; - } - - switch (action.OrchestratorActionType) - { - case OrchestratorActionType.ScheduleOrchestrator: - var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; - - protoAction.ScheduleTask = new P.ScheduleTaskAction - { - Name = scheduleTaskAction.Name, - Version = scheduleTaskAction.Version, - Input = scheduleTaskAction.Input, - ParentTraceContext = CreateTraceContext(), - }; - - if (scheduleTaskAction.Tags != null) - { - foreach (KeyValuePair tag in scheduleTaskAction.Tags) - { - protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; - } - } - - break; - case OrchestratorActionType.CreateSubOrchestration: - var subOrchestrationAction = (CreateSubOrchestrationAction)action; - protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction - { - Input = subOrchestrationAction.Input, - InstanceId = subOrchestrationAction.InstanceId, - Name = subOrchestrationAction.Name, - Version = subOrchestrationAction.Version, - ParentTraceContext = CreateTraceContext(), + default: + throw new NotSupportedException($"Deserialization of {proto.EventTypeCase} is not supported."); + } + + historyEvent.Timestamp = proto.Timestamp.ToDateTime(); + return historyEvent; + } + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp ToTimestamp(this DateTime dateTime) + { + // The protobuf libraries require timestamps to be in UTC + if (dateTime.Kind == DateTimeKind.Unspecified) + { + dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + } + else if (dateTime.Kind == DateTimeKind.Local) + { + dateTime = dateTime.ToUniversalTime(); + } + + return Timestamp.FromDateTime(dateTime); + } + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp? ToTimestamp(this DateTime? dateTime) + => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp ToTimestamp(this DateTimeOffset dateTime) => Timestamp.FromDateTimeOffset(dateTime); + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp? ToTimestamp(this DateTimeOffset? dateTime) + => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; + + /// + /// Constructs a . + /// + /// The orchestrator instance ID. + /// The orchestrator execution ID. + /// The orchestrator customer status or null if no custom status. + /// The orchestrator actions. + /// + /// The completion token for the work item. It must be the exact same + /// value that was provided by the corresponding that triggered the orchestrator execution. + /// + /// The entity conversion state, or null if no conversion is required. + /// The that represents orchestration execution. + /// Whether or not a history is required to complete the orchestration request and none was provided. + /// The orchestrator response. + /// When an orchestrator action is unknown. + internal static P.OrchestratorResponse ConstructOrchestratorResponse( + string instanceId, + string executionId, + string? customStatus, + IEnumerable? actions, + string completionToken, + EntityConversionState? entityConversionState, + Activity? orchestrationActivity, + bool requiresHistory = false) + { + var response = new P.OrchestratorResponse + { + InstanceId = instanceId, + CustomStatus = customStatus, + CompletionToken = completionToken, + OrchestrationTraceContext = + new() + { + SpanID = orchestrationActivity?.SpanId.ToString(), + SpanStartTime = orchestrationActivity?.StartTimeUtc.ToTimestamp(), + }, + RequiresHistory = requiresHistory, + }; + + // If a history is required and the orchestration request was not completed, then there is no list of actions. + if (requiresHistory) + { + return response; + } + + Check.NotNull(actions); + foreach (OrchestratorAction action in actions) + { + var protoAction = new P.OrchestratorAction { Id = action.Id }; + + P.TraceContext? CreateTraceContext() + { + if (orchestrationActivity is null) + { + return null; + } + + ActivitySpanId clientSpanId = ActivitySpanId.CreateRandom(); + ActivityContext clientActivityContext = new(orchestrationActivity.TraceId, clientSpanId, orchestrationActivity.ActivityTraceFlags, orchestrationActivity.TraceStateString); + + return new P.TraceContext + { + TraceParent = $"00-{clientActivityContext.TraceId}-{clientActivityContext.SpanId}-0{clientActivityContext.TraceFlags:d}", + TraceState = clientActivityContext.TraceState, + }; + } + + switch (action.OrchestratorActionType) + { + case OrchestratorActionType.ScheduleOrchestrator: + var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; + + protoAction.ScheduleTask = new P.ScheduleTaskAction + { + Name = scheduleTaskAction.Name, + Version = scheduleTaskAction.Version, + Input = scheduleTaskAction.Input, + ParentTraceContext = CreateTraceContext(), }; - if (subOrchestrationAction.Tags != null) - { - foreach (KeyValuePair tag in subOrchestrationAction.Tags) - { - protoAction.CreateSubOrchestration.Tags[tag.Key] = tag.Value; - } + if (scheduleTaskAction.Tags != null) + { + foreach (KeyValuePair tag in scheduleTaskAction.Tags) + { + protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; + } } - - break; - case OrchestratorActionType.CreateTimer: - var createTimerAction = (CreateTimerOrchestratorAction)action; - protoAction.CreateTimer = new P.CreateTimerAction - { - FireAt = createTimerAction.FireAt.ToTimestamp(), - }; - break; - case OrchestratorActionType.SendEvent: - var sendEventAction = (SendEventOrchestratorAction)action; - if (sendEventAction.Instance == null) - { - throw new ArgumentException( - $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); - } - - if (entityConversionState is not null - && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) - && sendEventAction.EventName is not null - && sendEventAction.EventData is not null) - { - P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); - protoAction.SendEntityMessage = sendAction; - - EntityConversions.DecodeEntityMessageAction( - sendEventAction.EventName, - sendEventAction.EventData, - sendEventAction.Instance.InstanceId, - sendAction, - out string requestId); - - sendAction.ParentTraceContext = CreateTraceContext(); - - entityConversionState.EntityRequestIds.Add(requestId); - - switch (sendAction.EntityMessageTypeCase) - { - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: - entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); - break; - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: - entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); - break; - default: - break; - } - } - else - { - protoAction.SendEvent = new P.SendEventAction - { - Instance = sendEventAction.Instance.ToProtobuf(), - Name = sendEventAction.EventName, - Data = sendEventAction.EventData, - }; - - // Distributed Tracing: start a new trace activity derived from the orchestration - // for an EventRaisedEvent (external event) - using Activity? traceActivity = TraceHelper.StartTraceActivityForEventRaisedFromWorker(sendEventAction, instanceId, executionId); - - traceActivity?.Stop(); - } - - break; - case OrchestratorActionType.OrchestrationComplete: - - if (entityConversionState is not null) - { - // as a precaution, unlock any entities that were not unlocked for some reason, before - // completing the orchestration. - foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) - { - response.Actions.Add(new P.OrchestratorAction - { - Id = action.Id, - SendEntityMessage = new P.SendEntityMessageAction - { - EntityUnlockSent = new P.EntityUnlockSentEvent - { - CriticalSectionId = criticalSectionId, - TargetInstanceId = target, - ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, - }, - }, - }); - } - } - - var completeAction = (OrchestrationCompleteOrchestratorAction)action; - protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction + + break; + case OrchestratorActionType.CreateSubOrchestration: + var subOrchestrationAction = (CreateSubOrchestrationAction)action; + protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction { - CarryoverEvents = { completeAction.CarryoverEvents.Select(ToProtobuf) }, - Details = completeAction.Details, - NewVersion = completeAction.NewVersion, - OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), - Result = completeAction.Result, + Input = subOrchestrationAction.Input, + InstanceId = subOrchestrationAction.InstanceId, + Name = subOrchestrationAction.Name, + Version = subOrchestrationAction.Version, + ParentTraceContext = CreateTraceContext(), + }; + + if (subOrchestrationAction.Tags != null) + { + foreach (KeyValuePair tag in subOrchestrationAction.Tags) + { + protoAction.CreateSubOrchestration.Tags[tag.Key] = tag.Value; + } + } + + break; + case OrchestratorActionType.CreateTimer: + var createTimerAction = (CreateTimerOrchestratorAction)action; + protoAction.CreateTimer = new P.CreateTimerAction + { + FireAt = createTimerAction.FireAt.ToTimestamp(), + }; + break; + case OrchestratorActionType.SendEvent: + var sendEventAction = (SendEventOrchestratorAction)action; + if (sendEventAction.Instance == null) + { + throw new ArgumentException( + $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); + } + + if (entityConversionState is not null + && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) + && sendEventAction.EventName is not null + && sendEventAction.EventData is not null) + { + P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); + protoAction.SendEntityMessage = sendAction; + + EntityConversions.DecodeEntityMessageAction( + sendEventAction.EventName, + sendEventAction.EventData, + sendEventAction.Instance.InstanceId, + sendAction, + out string requestId); + + sendAction.ParentTraceContext = CreateTraceContext(); + + entityConversionState.EntityRequestIds.Add(requestId); + + switch (sendAction.EntityMessageTypeCase) + { + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: + entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); + break; + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: + entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); + break; + default: + break; + } + } + else + { + protoAction.SendEvent = new P.SendEventAction + { + Instance = sendEventAction.Instance.ToProtobuf(), + Name = sendEventAction.EventName, + Data = sendEventAction.EventData, + }; + + // Distributed Tracing: start a new trace activity derived from the orchestration + // for an EventRaisedEvent (external event) + using Activity? traceActivity = TraceHelper.StartTraceActivityForEventRaisedFromWorker(sendEventAction, instanceId, executionId); + + traceActivity?.Stop(); + } + + break; + case OrchestratorActionType.OrchestrationComplete: + + if (entityConversionState is not null) + { + // as a precaution, unlock any entities that were not unlocked for some reason, before + // completing the orchestration. + foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) + { + response.Actions.Add(new P.OrchestratorAction + { + Id = action.Id, + SendEntityMessage = new P.SendEntityMessageAction + { + EntityUnlockSent = new P.EntityUnlockSentEvent + { + CriticalSectionId = criticalSectionId, + TargetInstanceId = target, + ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, + }, + }, + }); + } + } + + var completeAction = (OrchestrationCompleteOrchestratorAction)action; + protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction + { + CarryoverEvents = { completeAction.CarryoverEvents.Select(ToProtobuf) }, + Details = completeAction.Details, + NewVersion = completeAction.NewVersion, + OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), + Result = completeAction.Result, }; foreach (KeyValuePair tag in completeAction.Tags) { protoAction.CompleteOrchestration.Tags[tag.Key] = tag.Value; - } - - if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) - { - protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); - } - - break; - default: - throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); - } - - response.Actions.Add(protoAction); - } - - return response; - } - - /// - /// Converts a to a . - /// - /// The status to convert. - /// The converted status. - internal static OrchestrationStatus ToCore(this P.OrchestrationStatus status) - { - return (OrchestrationStatus)status; - } - - /// - /// Converts a to a . - /// - /// The status to convert. - /// The converted status. - [return: NotNullIfNotNull(nameof(status))] - internal static OrchestrationInstance? ToCore(this P.OrchestrationInstance? status) - { - if (status == null) - { - return null; - } - - return new OrchestrationInstance - { - InstanceId = status.InstanceId, - ExecutionId = status.ExecutionId, - }; - } - - /// - /// Converts a to a . - /// - /// The failure details to convert. - /// The converted failure details. - [return: NotNullIfNotNull(nameof(failureDetails))] - internal static TaskFailureDetails? ToTaskFailureDetails(this P.TaskFailureDetails? failureDetails) - { - if (failureDetails == null) - { - return null; - } - - return new TaskFailureDetails( - failureDetails.ErrorType, - failureDetails.ErrorMessage, - failureDetails.StackTrace, - failureDetails.InnerFailure.ToTaskFailureDetails(), - ConvertProperties(failureDetails.Properties)); - } - - /// - /// Converts a to . - /// - /// The exception to convert. - /// Optional exception properties provider. - /// The task failure details. - [return: NotNullIfNotNull(nameof(e))] - internal static P.TaskFailureDetails? ToTaskFailureDetails(this Exception? e, DTCore.IExceptionPropertiesProvider? exceptionPropertiesProvider = null) - { - if (e == null) - { - return null; - } - - IDictionary? properties = exceptionPropertiesProvider?.GetExceptionProperties(e); - - var taskFailureDetails = new P.TaskFailureDetails - { - ErrorType = e.GetType().FullName, - ErrorMessage = e.Message, - StackTrace = e.StackTrace, - InnerFailure = e.InnerException.ToTaskFailureDetails(exceptionPropertiesProvider), - }; - - if (properties != null) - { - foreach (var kvp in properties) - { - taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); - } - } - - return taskFailureDetails; - } - - /// - /// Converts a to a . - /// - /// The entity batch request to convert. - /// The converted entity batch request. - [return: NotNullIfNotNull(nameof(entityBatchRequest))] - internal static EntityBatchRequest? ToEntityBatchRequest(this P.EntityBatchRequest? entityBatchRequest) - { - if (entityBatchRequest == null) - { - return null; - } - - return new EntityBatchRequest() - { - EntityState = entityBatchRequest.EntityState, - InstanceId = entityBatchRequest.InstanceId, - Operations = entityBatchRequest.Operations.Select(r => r.ToOperationRequest()).ToList(), - }; - } - - /// - /// Converts a to a . - /// - /// The entity request to convert. - /// The converted request. - /// Additional info about each operation, required by DTS. - internal static void ToEntityBatchRequest( - this P.EntityRequest entityRequest, - out EntityBatchRequest batchRequest, - out List operationInfos) - { - batchRequest = new EntityBatchRequest() - { - EntityState = entityRequest.EntityState, - InstanceId = entityRequest.InstanceId, - Operations = [], // operations are added to this collection below - }; - - operationInfos = new(entityRequest.OperationRequests.Count); - - foreach (P.HistoryEvent? op in entityRequest.OperationRequests) - { - if (op.EntityOperationSignaled is not null) - { - batchRequest.Operations.Add(new OperationRequest - { - Id = Guid.Parse(op.EntityOperationSignaled.RequestId), - Operation = op.EntityOperationSignaled.Operation, - Input = op.EntityOperationSignaled.Input, - TraceContext = op.EntityOperationSignaled.ParentTraceContext != null - ? new DistributedTraceContext( - op.EntityOperationSignaled.ParentTraceContext.TraceParent, - op.EntityOperationSignaled.ParentTraceContext.TraceState) - : null, - }); - operationInfos.Add(new P.OperationInfo - { - RequestId = op.EntityOperationSignaled.RequestId, - ResponseDestination = null, // means we don't send back a response to the caller - }); - } - else if (op.EntityOperationCalled is not null) - { - batchRequest.Operations.Add(new OperationRequest - { - Id = Guid.Parse(op.EntityOperationCalled.RequestId), - Operation = op.EntityOperationCalled.Operation, - Input = op.EntityOperationCalled.Input, - TraceContext = op.EntityOperationCalled.ParentTraceContext != null - ? new DistributedTraceContext( - op.EntityOperationCalled.ParentTraceContext.TraceParent, - op.EntityOperationCalled.ParentTraceContext.TraceState) - : null, - }); - operationInfos.Add(new P.OperationInfo - { - RequestId = op.EntityOperationCalled.RequestId, - ResponseDestination = new P.OrchestrationInstance - { - InstanceId = op.EntityOperationCalled.ParentInstanceId, - ExecutionId = op.EntityOperationCalled.ParentExecutionId, - }, - }); - } - } - } - - /// - /// Converts a to a . - /// - /// The operation request to convert. - /// The converted operation request. - [return: NotNullIfNotNull(nameof(operationRequest))] - internal static OperationRequest? ToOperationRequest(this P.OperationRequest? operationRequest) - { - if (operationRequest == null) - { - return null; - } - - return new OperationRequest() - { - Operation = operationRequest.Operation, - Input = operationRequest.Input, - Id = Guid.Parse(operationRequest.RequestId), - TraceContext = operationRequest.TraceContext != null ? - new DistributedTraceContext( - operationRequest.TraceContext.TraceParent, - operationRequest.TraceContext.TraceState) : null, - }; - } - - /// - /// Converts a to a . - /// - /// The operation result to convert. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(operationResult))] - internal static OperationResult? ToOperationResult(this P.OperationResult? operationResult) - { - if (operationResult == null) - { - return null; - } - - switch (operationResult.ResultTypeCase) - { - case P.OperationResult.ResultTypeOneofCase.Success: - return new OperationResult() - { - Result = operationResult.Success.Result, - StartTimeUtc = operationResult.Success.StartTimeUtc?.ToDateTime(), - EndTimeUtc = operationResult.Success.EndTimeUtc?.ToDateTime(), - }; - - case P.OperationResult.ResultTypeOneofCase.Failure: - return new OperationResult() - { - FailureDetails = operationResult.Failure.FailureDetails.ToCore(), - StartTimeUtc = operationResult.Failure.StartTimeUtc?.ToDateTime(), - EndTimeUtc = operationResult.Failure.EndTimeUtc?.ToDateTime(), - }; - - default: - throw new NotSupportedException($"Deserialization of {operationResult.ResultTypeCase} is not supported."); - } - } - - /// - /// Converts a to . - /// - /// The operation result to convert. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(operationResult))] - internal static P.OperationResult? ToOperationResult(this OperationResult? operationResult) - { - if (operationResult == null) - { - return null; - } - - if (operationResult.FailureDetails == null) - { - return new P.OperationResult() - { - Success = new P.OperationResultSuccess() - { - Result = operationResult.Result, - StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), - EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), - }, - }; - } - else - { - return new P.OperationResult() - { - Failure = new P.OperationResultFailure() - { - FailureDetails = ToProtobuf(operationResult.FailureDetails), - StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), - EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), - }, - }; - } - } - - /// - /// Converts a to a . - /// - /// The operation action to convert. - /// The converted operation action. - [return: NotNullIfNotNull(nameof(operationAction))] - internal static OperationAction? ToOperationAction(this P.OperationAction? operationAction) - { - if (operationAction == null) - { - return null; - } - - switch (operationAction.OperationActionTypeCase) - { - case P.OperationAction.OperationActionTypeOneofCase.SendSignal: - - return new SendSignalOperationAction() - { - Name = operationAction.SendSignal.Name, - Input = operationAction.SendSignal.Input, - InstanceId = operationAction.SendSignal.InstanceId, - ScheduledTime = operationAction.SendSignal.ScheduledTime?.ToDateTime(), - RequestTime = operationAction.SendSignal.RequestTime?.ToDateTimeOffset(), - ParentTraceContext = operationAction.SendSignal.ParentTraceContext != null ? - new DistributedTraceContext( - operationAction.SendSignal.ParentTraceContext.TraceParent, - operationAction.SendSignal.ParentTraceContext.TraceState) : null, - }; - - case P.OperationAction.OperationActionTypeOneofCase.StartNewOrchestration: - - return new StartNewOrchestrationOperationAction() - { - Name = operationAction.StartNewOrchestration.Name, - Input = operationAction.StartNewOrchestration.Input, - InstanceId = operationAction.StartNewOrchestration.InstanceId, - Version = operationAction.StartNewOrchestration.Version, - ScheduledStartTime = operationAction.StartNewOrchestration.ScheduledTime?.ToDateTime(), - RequestTime = operationAction.StartNewOrchestration.RequestTime?.ToDateTimeOffset(), - ParentTraceContext = operationAction.StartNewOrchestration.ParentTraceContext != null ? - new DistributedTraceContext( - operationAction.StartNewOrchestration.ParentTraceContext.TraceParent, - operationAction.StartNewOrchestration.ParentTraceContext.TraceState) : null, - }; - default: - throw new NotSupportedException($"Deserialization of {operationAction.OperationActionTypeCase} is not supported."); - } - } - - /// - /// Converts a to . - /// - /// The operation action to convert. - /// The converted operation action. - [return: NotNullIfNotNull(nameof(operationAction))] - internal static P.OperationAction? ToOperationAction(this OperationAction? operationAction) - { - if (operationAction == null) - { - return null; - } - - var action = new P.OperationAction(); - - switch (operationAction) - { - case SendSignalOperationAction sendSignalAction: - - action.SendSignal = new P.SendSignalAction() - { - Name = sendSignalAction.Name, - Input = sendSignalAction.Input, - InstanceId = sendSignalAction.InstanceId, - ScheduledTime = sendSignalAction.ScheduledTime?.ToTimestamp(), - RequestTime = sendSignalAction.RequestTime?.ToTimestamp(), - ParentTraceContext = sendSignalAction.ParentTraceContext != null ? - new P.TraceContext - { - TraceParent = sendSignalAction.ParentTraceContext.TraceParent, - TraceState = sendSignalAction.ParentTraceContext.TraceState, - } - : null, - }; - break; - - case StartNewOrchestrationOperationAction startNewOrchestrationAction: - - action.StartNewOrchestration = new P.StartNewOrchestrationAction() - { - Name = startNewOrchestrationAction.Name, - Input = startNewOrchestrationAction.Input, - Version = startNewOrchestrationAction.Version, - InstanceId = startNewOrchestrationAction.InstanceId, - ScheduledTime = startNewOrchestrationAction.ScheduledStartTime?.ToTimestamp(), - RequestTime = startNewOrchestrationAction.RequestTime?.ToTimestamp(), - ParentTraceContext = startNewOrchestrationAction.ParentTraceContext != null ? - new P.TraceContext - { - TraceParent = startNewOrchestrationAction.ParentTraceContext.TraceParent, - TraceState = startNewOrchestrationAction.ParentTraceContext.TraceState, - } - : null, - }; - break; - } - - return action; - } - - /// - /// Converts a to a . - /// - /// The operation result to convert. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(entityBatchResult))] - internal static EntityBatchResult? ToEntityBatchResult(this P.EntityBatchResult? entityBatchResult) - { - if (entityBatchResult == null) - { - return null; - } - - return new EntityBatchResult() - { - Actions = entityBatchResult.Actions.Select(operationAction => operationAction!.ToOperationAction()).ToList(), - EntityState = entityBatchResult.EntityState, - Results = entityBatchResult.Results.Select(operationResult => operationResult!.ToOperationResult()).ToList(), - FailureDetails = entityBatchResult.FailureDetails.ToCore(), - }; - } - - /// - /// Converts a to . - /// - /// The operation result to convert. - /// The completion token, or null for the older protocol. - /// Additional information about each operation, required by DTS. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(entityBatchResult))] - internal static P.EntityBatchResult? ToEntityBatchResult( - this EntityBatchResult? entityBatchResult, - string? completionToken = null, - IEnumerable? operationInfos = null) - { - if (entityBatchResult == null) - { - return null; - } - - return new P.EntityBatchResult() - { - EntityState = entityBatchResult.EntityState, - FailureDetails = entityBatchResult.FailureDetails.ToProtobuf(), - Actions = { entityBatchResult.Actions?.Select(a => a.ToOperationAction()) ?? [] }, - Results = { entityBatchResult.Results?.Select(a => a.ToOperationResult()) ?? [] }, - CompletionToken = completionToken ?? string.Empty, - OperationInfos = { operationInfos ?? [] }, - }; - } - - /// - /// Converts the gRPC representation of orchestrator entity parameters to the DT.Core representation. - /// - /// The DT.Core representation. - /// The gRPC representation. - [return: NotNullIfNotNull(nameof(parameters))] - internal static TaskOrchestrationEntityParameters? ToCore(this P.OrchestratorEntityParameters? parameters) - { - if (parameters == null) - { - return null; - } - - return new TaskOrchestrationEntityParameters() - { - EntityMessageReorderWindow = parameters.EntityMessageReorderWindow.ToTimeSpan(), - }; - } - - /// - /// Gets the approximate byte count for a . - /// - /// The failure details. - /// The approximate byte count. - internal static int GetApproximateByteCount(this P.TaskFailureDetails failureDetails) - { - // Protobuf strings are always UTF-8: https://developers.google.com/protocol-buffers/docs/proto3#scalar - Encoding encoding = Encoding.UTF8; - - int byteCount = 0; - if (failureDetails.ErrorType != null) - { - byteCount += encoding.GetByteCount(failureDetails.ErrorType); - } - - if (failureDetails.ErrorMessage != null) - { - byteCount += encoding.GetByteCount(failureDetails.ErrorMessage); - } - - if (failureDetails.StackTrace != null) - { - byteCount += encoding.GetByteCount(failureDetails.StackTrace); - } - - if (failureDetails.InnerFailure != null) - { - byteCount += failureDetails.InnerFailure.GetApproximateByteCount(); - } - - return byteCount; - } - - /// - /// Decode a protobuf message from a base64 string. - /// - /// The type to decode to. - /// The message parser. - /// The base64 encoded message. - /// The decoded message. - /// If decoding fails. - internal static T Base64Decode(this MessageParser parser, string encodedMessage) where T : IMessage - { - // Decode the base64 in a way that doesn't allocate a byte[] on each request - int encodedByteCount = Encoding.UTF8.GetByteCount(encodedMessage); - byte[] buffer = ArrayPool.Shared.Rent(encodedByteCount); - try - { - // The Base64 APIs require first converting the string into UTF-8 bytes. We then - // do an in-place conversion from base64 UTF-8 bytes to protobuf bytes so that - // we can finally decode the protobuf request. - Encoding.UTF8.GetBytes(encodedMessage, 0, encodedMessage.Length, buffer, 0); - OperationStatus status = Base64.DecodeFromUtf8InPlace( - buffer.AsSpan(0, encodedByteCount), - out int bytesWritten); - if (status != OperationStatus.Done) - { - throw new ArgumentException( - $"Failed to base64-decode the '{typeof(T).Name}' payload: {status}", nameof(encodedMessage)); - } - - return (T)parser.ParseFrom(buffer, 0, bytesWritten); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - /// - /// Converts a grpc to a . - /// - /// The failure details to convert. - /// The converted failure details. - internal static FailureDetails? ToCore(this P.TaskFailureDetails? failureDetails) - { - if (failureDetails == null) - { - return null; - } - - return new FailureDetails( - failureDetails.ErrorType, - failureDetails.ErrorMessage, - failureDetails.StackTrace, - failureDetails.InnerFailure.ToCore(), - failureDetails.IsNonRetriable, - ConvertProperties(failureDetails.Properties)); - } - - /// - /// Converts a instance to a corresponding C# object. - /// - /// The Protobuf Value to convert. - /// The corresponding C# object. - /// - /// Thrown when the Protobuf Value.KindCase is not one of the supported types. - /// - internal static object? ConvertValueToObject(Google.Protobuf.WellKnownTypes.Value value) - { - switch (value.KindCase) - { - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NullValue: - return null; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NumberValue: - return value.NumberValue; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StringValue: + } + + if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) + { + protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); + } + + break; + default: + throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); + } + + response.Actions.Add(protoAction); + } + + return response; + } + + /// + /// Converts a to a . + /// + /// The status to convert. + /// The converted status. + internal static OrchestrationStatus ToCore(this P.OrchestrationStatus status) + { + return (OrchestrationStatus)status; + } + + /// + /// Converts a to a . + /// + /// The status to convert. + /// The converted status. + [return: NotNullIfNotNull(nameof(status))] + internal static OrchestrationInstance? ToCore(this P.OrchestrationInstance? status) + { + if (status == null) + { + return null; + } + + return new OrchestrationInstance + { + InstanceId = status.InstanceId, + ExecutionId = status.ExecutionId, + }; + } + + /// + /// Converts a to a . + /// + /// The failure details to convert. + /// The converted failure details. + [return: NotNullIfNotNull(nameof(failureDetails))] + internal static TaskFailureDetails? ToTaskFailureDetails(this P.TaskFailureDetails? failureDetails) + { + if (failureDetails == null) + { + return null; + } + + return new TaskFailureDetails( + failureDetails.ErrorType, + failureDetails.ErrorMessage, + failureDetails.StackTrace, + failureDetails.InnerFailure.ToTaskFailureDetails(), + ConvertProperties(failureDetails.Properties)); + } + + /// + /// Converts a to . + /// + /// The exception to convert. + /// Optional exception properties provider. + /// The task failure details. + [return: NotNullIfNotNull(nameof(e))] + internal static P.TaskFailureDetails? ToTaskFailureDetails(this Exception? e, DTCore.IExceptionPropertiesProvider? exceptionPropertiesProvider = null) + { + if (e == null) + { + return null; + } + + IDictionary? properties = exceptionPropertiesProvider?.GetExceptionProperties(e); + + var taskFailureDetails = new P.TaskFailureDetails + { + ErrorType = e.GetType().FullName, + ErrorMessage = e.Message, + StackTrace = e.StackTrace, + InnerFailure = e.InnerException.ToTaskFailureDetails(exceptionPropertiesProvider), + }; + + if (properties != null) + { + foreach (var kvp in properties) + { + taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); + } + } + + return taskFailureDetails; + } + + /// + /// Converts a to a . + /// + /// The entity batch request to convert. + /// The converted entity batch request. + [return: NotNullIfNotNull(nameof(entityBatchRequest))] + internal static EntityBatchRequest? ToEntityBatchRequest(this P.EntityBatchRequest? entityBatchRequest) + { + if (entityBatchRequest == null) + { + return null; + } + + return new EntityBatchRequest() + { + EntityState = entityBatchRequest.EntityState, + InstanceId = entityBatchRequest.InstanceId, + Operations = entityBatchRequest.Operations.Select(r => r.ToOperationRequest()).ToList(), + }; + } + + /// + /// Converts a to a . + /// + /// The entity request to convert. + /// The converted request. + /// Additional info about each operation, required by DTS. + internal static void ToEntityBatchRequest( + this P.EntityRequest entityRequest, + out EntityBatchRequest batchRequest, + out List operationInfos) + { + batchRequest = new EntityBatchRequest() + { + EntityState = entityRequest.EntityState, + InstanceId = entityRequest.InstanceId, + Operations = [], // operations are added to this collection below + }; + + operationInfos = new(entityRequest.OperationRequests.Count); + + foreach (P.HistoryEvent? op in entityRequest.OperationRequests) + { + if (op.EntityOperationSignaled is not null) + { + batchRequest.Operations.Add(new OperationRequest + { + Id = Guid.Parse(op.EntityOperationSignaled.RequestId), + Operation = op.EntityOperationSignaled.Operation, + Input = op.EntityOperationSignaled.Input, + TraceContext = op.EntityOperationSignaled.ParentTraceContext != null + ? new DistributedTraceContext( + op.EntityOperationSignaled.ParentTraceContext.TraceParent, + op.EntityOperationSignaled.ParentTraceContext.TraceState) + : null, + }); + operationInfos.Add(new P.OperationInfo + { + RequestId = op.EntityOperationSignaled.RequestId, + ResponseDestination = null, // means we don't send back a response to the caller + }); + } + else if (op.EntityOperationCalled is not null) + { + batchRequest.Operations.Add(new OperationRequest + { + Id = Guid.Parse(op.EntityOperationCalled.RequestId), + Operation = op.EntityOperationCalled.Operation, + Input = op.EntityOperationCalled.Input, + TraceContext = op.EntityOperationCalled.ParentTraceContext != null + ? new DistributedTraceContext( + op.EntityOperationCalled.ParentTraceContext.TraceParent, + op.EntityOperationCalled.ParentTraceContext.TraceState) + : null, + }); + operationInfos.Add(new P.OperationInfo + { + RequestId = op.EntityOperationCalled.RequestId, + ResponseDestination = new P.OrchestrationInstance + { + InstanceId = op.EntityOperationCalled.ParentInstanceId, + ExecutionId = op.EntityOperationCalled.ParentExecutionId, + }, + }); + } + } + } + + /// + /// Converts a to a . + /// + /// The operation request to convert. + /// The converted operation request. + [return: NotNullIfNotNull(nameof(operationRequest))] + internal static OperationRequest? ToOperationRequest(this P.OperationRequest? operationRequest) + { + if (operationRequest == null) + { + return null; + } + + return new OperationRequest() + { + Operation = operationRequest.Operation, + Input = operationRequest.Input, + Id = Guid.Parse(operationRequest.RequestId), + TraceContext = operationRequest.TraceContext != null ? + new DistributedTraceContext( + operationRequest.TraceContext.TraceParent, + operationRequest.TraceContext.TraceState) : null, + }; + } + + /// + /// Converts a to a . + /// + /// The operation result to convert. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(operationResult))] + internal static OperationResult? ToOperationResult(this P.OperationResult? operationResult) + { + if (operationResult == null) + { + return null; + } + + switch (operationResult.ResultTypeCase) + { + case P.OperationResult.ResultTypeOneofCase.Success: + return new OperationResult() + { + Result = operationResult.Success.Result, + StartTimeUtc = operationResult.Success.StartTimeUtc?.ToDateTime(), + EndTimeUtc = operationResult.Success.EndTimeUtc?.ToDateTime(), + }; + + case P.OperationResult.ResultTypeOneofCase.Failure: + return new OperationResult() + { + FailureDetails = operationResult.Failure.FailureDetails.ToCore(), + StartTimeUtc = operationResult.Failure.StartTimeUtc?.ToDateTime(), + EndTimeUtc = operationResult.Failure.EndTimeUtc?.ToDateTime(), + }; + + default: + throw new NotSupportedException($"Deserialization of {operationResult.ResultTypeCase} is not supported."); + } + } + + /// + /// Converts a to . + /// + /// The operation result to convert. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(operationResult))] + internal static P.OperationResult? ToOperationResult(this OperationResult? operationResult) + { + if (operationResult == null) + { + return null; + } + + if (operationResult.FailureDetails == null) + { + return new P.OperationResult() + { + Success = new P.OperationResultSuccess() + { + Result = operationResult.Result, + StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), + EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), + }, + }; + } + else + { + return new P.OperationResult() + { + Failure = new P.OperationResultFailure() + { + FailureDetails = ToProtobuf(operationResult.FailureDetails), + StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), + EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), + }, + }; + } + } + + /// + /// Converts a to a . + /// + /// The operation action to convert. + /// The converted operation action. + [return: NotNullIfNotNull(nameof(operationAction))] + internal static OperationAction? ToOperationAction(this P.OperationAction? operationAction) + { + if (operationAction == null) + { + return null; + } + + switch (operationAction.OperationActionTypeCase) + { + case P.OperationAction.OperationActionTypeOneofCase.SendSignal: + + return new SendSignalOperationAction() + { + Name = operationAction.SendSignal.Name, + Input = operationAction.SendSignal.Input, + InstanceId = operationAction.SendSignal.InstanceId, + ScheduledTime = operationAction.SendSignal.ScheduledTime?.ToDateTime(), + RequestTime = operationAction.SendSignal.RequestTime?.ToDateTimeOffset(), + ParentTraceContext = operationAction.SendSignal.ParentTraceContext != null ? + new DistributedTraceContext( + operationAction.SendSignal.ParentTraceContext.TraceParent, + operationAction.SendSignal.ParentTraceContext.TraceState) : null, + }; + + case P.OperationAction.OperationActionTypeOneofCase.StartNewOrchestration: + + return new StartNewOrchestrationOperationAction() + { + Name = operationAction.StartNewOrchestration.Name, + Input = operationAction.StartNewOrchestration.Input, + InstanceId = operationAction.StartNewOrchestration.InstanceId, + Version = operationAction.StartNewOrchestration.Version, + ScheduledStartTime = operationAction.StartNewOrchestration.ScheduledTime?.ToDateTime(), + RequestTime = operationAction.StartNewOrchestration.RequestTime?.ToDateTimeOffset(), + ParentTraceContext = operationAction.StartNewOrchestration.ParentTraceContext != null ? + new DistributedTraceContext( + operationAction.StartNewOrchestration.ParentTraceContext.TraceParent, + operationAction.StartNewOrchestration.ParentTraceContext.TraceState) : null, + }; + default: + throw new NotSupportedException($"Deserialization of {operationAction.OperationActionTypeCase} is not supported."); + } + } + + /// + /// Converts a to . + /// + /// The operation action to convert. + /// The converted operation action. + [return: NotNullIfNotNull(nameof(operationAction))] + internal static P.OperationAction? ToOperationAction(this OperationAction? operationAction) + { + if (operationAction == null) + { + return null; + } + + var action = new P.OperationAction(); + + switch (operationAction) + { + case SendSignalOperationAction sendSignalAction: + + action.SendSignal = new P.SendSignalAction() + { + Name = sendSignalAction.Name, + Input = sendSignalAction.Input, + InstanceId = sendSignalAction.InstanceId, + ScheduledTime = sendSignalAction.ScheduledTime?.ToTimestamp(), + RequestTime = sendSignalAction.RequestTime?.ToTimestamp(), + ParentTraceContext = sendSignalAction.ParentTraceContext != null ? + new P.TraceContext + { + TraceParent = sendSignalAction.ParentTraceContext.TraceParent, + TraceState = sendSignalAction.ParentTraceContext.TraceState, + } + : null, + }; + break; + + case StartNewOrchestrationOperationAction startNewOrchestrationAction: + + action.StartNewOrchestration = new P.StartNewOrchestrationAction() + { + Name = startNewOrchestrationAction.Name, + Input = startNewOrchestrationAction.Input, + Version = startNewOrchestrationAction.Version, + InstanceId = startNewOrchestrationAction.InstanceId, + ScheduledTime = startNewOrchestrationAction.ScheduledStartTime?.ToTimestamp(), + RequestTime = startNewOrchestrationAction.RequestTime?.ToTimestamp(), + ParentTraceContext = startNewOrchestrationAction.ParentTraceContext != null ? + new P.TraceContext + { + TraceParent = startNewOrchestrationAction.ParentTraceContext.TraceParent, + TraceState = startNewOrchestrationAction.ParentTraceContext.TraceState, + } + : null, + }; + break; + } + + return action; + } + + /// + /// Converts a to a . + /// + /// The operation result to convert. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(entityBatchResult))] + internal static EntityBatchResult? ToEntityBatchResult(this P.EntityBatchResult? entityBatchResult) + { + if (entityBatchResult == null) + { + return null; + } + + return new EntityBatchResult() + { + Actions = entityBatchResult.Actions.Select(operationAction => operationAction!.ToOperationAction()).ToList(), + EntityState = entityBatchResult.EntityState, + Results = entityBatchResult.Results.Select(operationResult => operationResult!.ToOperationResult()).ToList(), + FailureDetails = entityBatchResult.FailureDetails.ToCore(), + }; + } + + /// + /// Converts a to . + /// + /// The operation result to convert. + /// The completion token, or null for the older protocol. + /// Additional information about each operation, required by DTS. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(entityBatchResult))] + internal static P.EntityBatchResult? ToEntityBatchResult( + this EntityBatchResult? entityBatchResult, + string? completionToken = null, + IEnumerable? operationInfos = null) + { + if (entityBatchResult == null) + { + return null; + } + + return new P.EntityBatchResult() + { + EntityState = entityBatchResult.EntityState, + FailureDetails = entityBatchResult.FailureDetails.ToProtobuf(), + Actions = { entityBatchResult.Actions?.Select(a => a.ToOperationAction()) ?? [] }, + Results = { entityBatchResult.Results?.Select(a => a.ToOperationResult()) ?? [] }, + CompletionToken = completionToken ?? string.Empty, + OperationInfos = { operationInfos ?? [] }, + }; + } + + /// + /// Converts the gRPC representation of orchestrator entity parameters to the DT.Core representation. + /// + /// The DT.Core representation. + /// The gRPC representation. + [return: NotNullIfNotNull(nameof(parameters))] + internal static TaskOrchestrationEntityParameters? ToCore(this P.OrchestratorEntityParameters? parameters) + { + if (parameters == null) + { + return null; + } + + return new TaskOrchestrationEntityParameters() + { + EntityMessageReorderWindow = parameters.EntityMessageReorderWindow.ToTimeSpan(), + }; + } + + /// + /// Gets the approximate byte count for a . + /// + /// The failure details. + /// The approximate byte count. + internal static int GetApproximateByteCount(this P.TaskFailureDetails failureDetails) + { + // Protobuf strings are always UTF-8: https://developers.google.com/protocol-buffers/docs/proto3#scalar + Encoding encoding = Encoding.UTF8; + + int byteCount = 0; + if (failureDetails.ErrorType != null) + { + byteCount += encoding.GetByteCount(failureDetails.ErrorType); + } + + if (failureDetails.ErrorMessage != null) + { + byteCount += encoding.GetByteCount(failureDetails.ErrorMessage); + } + + if (failureDetails.StackTrace != null) + { + byteCount += encoding.GetByteCount(failureDetails.StackTrace); + } + + if (failureDetails.InnerFailure != null) + { + byteCount += failureDetails.InnerFailure.GetApproximateByteCount(); + } + + return byteCount; + } + + /// + /// Decode a protobuf message from a base64 string. + /// + /// The type to decode to. + /// The message parser. + /// The base64 encoded message. + /// The decoded message. + /// If decoding fails. + internal static T Base64Decode(this MessageParser parser, string encodedMessage) where T : IMessage + { + // Decode the base64 in a way that doesn't allocate a byte[] on each request + int encodedByteCount = Encoding.UTF8.GetByteCount(encodedMessage); + byte[] buffer = ArrayPool.Shared.Rent(encodedByteCount); + try + { + // The Base64 APIs require first converting the string into UTF-8 bytes. We then + // do an in-place conversion from base64 UTF-8 bytes to protobuf bytes so that + // we can finally decode the protobuf request. + Encoding.UTF8.GetBytes(encodedMessage, 0, encodedMessage.Length, buffer, 0); + OperationStatus status = Base64.DecodeFromUtf8InPlace( + buffer.AsSpan(0, encodedByteCount), + out int bytesWritten); + if (status != OperationStatus.Done) + { + throw new ArgumentException( + $"Failed to base64-decode the '{typeof(T).Name}' payload: {status}", nameof(encodedMessage)); + } + + return (T)parser.ParseFrom(buffer, 0, bytesWritten); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Converts a grpc to a . + /// + /// The failure details to convert. + /// The converted failure details. + internal static FailureDetails? ToCore(this P.TaskFailureDetails? failureDetails) + { + if (failureDetails == null) + { + return null; + } + + return new FailureDetails( + failureDetails.ErrorType, + failureDetails.ErrorMessage, + failureDetails.StackTrace, + failureDetails.InnerFailure.ToCore(), + failureDetails.IsNonRetriable, + ConvertProperties(failureDetails.Properties)); + } + + /// + /// Converts a instance to a corresponding C# object. + /// + /// The Protobuf Value to convert. + /// The corresponding C# object. + /// + /// Thrown when the Protobuf Value.KindCase is not one of the supported types. + /// + internal static object? ConvertValueToObject(Google.Protobuf.WellKnownTypes.Value value) + { + switch (value.KindCase) + { + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NullValue: + return null; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NumberValue: + return value.NumberValue; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StringValue: string stringValue = value.StringValue; - // If the value starts with the 'dt:' prefix, it may represent a DateTime value � attempt to parse it. + // If the value starts with the 'dt:' prefix, it may represent a DateTime value - attempt to parse it. if (stringValue.StartsWith("dt:", StringComparison.Ordinal)) { if (DateTime.TryParse(stringValue[3..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime date)) @@ -1075,7 +1075,7 @@ internal static T Base64Decode(this MessageParser parser, string encodedMessa } } - // If the value starts with the 'dto:' prefix, it may represent a DateTime value � attempt to parse it. + // If the value starts with the 'dto:' prefix, it may represent a DateTime value - attempt to parse it. if (stringValue.StartsWith("dto:", StringComparison.Ordinal)) { if (DateTimeOffset.TryParse(stringValue[4..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset date)) @@ -1085,110 +1085,110 @@ internal static T Base64Decode(this MessageParser parser, string encodedMessa } // Otherwise just return as string - return stringValue; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.BoolValue: - return value.BoolValue; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StructValue: - return value.StructValue.Fields.ToDictionary( - pair => pair.Key, - pair => ConvertValueToObject(pair.Value)); - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.ListValue: - return value.ListValue.Values.Select(ConvertValueToObject).ToList(); + return stringValue; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.BoolValue: + return value.BoolValue; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StructValue: + return value.StructValue.Fields.ToDictionary( + pair => pair.Key, + pair => ConvertValueToObject(pair.Value)); + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.ListValue: + return value.ListValue.Values.Select(ConvertValueToObject).ToList(); default: // Fallback: serialize the whole value to JSON string - return JsonSerializer.Serialize(value); - } + return JsonSerializer.Serialize(value); + } } - /// - /// Converts a MapFieldinto a IDictionary. - /// + /// + /// Converts a MapFieldinto a IDictionary. + /// /// The map to convert. - /// Dictionary contains the converted obejct. - internal static IDictionary ConvertProperties(MapField properties) - { - return properties.ToDictionary( - kvp => kvp.Key, - kvp => ConvertValueToObject(kvp.Value)); - } - - /// - /// Converts a C# object to a protobuf Value. - /// - /// The object to convert. - /// The converted protobuf Value. - internal static Value ConvertObjectToValue(object? obj) - { - return obj switch - { - null => Value.ForNull(), - string str => Value.ForString(str), - bool b => Value.ForBool(b), - int i => Value.ForNumber(i), - long l => Value.ForNumber(l), - float f => Value.ForNumber(f), - double d => Value.ForNumber(d), + /// Dictionary contains the converted obejct. + internal static IDictionary ConvertProperties(MapField properties) + { + return properties.ToDictionary( + kvp => kvp.Key, + kvp => ConvertValueToObject(kvp.Value)); + } + + /// + /// Converts a C# object to a protobuf Value. + /// + /// The object to convert. + /// The converted protobuf Value. + internal static Value ConvertObjectToValue(object? obj) + { + return obj switch + { + null => Value.ForNull(), + string str => Value.ForString(str), + bool b => Value.ForBool(b), + int i => Value.ForNumber(i), + long l => Value.ForNumber(l), + float f => Value.ForNumber(f), + double d => Value.ForNumber(d), decimal dec => Value.ForNumber((double)dec), - // For DateTime and DateTimeOffset, add prefix to distinguish from normal string. - DateTime dt => Value.ForString($"dt:{dt.ToString("O")}"), - DateTimeOffset dto => Value.ForString($"dto:{dto.ToString("O")}"), - IDictionary dict => Value.ForStruct(new Struct - { - Fields = { dict.ToDictionary(kvp => kvp.Key, kvp => ConvertObjectToValue(kvp.Value)) }, - }), + // For DateTime and DateTimeOffset, add prefix to distinguish from normal string. + DateTime dt => Value.ForString($"dt:{dt.ToString("O")}"), + DateTimeOffset dto => Value.ForString($"dto:{dto.ToString("O")}"), + IDictionary dict => Value.ForStruct(new Struct + { + Fields = { dict.ToDictionary(kvp => kvp.Key, kvp => ConvertObjectToValue(kvp.Value)) }, + }), IEnumerable e => Value.ForList(e.Cast().Select(ConvertObjectToValue).ToArray()), // Fallback: convert unlisted type to string. - _ => Value.ForString(obj.ToString() ?? string.Empty), - }; - } - - /// - /// Converts a to a grpc . - /// - /// The failure details to convert. - /// The converted failure details. - static P.TaskFailureDetails? ToProtobuf(this FailureDetails? failureDetails) - { - if (failureDetails == null) - { - return null; - } - - var taskFailureDetails = new P.TaskFailureDetails - { - ErrorType = failureDetails.ErrorType ?? "(unknown)", - ErrorMessage = failureDetails.ErrorMessage ?? "(unknown)", - StackTrace = failureDetails.StackTrace, - IsNonRetriable = failureDetails.IsNonRetriable, - InnerFailure = failureDetails.InnerFailure.ToProtobuf(), - }; - - // Properly populate the MapField - if (failureDetails.Properties != null) - { - foreach (var kvp in failureDetails.Properties) - { - taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); - } - } - - return taskFailureDetails; - } - - static P.OrchestrationStatus ToProtobuf(this OrchestrationStatus status) - { - return (P.OrchestrationStatus)status; - } - - static P.OrchestrationInstance ToProtobuf(this OrchestrationInstance instance) - { - return new P.OrchestrationInstance - { - InstanceId = instance.InstanceId, - ExecutionId = instance.ExecutionId, - }; + _ => Value.ForString(obj.ToString() ?? string.Empty), + }; + } + + /// + /// Converts a to a grpc . + /// + /// The failure details to convert. + /// The converted failure details. + static P.TaskFailureDetails? ToProtobuf(this FailureDetails? failureDetails) + { + if (failureDetails == null) + { + return null; + } + + var taskFailureDetails = new P.TaskFailureDetails + { + ErrorType = failureDetails.ErrorType ?? "(unknown)", + ErrorMessage = failureDetails.ErrorMessage ?? "(unknown)", + StackTrace = failureDetails.StackTrace, + IsNonRetriable = failureDetails.IsNonRetriable, + InnerFailure = failureDetails.InnerFailure.ToProtobuf(), + }; + + // Properly populate the MapField + if (failureDetails.Properties != null) + { + foreach (var kvp in failureDetails.Properties) + { + taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); + } + } + + return taskFailureDetails; + } + + static P.OrchestrationStatus ToProtobuf(this OrchestrationStatus status) + { + return (P.OrchestrationStatus)status; + } + + static P.OrchestrationInstance ToProtobuf(this OrchestrationInstance instance) + { + return new P.OrchestrationInstance + { + InstanceId = instance.InstanceId, + ExecutionId = instance.ExecutionId, + }; } static P.HistoryEvent ToProtobuf(HistoryEvent e) @@ -1211,107 +1211,107 @@ static P.HistoryEvent ToProtobuf(HistoryEvent e) } throw new ArgumentException("Unsupported event type"); - } - - /// - /// Tracks state required for converting orchestration histories containing entity-related events. - /// - internal class EntityConversionState - { - readonly bool insertMissingEntityUnlocks; - - OrchestrationInstance? instance; - HashSet? entityRequestIds; - Dictionary? unlockObligations; - - /// - /// Initializes a new instance of the class. - /// - /// Whether to insert missing unlock events in to the history - /// when the orchestration completes. - public EntityConversionState(bool insertMissingEntityUnlocks) - { - this.ConvertFromProto = (P.HistoryEvent e) => ProtoUtils.ConvertHistoryEvent(e, this); - this.insertMissingEntityUnlocks = insertMissingEntityUnlocks; - } - - /// - /// Gets a function that converts a history event in protobuf format to a core history event. - /// - public Func ConvertFromProto { get; } - - /// - /// Gets the orchestration instance of this history. - /// - public OrchestrationInstance? CurrentInstance => this.instance; - - /// - /// Gets the set of guids that have been used as entity request ids in this history. - /// - public HashSet EntityRequestIds => this.entityRequestIds ??= new(); - - /// - /// Records the orchestration instance, which may be needed for some conversions. - /// - /// The orchestration instance. - public void SetOrchestrationInstance(OrchestrationInstance instance) - { - this.instance = instance; - } - - /// - /// Adds unlock obligations for all entities that are being locked by this request. - /// - /// The lock request. - public void AddUnlockObligations(P.EntityLockRequestedEvent request) - { - if (!this.insertMissingEntityUnlocks) - { - return; - } - - this.unlockObligations ??= new(); - - foreach (string target in request.LockSet) - { - this.unlockObligations[target] = request.CriticalSectionId; - } - } - - /// - /// Removes an unlock obligation. - /// - /// The target entity. - public void RemoveUnlockObligation(string target) - { - if (!this.insertMissingEntityUnlocks) - { - return; - } - - this.unlockObligations?.Remove(target); - } - - /// - /// Returns the remaining unlock obligations, and clears the list. - /// - /// The unlock obligations. - public IEnumerable<(string Target, string CriticalSectionId)> ResetObligations() - { - if (!this.insertMissingEntityUnlocks) - { - yield break; - } - - if (this.unlockObligations is not null) - { - foreach (var kvp in this.unlockObligations) - { - yield return (kvp.Key, kvp.Value); - } - - this.unlockObligations = null; - } - } - } -} + } + + /// + /// Tracks state required for converting orchestration histories containing entity-related events. + /// + internal class EntityConversionState + { + readonly bool insertMissingEntityUnlocks; + + OrchestrationInstance? instance; + HashSet? entityRequestIds; + Dictionary? unlockObligations; + + /// + /// Initializes a new instance of the class. + /// + /// Whether to insert missing unlock events in to the history + /// when the orchestration completes. + public EntityConversionState(bool insertMissingEntityUnlocks) + { + this.ConvertFromProto = (P.HistoryEvent e) => ProtoUtils.ConvertHistoryEvent(e, this); + this.insertMissingEntityUnlocks = insertMissingEntityUnlocks; + } + + /// + /// Gets a function that converts a history event in protobuf format to a core history event. + /// + public Func ConvertFromProto { get; } + + /// + /// Gets the orchestration instance of this history. + /// + public OrchestrationInstance? CurrentInstance => this.instance; + + /// + /// Gets the set of guids that have been used as entity request ids in this history. + /// + public HashSet EntityRequestIds => this.entityRequestIds ??= new(); + + /// + /// Records the orchestration instance, which may be needed for some conversions. + /// + /// The orchestration instance. + public void SetOrchestrationInstance(OrchestrationInstance instance) + { + this.instance = instance; + } + + /// + /// Adds unlock obligations for all entities that are being locked by this request. + /// + /// The lock request. + public void AddUnlockObligations(P.EntityLockRequestedEvent request) + { + if (!this.insertMissingEntityUnlocks) + { + return; + } + + this.unlockObligations ??= new(); + + foreach (string target in request.LockSet) + { + this.unlockObligations[target] = request.CriticalSectionId; + } + } + + /// + /// Removes an unlock obligation. + /// + /// The target entity. + public void RemoveUnlockObligation(string target) + { + if (!this.insertMissingEntityUnlocks) + { + return; + } + + this.unlockObligations?.Remove(target); + } + + /// + /// Returns the remaining unlock obligations, and clears the list. + /// + /// The unlock obligations. + public IEnumerable<(string Target, string CriticalSectionId)> ResetObligations() + { + if (!this.insertMissingEntityUnlocks) + { + yield break; + } + + if (this.unlockObligations is not null) + { + foreach (var kvp in this.unlockObligations) + { + yield return (kvp.Key, kvp.Value); + } + + this.unlockObligations = null; + } + } + } +} From 6fade3c5dc573acd1c98d4e421d420a2699e46dc Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Tue, 3 Mar 2026 15:18:06 -0800 Subject: [PATCH 4/4] Fix line ending normalization in ProtoUtils.cs Restore original LF line endings that were inadvertently converted to CRLF when editing on Windows. Only the actual code changes remain in the diff. --- src/Shared/Grpc/ProtoUtils.cs | 2501 ++++++++++++++++----------------- 1 file changed, 1248 insertions(+), 1253 deletions(-) diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index bd0ccf2a..6bac27c5 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -1,1072 +1,1067 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Buffers; -using System.Buffers.Text; -using System.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text; -using System.Text.Json; -using DurableTask.Core; -using DurableTask.Core.Command; -using DurableTask.Core.Entities; -using DurableTask.Core.Entities.OperationFormat; -using DurableTask.Core.History; -using DurableTask.Core.Tracing; -using Google.Protobuf; -using Google.Protobuf.Collections; -using Google.Protobuf.WellKnownTypes; -using DTCore = DurableTask.Core; -using P = Microsoft.DurableTask.Protobuf; -using TraceHelper = Microsoft.DurableTask.Tracing.TraceHelper; - -namespace Microsoft.DurableTask; - -/// -/// Protobuf utilities and helpers. -/// -static class ProtoUtils -{ - /// - /// Converts a history event from to . - /// - /// The proto history event to converter. - /// The converted history event. - /// When the provided history event type is not supported. - internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto) - { - return ConvertHistoryEvent(proto, conversionState: null); - } - - /// - /// Converts a history event from to , and performs - /// stateful conversions of entity-related events. - /// - /// The proto history event to converter. - /// State needed for converting entity-related history entries and actions. - /// The converted history event. - /// When the provided history event type is not supported. - internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto, EntityConversionState? conversionState) - { - Check.NotNull(proto); - HistoryEvent historyEvent; - switch (proto.EventTypeCase) - { - case P.HistoryEvent.EventTypeOneofCase.ContinueAsNew: - historyEvent = new ContinueAsNewEvent(proto.EventId, proto.ContinueAsNew.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionStarted: - OrchestrationInstance instance = proto.ExecutionStarted.OrchestrationInstance.ToCore(); - conversionState?.SetOrchestrationInstance(instance); - historyEvent = new ExecutionStartedEvent(proto.EventId, proto.ExecutionStarted.Input) - { - Name = proto.ExecutionStarted.Name, - Version = proto.ExecutionStarted.Version, - OrchestrationInstance = instance, - Tags = proto.ExecutionStarted.Tags, - ParentInstance = proto.ExecutionStarted.ParentInstance == null ? null : new ParentInstance - { - Name = proto.ExecutionStarted.ParentInstance.Name, - Version = proto.ExecutionStarted.ParentInstance.Version, - OrchestrationInstance = proto.ExecutionStarted.ParentInstance.OrchestrationInstance.ToCore(), - TaskScheduleId = proto.ExecutionStarted.ParentInstance.TaskScheduledId, - }, - ScheduledStartTime = proto.ExecutionStarted.ScheduledStartTimestamp?.ToDateTime(), - }; - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionCompleted: - historyEvent = new ExecutionCompletedEvent( - proto.EventId, - proto.ExecutionCompleted.Result, +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Buffers; +using System.Buffers.Text; +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using System.Text.Json; +using DurableTask.Core; +using DurableTask.Core.Command; +using DurableTask.Core.Entities; +using DurableTask.Core.Entities.OperationFormat; +using DurableTask.Core.History; +using DurableTask.Core.Tracing; +using Google.Protobuf; +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; +using DTCore = DurableTask.Core; +using P = Microsoft.DurableTask.Protobuf; +using TraceHelper = Microsoft.DurableTask.Tracing.TraceHelper; + +namespace Microsoft.DurableTask; + +/// +/// Protobuf utilities and helpers. +/// +static class ProtoUtils +{ + /// + /// Converts a history event from to . + /// + /// The proto history event to converter. + /// The converted history event. + /// When the provided history event type is not supported. + internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto) + { + return ConvertHistoryEvent(proto, conversionState: null); + } + + /// + /// Converts a history event from to , and performs + /// stateful conversions of entity-related events. + /// + /// The proto history event to converter. + /// State needed for converting entity-related history entries and actions. + /// The converted history event. + /// When the provided history event type is not supported. + internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto, EntityConversionState? conversionState) + { + Check.NotNull(proto); + HistoryEvent historyEvent; + switch (proto.EventTypeCase) + { + case P.HistoryEvent.EventTypeOneofCase.ContinueAsNew: + historyEvent = new ContinueAsNewEvent(proto.EventId, proto.ContinueAsNew.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionStarted: + OrchestrationInstance instance = proto.ExecutionStarted.OrchestrationInstance.ToCore(); + conversionState?.SetOrchestrationInstance(instance); + historyEvent = new ExecutionStartedEvent(proto.EventId, proto.ExecutionStarted.Input) + { + Name = proto.ExecutionStarted.Name, + Version = proto.ExecutionStarted.Version, + OrchestrationInstance = instance, + Tags = proto.ExecutionStarted.Tags, + ParentInstance = proto.ExecutionStarted.ParentInstance == null ? null : new ParentInstance + { + Name = proto.ExecutionStarted.ParentInstance.Name, + Version = proto.ExecutionStarted.ParentInstance.Version, + OrchestrationInstance = proto.ExecutionStarted.ParentInstance.OrchestrationInstance.ToCore(), + TaskScheduleId = proto.ExecutionStarted.ParentInstance.TaskScheduledId, + }, + ScheduledStartTime = proto.ExecutionStarted.ScheduledStartTimestamp?.ToDateTime(), + }; + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionCompleted: + historyEvent = new ExecutionCompletedEvent( + proto.EventId, + proto.ExecutionCompleted.Result, proto.ExecutionCompleted.OrchestrationStatus.ToCore(), - proto.ExecutionCompleted.FailureDetails.ToCore()); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionTerminated: - historyEvent = new ExecutionTerminatedEvent(proto.EventId, proto.ExecutionTerminated.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionSuspended: - historyEvent = new ExecutionSuspendedEvent(proto.EventId, proto.ExecutionSuspended.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.ExecutionResumed: - historyEvent = new ExecutionResumedEvent(proto.EventId, proto.ExecutionResumed.Input); - break; - case P.HistoryEvent.EventTypeOneofCase.TaskScheduled: - historyEvent = new TaskScheduledEvent( - proto.EventId, - proto.TaskScheduled.Name, - proto.TaskScheduled.Version, - proto.TaskScheduled.Input) - { - Tags = proto.TaskScheduled.Tags, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.TaskCompleted: - historyEvent = new TaskCompletedEvent( - proto.EventId, - proto.TaskCompleted.TaskScheduledId, - proto.TaskCompleted.Result); - break; - case P.HistoryEvent.EventTypeOneofCase.TaskFailed: - historyEvent = new TaskFailedEvent( - proto.EventId, - proto.TaskFailed.TaskScheduledId, - reason: null, /* not supported */ - details: null, /* not supported */ - proto.TaskFailed.FailureDetails.ToCore()); - break; - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated: - historyEvent = new SubOrchestrationInstanceCreatedEvent(proto.EventId) - { - Input = proto.SubOrchestrationInstanceCreated.Input, - InstanceId = proto.SubOrchestrationInstanceCreated.InstanceId, - Name = proto.SubOrchestrationInstanceCreated.Name, - Version = proto.SubOrchestrationInstanceCreated.Version, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted: - historyEvent = new SubOrchestrationInstanceCompletedEvent( - proto.EventId, - proto.SubOrchestrationInstanceCompleted.TaskScheduledId, - proto.SubOrchestrationInstanceCompleted.Result); - break; - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed: - historyEvent = new SubOrchestrationInstanceFailedEvent( - proto.EventId, - proto.SubOrchestrationInstanceFailed.TaskScheduledId, - reason: null /* not supported */, - details: null /* not supported */, - proto.SubOrchestrationInstanceFailed.FailureDetails.ToCore()); - break; - case P.HistoryEvent.EventTypeOneofCase.TimerCreated: - historyEvent = new TimerCreatedEvent( - proto.EventId, - proto.TimerCreated.FireAt.ToDateTime()); - break; - case P.HistoryEvent.EventTypeOneofCase.TimerFired: - historyEvent = new TimerFiredEvent( - eventId: -1, - proto.TimerFired.FireAt.ToDateTime()) - { - TimerId = proto.TimerFired.TimerId, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.OrchestratorStarted: - historyEvent = new OrchestratorStartedEvent(proto.EventId); - break; - case P.HistoryEvent.EventTypeOneofCase.OrchestratorCompleted: - historyEvent = new OrchestratorCompletedEvent(proto.EventId); - break; - case P.HistoryEvent.EventTypeOneofCase.EventSent: - historyEvent = new EventSentEvent(proto.EventId) - { - InstanceId = proto.EventSent.InstanceId, - Name = proto.EventSent.Name, - Input = proto.EventSent.Input, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.EventRaised: - historyEvent = new EventRaisedEvent(proto.EventId, proto.EventRaised.Input) - { - Name = proto.EventRaised.Name, - }; - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationCalled: - historyEvent = EntityConversions.EncodeOperationCalled(proto, conversionState!.CurrentInstance); - conversionState?.EntityRequestIds.Add(proto.EntityOperationCalled.RequestId); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationSignaled: - historyEvent = EntityConversions.EncodeOperationSignaled(proto); - conversionState?.EntityRequestIds.Add(proto.EntityOperationSignaled.RequestId); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityLockRequested: - historyEvent = EntityConversions.EncodeLockRequested(proto, conversionState!.CurrentInstance); - conversionState?.AddUnlockObligations(proto.EntityLockRequested); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityUnlockSent: - historyEvent = EntityConversions.EncodeUnlockSent(proto, conversionState!.CurrentInstance); - conversionState?.RemoveUnlockObligation(proto.EntityUnlockSent.TargetInstanceId); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityLockGranted: - historyEvent = EntityConversions.EncodeLockGranted(proto); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationCompleted: - historyEvent = EntityConversions.EncodeOperationCompleted(proto); - break; - case P.HistoryEvent.EventTypeOneofCase.EntityOperationFailed: - historyEvent = EntityConversions.EncodeOperationFailed(proto); - break; - case P.HistoryEvent.EventTypeOneofCase.GenericEvent: - historyEvent = new GenericEvent(proto.EventId, proto.GenericEvent.Data); - break; - case P.HistoryEvent.EventTypeOneofCase.HistoryState: - historyEvent = new HistoryStateEvent( - proto.EventId, - new OrchestrationState - { - OrchestrationInstance = new OrchestrationInstance - { - InstanceId = proto.HistoryState.OrchestrationState.InstanceId, - }, - Name = proto.HistoryState.OrchestrationState.Name, - Version = proto.HistoryState.OrchestrationState.Version, - ScheduledStartTime = proto.HistoryState.OrchestrationState.ScheduledStartTimestamp.ToDateTime(), - CreatedTime = proto.HistoryState.OrchestrationState.CreatedTimestamp.ToDateTime(), - LastUpdatedTime = proto.HistoryState.OrchestrationState.LastUpdatedTimestamp.ToDateTime(), - Input = proto.HistoryState.OrchestrationState.Input, - Output = proto.HistoryState.OrchestrationState.Output, - Status = proto.HistoryState.OrchestrationState.CustomStatus, - Tags = proto.HistoryState.OrchestrationState.Tags, - }); - break; + proto.ExecutionCompleted.FailureDetails.ToCore()); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionTerminated: + historyEvent = new ExecutionTerminatedEvent(proto.EventId, proto.ExecutionTerminated.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionSuspended: + historyEvent = new ExecutionSuspendedEvent(proto.EventId, proto.ExecutionSuspended.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.ExecutionResumed: + historyEvent = new ExecutionResumedEvent(proto.EventId, proto.ExecutionResumed.Input); + break; + case P.HistoryEvent.EventTypeOneofCase.TaskScheduled: + historyEvent = new TaskScheduledEvent( + proto.EventId, + proto.TaskScheduled.Name, + proto.TaskScheduled.Version, + proto.TaskScheduled.Input) + { + Tags = proto.TaskScheduled.Tags, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.TaskCompleted: + historyEvent = new TaskCompletedEvent( + proto.EventId, + proto.TaskCompleted.TaskScheduledId, + proto.TaskCompleted.Result); + break; + case P.HistoryEvent.EventTypeOneofCase.TaskFailed: + historyEvent = new TaskFailedEvent( + proto.EventId, + proto.TaskFailed.TaskScheduledId, + reason: null, /* not supported */ + details: null, /* not supported */ + proto.TaskFailed.FailureDetails.ToCore()); + break; + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated: + historyEvent = new SubOrchestrationInstanceCreatedEvent(proto.EventId) + { + Input = proto.SubOrchestrationInstanceCreated.Input, + InstanceId = proto.SubOrchestrationInstanceCreated.InstanceId, + Name = proto.SubOrchestrationInstanceCreated.Name, + Version = proto.SubOrchestrationInstanceCreated.Version, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted: + historyEvent = new SubOrchestrationInstanceCompletedEvent( + proto.EventId, + proto.SubOrchestrationInstanceCompleted.TaskScheduledId, + proto.SubOrchestrationInstanceCompleted.Result); + break; + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed: + historyEvent = new SubOrchestrationInstanceFailedEvent( + proto.EventId, + proto.SubOrchestrationInstanceFailed.TaskScheduledId, + reason: null /* not supported */, + details: null /* not supported */, + proto.SubOrchestrationInstanceFailed.FailureDetails.ToCore()); + break; + case P.HistoryEvent.EventTypeOneofCase.TimerCreated: + historyEvent = new TimerCreatedEvent( + proto.EventId, + proto.TimerCreated.FireAt.ToDateTime()); + break; + case P.HistoryEvent.EventTypeOneofCase.TimerFired: + historyEvent = new TimerFiredEvent( + eventId: -1, + proto.TimerFired.FireAt.ToDateTime()) + { + TimerId = proto.TimerFired.TimerId, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.OrchestratorStarted: + historyEvent = new OrchestratorStartedEvent(proto.EventId); + break; + case P.HistoryEvent.EventTypeOneofCase.OrchestratorCompleted: + historyEvent = new OrchestratorCompletedEvent(proto.EventId); + break; + case P.HistoryEvent.EventTypeOneofCase.EventSent: + historyEvent = new EventSentEvent(proto.EventId) + { + InstanceId = proto.EventSent.InstanceId, + Name = proto.EventSent.Name, + Input = proto.EventSent.Input, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.EventRaised: + historyEvent = new EventRaisedEvent(proto.EventId, proto.EventRaised.Input) + { + Name = proto.EventRaised.Name, + }; + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationCalled: + historyEvent = EntityConversions.EncodeOperationCalled(proto, conversionState!.CurrentInstance); + conversionState?.EntityRequestIds.Add(proto.EntityOperationCalled.RequestId); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationSignaled: + historyEvent = EntityConversions.EncodeOperationSignaled(proto); + conversionState?.EntityRequestIds.Add(proto.EntityOperationSignaled.RequestId); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityLockRequested: + historyEvent = EntityConversions.EncodeLockRequested(proto, conversionState!.CurrentInstance); + conversionState?.AddUnlockObligations(proto.EntityLockRequested); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityUnlockSent: + historyEvent = EntityConversions.EncodeUnlockSent(proto, conversionState!.CurrentInstance); + conversionState?.RemoveUnlockObligation(proto.EntityUnlockSent.TargetInstanceId); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityLockGranted: + historyEvent = EntityConversions.EncodeLockGranted(proto); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationCompleted: + historyEvent = EntityConversions.EncodeOperationCompleted(proto); + break; + case P.HistoryEvent.EventTypeOneofCase.EntityOperationFailed: + historyEvent = EntityConversions.EncodeOperationFailed(proto); + break; + case P.HistoryEvent.EventTypeOneofCase.GenericEvent: + historyEvent = new GenericEvent(proto.EventId, proto.GenericEvent.Data); + break; + case P.HistoryEvent.EventTypeOneofCase.HistoryState: + historyEvent = new HistoryStateEvent( + proto.EventId, + new OrchestrationState + { + OrchestrationInstance = new OrchestrationInstance + { + InstanceId = proto.HistoryState.OrchestrationState.InstanceId, + }, + Name = proto.HistoryState.OrchestrationState.Name, + Version = proto.HistoryState.OrchestrationState.Version, + ScheduledStartTime = proto.HistoryState.OrchestrationState.ScheduledStartTimestamp.ToDateTime(), + CreatedTime = proto.HistoryState.OrchestrationState.CreatedTimestamp.ToDateTime(), + LastUpdatedTime = proto.HistoryState.OrchestrationState.LastUpdatedTimestamp.ToDateTime(), + Input = proto.HistoryState.OrchestrationState.Input, + Output = proto.HistoryState.OrchestrationState.Output, + Status = proto.HistoryState.OrchestrationState.CustomStatus, + Tags = proto.HistoryState.OrchestrationState.Tags, + }); + break; case P.HistoryEvent.EventTypeOneofCase.ExecutionRewound: historyEvent = new ExecutionRewoundEvent(proto.EventId); break; - default: - throw new NotSupportedException($"Deserialization of {proto.EventTypeCase} is not supported."); - } - - historyEvent.Timestamp = proto.Timestamp.ToDateTime(); - return historyEvent; - } - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp ToTimestamp(this DateTime dateTime) - { - // The protobuf libraries require timestamps to be in UTC - if (dateTime.Kind == DateTimeKind.Unspecified) - { - dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); - } - else if (dateTime.Kind == DateTimeKind.Local) - { - dateTime = dateTime.ToUniversalTime(); - } - - return Timestamp.FromDateTime(dateTime); - } - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp? ToTimestamp(this DateTime? dateTime) - => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp ToTimestamp(this DateTimeOffset dateTime) => Timestamp.FromDateTimeOffset(dateTime); - - /// - /// Converts a to a gRPC . - /// - /// The date-time to convert. - /// The gRPC timestamp. - internal static Timestamp? ToTimestamp(this DateTimeOffset? dateTime) - => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; - - /// - /// Constructs a . - /// - /// The orchestrator instance ID. - /// The orchestrator execution ID. - /// The orchestrator customer status or null if no custom status. - /// The orchestrator actions. - /// - /// The completion token for the work item. It must be the exact same - /// value that was provided by the corresponding that triggered the orchestrator execution. - /// - /// The entity conversion state, or null if no conversion is required. - /// The that represents orchestration execution. - /// Whether or not a history is required to complete the orchestration request and none was provided. - /// The orchestrator response. - /// When an orchestrator action is unknown. - internal static P.OrchestratorResponse ConstructOrchestratorResponse( - string instanceId, - string executionId, - string? customStatus, - IEnumerable? actions, - string completionToken, - EntityConversionState? entityConversionState, - Activity? orchestrationActivity, - bool requiresHistory = false) - { - var response = new P.OrchestratorResponse - { - InstanceId = instanceId, - CustomStatus = customStatus, - CompletionToken = completionToken, - OrchestrationTraceContext = - new() - { - SpanID = orchestrationActivity?.SpanId.ToString(), - SpanStartTime = orchestrationActivity?.StartTimeUtc.ToTimestamp(), - }, - RequiresHistory = requiresHistory, - }; - - // If a history is required and the orchestration request was not completed, then there is no list of actions. - if (requiresHistory) - { - return response; - } - - Check.NotNull(actions); - foreach (OrchestratorAction action in actions) - { - var protoAction = new P.OrchestratorAction { Id = action.Id }; - - P.TraceContext? CreateTraceContext() - { - if (orchestrationActivity is null) - { - return null; - } - - ActivitySpanId clientSpanId = ActivitySpanId.CreateRandom(); - ActivityContext clientActivityContext = new(orchestrationActivity.TraceId, clientSpanId, orchestrationActivity.ActivityTraceFlags, orchestrationActivity.TraceStateString); - - return new P.TraceContext - { - TraceParent = $"00-{clientActivityContext.TraceId}-{clientActivityContext.SpanId}-0{clientActivityContext.TraceFlags:d}", - TraceState = clientActivityContext.TraceState, - }; - } - - switch (action.OrchestratorActionType) - { - case OrchestratorActionType.ScheduleOrchestrator: - var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; - - protoAction.ScheduleTask = new P.ScheduleTaskAction - { - Name = scheduleTaskAction.Name, - Version = scheduleTaskAction.Version, - Input = scheduleTaskAction.Input, - ParentTraceContext = CreateTraceContext(), + default: + throw new NotSupportedException($"Deserialization of {proto.EventTypeCase} is not supported."); + } + + historyEvent.Timestamp = proto.Timestamp.ToDateTime(); + return historyEvent; + } + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp ToTimestamp(this DateTime dateTime) + { + // The protobuf libraries require timestamps to be in UTC + if (dateTime.Kind == DateTimeKind.Unspecified) + { + dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + } + else if (dateTime.Kind == DateTimeKind.Local) + { + dateTime = dateTime.ToUniversalTime(); + } + + return Timestamp.FromDateTime(dateTime); + } + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp? ToTimestamp(this DateTime? dateTime) + => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp ToTimestamp(this DateTimeOffset dateTime) => Timestamp.FromDateTimeOffset(dateTime); + + /// + /// Converts a to a gRPC . + /// + /// The date-time to convert. + /// The gRPC timestamp. + internal static Timestamp? ToTimestamp(this DateTimeOffset? dateTime) + => dateTime.HasValue ? dateTime.Value.ToTimestamp() : null; + + /// + /// Constructs a . + /// + /// The orchestrator instance ID. + /// The orchestrator execution ID. + /// The orchestrator customer status or null if no custom status. + /// The orchestrator actions. + /// + /// The completion token for the work item. It must be the exact same + /// value that was provided by the corresponding that triggered the orchestrator execution. + /// + /// The entity conversion state, or null if no conversion is required. + /// The that represents orchestration execution. + /// Whether or not a history is required to complete the orchestration request and none was provided. + /// The orchestrator response. + /// When an orchestrator action is unknown. + internal static P.OrchestratorResponse ConstructOrchestratorResponse( + string instanceId, + string executionId, + string? customStatus, + IEnumerable? actions, + string completionToken, + EntityConversionState? entityConversionState, + Activity? orchestrationActivity, + bool requiresHistory = false) + { + var response = new P.OrchestratorResponse + { + InstanceId = instanceId, + CustomStatus = customStatus, + CompletionToken = completionToken, + OrchestrationTraceContext = + new() + { + SpanID = orchestrationActivity?.SpanId.ToString(), + SpanStartTime = orchestrationActivity?.StartTimeUtc.ToTimestamp(), + }, + RequiresHistory = requiresHistory, + }; + + // If a history is required and the orchestration request was not completed, then there is no list of actions. + if (requiresHistory) + { + return response; + } + + Check.NotNull(actions); + foreach (OrchestratorAction action in actions) + { + var protoAction = new P.OrchestratorAction { Id = action.Id }; + + P.TraceContext? CreateTraceContext() + { + if (orchestrationActivity is null) + { + return null; + } + + ActivitySpanId clientSpanId = ActivitySpanId.CreateRandom(); + ActivityContext clientActivityContext = new(orchestrationActivity.TraceId, clientSpanId, orchestrationActivity.ActivityTraceFlags, orchestrationActivity.TraceStateString); + + return new P.TraceContext + { + TraceParent = $"00-{clientActivityContext.TraceId}-{clientActivityContext.SpanId}-0{clientActivityContext.TraceFlags:d}", + TraceState = clientActivityContext.TraceState, + }; + } + + switch (action.OrchestratorActionType) + { + case OrchestratorActionType.ScheduleOrchestrator: + var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; + + protoAction.ScheduleTask = new P.ScheduleTaskAction + { + Name = scheduleTaskAction.Name, + Version = scheduleTaskAction.Version, + Input = scheduleTaskAction.Input, + ParentTraceContext = CreateTraceContext(), + }; + + if (scheduleTaskAction.Tags != null) + { + foreach (KeyValuePair tag in scheduleTaskAction.Tags) + { + protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; + } + } + + break; + case OrchestratorActionType.CreateSubOrchestration: + var subOrchestrationAction = (CreateSubOrchestrationAction)action; + protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction + { + Input = subOrchestrationAction.Input, + InstanceId = subOrchestrationAction.InstanceId, + Name = subOrchestrationAction.Name, + Version = subOrchestrationAction.Version, + ParentTraceContext = CreateTraceContext(), }; - if (scheduleTaskAction.Tags != null) - { - foreach (KeyValuePair tag in scheduleTaskAction.Tags) - { - protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; - } + if (subOrchestrationAction.Tags != null) + { + foreach (KeyValuePair tag in subOrchestrationAction.Tags) + { + protoAction.CreateSubOrchestration.Tags[tag.Key] = tag.Value; + } } - - break; - case OrchestratorActionType.CreateSubOrchestration: - var subOrchestrationAction = (CreateSubOrchestrationAction)action; - protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction + + break; + case OrchestratorActionType.CreateTimer: + var createTimerAction = (CreateTimerOrchestratorAction)action; + protoAction.CreateTimer = new P.CreateTimerAction + { + FireAt = createTimerAction.FireAt.ToTimestamp(), + }; + break; + case OrchestratorActionType.SendEvent: + var sendEventAction = (SendEventOrchestratorAction)action; + if (sendEventAction.Instance == null) + { + throw new ArgumentException( + $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); + } + + if (entityConversionState is not null + && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) + && sendEventAction.EventName is not null + && sendEventAction.EventData is not null) + { + P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); + protoAction.SendEntityMessage = sendAction; + + EntityConversions.DecodeEntityMessageAction( + sendEventAction.EventName, + sendEventAction.EventData, + sendEventAction.Instance.InstanceId, + sendAction, + out string requestId); + + entityConversionState.EntityRequestIds.Add(requestId); + sendAction.ParentTraceContext = CreateTraceContext(); + + switch (sendAction.EntityMessageTypeCase) + { + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: + entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); + break; + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: + entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); + break; + default: + break; + } + } + else + { + protoAction.SendEvent = new P.SendEventAction + { + Instance = sendEventAction.Instance.ToProtobuf(), + Name = sendEventAction.EventName, + Data = sendEventAction.EventData, + }; + + // Distributed Tracing: start a new trace activity derived from the orchestration + // for an EventRaisedEvent (external event) + using Activity? traceActivity = TraceHelper.StartTraceActivityForEventRaisedFromWorker(sendEventAction, instanceId, executionId); + + traceActivity?.Stop(); + } + + break; + case OrchestratorActionType.OrchestrationComplete: + + if (entityConversionState is not null) + { + // as a precaution, unlock any entities that were not unlocked for some reason, before + // completing the orchestration. + foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) + { + response.Actions.Add(new P.OrchestratorAction + { + Id = action.Id, + SendEntityMessage = new P.SendEntityMessageAction + { + EntityUnlockSent = new P.EntityUnlockSentEvent + { + CriticalSectionId = criticalSectionId, + TargetInstanceId = target, + ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, + }, + }, + }); + } + } + + var completeAction = (OrchestrationCompleteOrchestratorAction)action; + protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction { - Input = subOrchestrationAction.Input, - InstanceId = subOrchestrationAction.InstanceId, - Name = subOrchestrationAction.Name, - Version = subOrchestrationAction.Version, - ParentTraceContext = CreateTraceContext(), - }; - - if (subOrchestrationAction.Tags != null) - { - foreach (KeyValuePair tag in subOrchestrationAction.Tags) - { - protoAction.CreateSubOrchestration.Tags[tag.Key] = tag.Value; - } - } - - break; - case OrchestratorActionType.CreateTimer: - var createTimerAction = (CreateTimerOrchestratorAction)action; - protoAction.CreateTimer = new P.CreateTimerAction - { - FireAt = createTimerAction.FireAt.ToTimestamp(), - }; - break; - case OrchestratorActionType.SendEvent: - var sendEventAction = (SendEventOrchestratorAction)action; - if (sendEventAction.Instance == null) - { - throw new ArgumentException( - $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); - } - - if (entityConversionState is not null - && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) - && sendEventAction.EventName is not null - && sendEventAction.EventData is not null) - { - P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); - protoAction.SendEntityMessage = sendAction; - - EntityConversions.DecodeEntityMessageAction( - sendEventAction.EventName, - sendEventAction.EventData, - sendEventAction.Instance.InstanceId, - sendAction, - out string requestId); - - sendAction.ParentTraceContext = CreateTraceContext(); - - entityConversionState.EntityRequestIds.Add(requestId); - - switch (sendAction.EntityMessageTypeCase) - { - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: - entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); - break; - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: - entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); - break; - default: - break; - } - } - else - { - protoAction.SendEvent = new P.SendEventAction - { - Instance = sendEventAction.Instance.ToProtobuf(), - Name = sendEventAction.EventName, - Data = sendEventAction.EventData, - }; - - // Distributed Tracing: start a new trace activity derived from the orchestration - // for an EventRaisedEvent (external event) - using Activity? traceActivity = TraceHelper.StartTraceActivityForEventRaisedFromWorker(sendEventAction, instanceId, executionId); - - traceActivity?.Stop(); - } - - break; - case OrchestratorActionType.OrchestrationComplete: - - if (entityConversionState is not null) - { - // as a precaution, unlock any entities that were not unlocked for some reason, before - // completing the orchestration. - foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) - { - response.Actions.Add(new P.OrchestratorAction - { - Id = action.Id, - SendEntityMessage = new P.SendEntityMessageAction - { - EntityUnlockSent = new P.EntityUnlockSentEvent - { - CriticalSectionId = criticalSectionId, - TargetInstanceId = target, - ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, - }, - }, - }); - } - } - - var completeAction = (OrchestrationCompleteOrchestratorAction)action; - protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction - { - CarryoverEvents = { completeAction.CarryoverEvents.Select(ToProtobuf) }, - Details = completeAction.Details, - NewVersion = completeAction.NewVersion, - OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), - Result = completeAction.Result, + CarryoverEvents = { completeAction.CarryoverEvents.Select(ToProtobuf) }, + Details = completeAction.Details, + NewVersion = completeAction.NewVersion, + OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), + Result = completeAction.Result, }; foreach (KeyValuePair tag in completeAction.Tags) { protoAction.CompleteOrchestration.Tags[tag.Key] = tag.Value; - } - - if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) - { - protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); - } - - break; - default: - throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); - } - - response.Actions.Add(protoAction); - } - - return response; - } - - /// - /// Converts a to a . - /// - /// The status to convert. - /// The converted status. - internal static OrchestrationStatus ToCore(this P.OrchestrationStatus status) - { - return (OrchestrationStatus)status; - } - - /// - /// Converts a to a . - /// - /// The status to convert. - /// The converted status. - [return: NotNullIfNotNull(nameof(status))] - internal static OrchestrationInstance? ToCore(this P.OrchestrationInstance? status) - { - if (status == null) - { - return null; - } - - return new OrchestrationInstance - { - InstanceId = status.InstanceId, - ExecutionId = status.ExecutionId, - }; - } - - /// - /// Converts a to a . - /// - /// The failure details to convert. - /// The converted failure details. - [return: NotNullIfNotNull(nameof(failureDetails))] - internal static TaskFailureDetails? ToTaskFailureDetails(this P.TaskFailureDetails? failureDetails) - { - if (failureDetails == null) - { - return null; - } - - return new TaskFailureDetails( - failureDetails.ErrorType, - failureDetails.ErrorMessage, - failureDetails.StackTrace, - failureDetails.InnerFailure.ToTaskFailureDetails(), - ConvertProperties(failureDetails.Properties)); - } - - /// - /// Converts a to . - /// - /// The exception to convert. - /// Optional exception properties provider. - /// The task failure details. - [return: NotNullIfNotNull(nameof(e))] - internal static P.TaskFailureDetails? ToTaskFailureDetails(this Exception? e, DTCore.IExceptionPropertiesProvider? exceptionPropertiesProvider = null) - { - if (e == null) - { - return null; - } - - IDictionary? properties = exceptionPropertiesProvider?.GetExceptionProperties(e); - - var taskFailureDetails = new P.TaskFailureDetails - { - ErrorType = e.GetType().FullName, - ErrorMessage = e.Message, - StackTrace = e.StackTrace, - InnerFailure = e.InnerException.ToTaskFailureDetails(exceptionPropertiesProvider), - }; - - if (properties != null) - { - foreach (var kvp in properties) - { - taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); - } - } - - return taskFailureDetails; - } - - /// - /// Converts a to a . - /// - /// The entity batch request to convert. - /// The converted entity batch request. - [return: NotNullIfNotNull(nameof(entityBatchRequest))] - internal static EntityBatchRequest? ToEntityBatchRequest(this P.EntityBatchRequest? entityBatchRequest) - { - if (entityBatchRequest == null) - { - return null; - } - - return new EntityBatchRequest() - { - EntityState = entityBatchRequest.EntityState, - InstanceId = entityBatchRequest.InstanceId, - Operations = entityBatchRequest.Operations.Select(r => r.ToOperationRequest()).ToList(), - }; - } - - /// - /// Converts a to a . - /// - /// The entity request to convert. - /// The converted request. - /// Additional info about each operation, required by DTS. - internal static void ToEntityBatchRequest( - this P.EntityRequest entityRequest, - out EntityBatchRequest batchRequest, - out List operationInfos) - { - batchRequest = new EntityBatchRequest() - { - EntityState = entityRequest.EntityState, - InstanceId = entityRequest.InstanceId, - Operations = [], // operations are added to this collection below - }; - - operationInfos = new(entityRequest.OperationRequests.Count); - - foreach (P.HistoryEvent? op in entityRequest.OperationRequests) - { - if (op.EntityOperationSignaled is not null) - { - batchRequest.Operations.Add(new OperationRequest - { - Id = Guid.Parse(op.EntityOperationSignaled.RequestId), - Operation = op.EntityOperationSignaled.Operation, - Input = op.EntityOperationSignaled.Input, - TraceContext = op.EntityOperationSignaled.ParentTraceContext != null - ? new DistributedTraceContext( - op.EntityOperationSignaled.ParentTraceContext.TraceParent, - op.EntityOperationSignaled.ParentTraceContext.TraceState) - : null, - }); - operationInfos.Add(new P.OperationInfo - { - RequestId = op.EntityOperationSignaled.RequestId, - ResponseDestination = null, // means we don't send back a response to the caller - }); - } - else if (op.EntityOperationCalled is not null) - { - batchRequest.Operations.Add(new OperationRequest - { - Id = Guid.Parse(op.EntityOperationCalled.RequestId), - Operation = op.EntityOperationCalled.Operation, - Input = op.EntityOperationCalled.Input, - TraceContext = op.EntityOperationCalled.ParentTraceContext != null - ? new DistributedTraceContext( - op.EntityOperationCalled.ParentTraceContext.TraceParent, - op.EntityOperationCalled.ParentTraceContext.TraceState) - : null, - }); - operationInfos.Add(new P.OperationInfo - { - RequestId = op.EntityOperationCalled.RequestId, - ResponseDestination = new P.OrchestrationInstance - { - InstanceId = op.EntityOperationCalled.ParentInstanceId, - ExecutionId = op.EntityOperationCalled.ParentExecutionId, - }, - }); - } - } - } - - /// - /// Converts a to a . - /// - /// The operation request to convert. - /// The converted operation request. - [return: NotNullIfNotNull(nameof(operationRequest))] - internal static OperationRequest? ToOperationRequest(this P.OperationRequest? operationRequest) - { - if (operationRequest == null) - { - return null; - } - - return new OperationRequest() - { - Operation = operationRequest.Operation, - Input = operationRequest.Input, - Id = Guid.Parse(operationRequest.RequestId), - TraceContext = operationRequest.TraceContext != null ? - new DistributedTraceContext( - operationRequest.TraceContext.TraceParent, - operationRequest.TraceContext.TraceState) : null, - }; - } - - /// - /// Converts a to a . - /// - /// The operation result to convert. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(operationResult))] - internal static OperationResult? ToOperationResult(this P.OperationResult? operationResult) - { - if (operationResult == null) - { - return null; - } - - switch (operationResult.ResultTypeCase) - { - case P.OperationResult.ResultTypeOneofCase.Success: - return new OperationResult() - { - Result = operationResult.Success.Result, - StartTimeUtc = operationResult.Success.StartTimeUtc?.ToDateTime(), - EndTimeUtc = operationResult.Success.EndTimeUtc?.ToDateTime(), - }; - - case P.OperationResult.ResultTypeOneofCase.Failure: - return new OperationResult() - { - FailureDetails = operationResult.Failure.FailureDetails.ToCore(), - StartTimeUtc = operationResult.Failure.StartTimeUtc?.ToDateTime(), - EndTimeUtc = operationResult.Failure.EndTimeUtc?.ToDateTime(), - }; - - default: - throw new NotSupportedException($"Deserialization of {operationResult.ResultTypeCase} is not supported."); - } - } - - /// - /// Converts a to . - /// - /// The operation result to convert. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(operationResult))] - internal static P.OperationResult? ToOperationResult(this OperationResult? operationResult) - { - if (operationResult == null) - { - return null; - } - - if (operationResult.FailureDetails == null) - { - return new P.OperationResult() - { - Success = new P.OperationResultSuccess() - { - Result = operationResult.Result, - StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), - EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), - }, - }; - } - else - { - return new P.OperationResult() - { - Failure = new P.OperationResultFailure() - { - FailureDetails = ToProtobuf(operationResult.FailureDetails), - StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), - EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), - }, - }; - } - } - - /// - /// Converts a to a . - /// - /// The operation action to convert. - /// The converted operation action. - [return: NotNullIfNotNull(nameof(operationAction))] - internal static OperationAction? ToOperationAction(this P.OperationAction? operationAction) - { - if (operationAction == null) - { - return null; - } - - switch (operationAction.OperationActionTypeCase) - { - case P.OperationAction.OperationActionTypeOneofCase.SendSignal: - - return new SendSignalOperationAction() - { - Name = operationAction.SendSignal.Name, - Input = operationAction.SendSignal.Input, - InstanceId = operationAction.SendSignal.InstanceId, - ScheduledTime = operationAction.SendSignal.ScheduledTime?.ToDateTime(), - RequestTime = operationAction.SendSignal.RequestTime?.ToDateTimeOffset(), - ParentTraceContext = operationAction.SendSignal.ParentTraceContext != null ? - new DistributedTraceContext( - operationAction.SendSignal.ParentTraceContext.TraceParent, - operationAction.SendSignal.ParentTraceContext.TraceState) : null, - }; - - case P.OperationAction.OperationActionTypeOneofCase.StartNewOrchestration: - - return new StartNewOrchestrationOperationAction() - { - Name = operationAction.StartNewOrchestration.Name, - Input = operationAction.StartNewOrchestration.Input, - InstanceId = operationAction.StartNewOrchestration.InstanceId, - Version = operationAction.StartNewOrchestration.Version, - ScheduledStartTime = operationAction.StartNewOrchestration.ScheduledTime?.ToDateTime(), - RequestTime = operationAction.StartNewOrchestration.RequestTime?.ToDateTimeOffset(), - ParentTraceContext = operationAction.StartNewOrchestration.ParentTraceContext != null ? - new DistributedTraceContext( - operationAction.StartNewOrchestration.ParentTraceContext.TraceParent, - operationAction.StartNewOrchestration.ParentTraceContext.TraceState) : null, - }; - default: - throw new NotSupportedException($"Deserialization of {operationAction.OperationActionTypeCase} is not supported."); - } - } - - /// - /// Converts a to . - /// - /// The operation action to convert. - /// The converted operation action. - [return: NotNullIfNotNull(nameof(operationAction))] - internal static P.OperationAction? ToOperationAction(this OperationAction? operationAction) - { - if (operationAction == null) - { - return null; - } - - var action = new P.OperationAction(); - - switch (operationAction) - { - case SendSignalOperationAction sendSignalAction: - - action.SendSignal = new P.SendSignalAction() - { - Name = sendSignalAction.Name, - Input = sendSignalAction.Input, - InstanceId = sendSignalAction.InstanceId, - ScheduledTime = sendSignalAction.ScheduledTime?.ToTimestamp(), - RequestTime = sendSignalAction.RequestTime?.ToTimestamp(), - ParentTraceContext = sendSignalAction.ParentTraceContext != null ? - new P.TraceContext - { - TraceParent = sendSignalAction.ParentTraceContext.TraceParent, - TraceState = sendSignalAction.ParentTraceContext.TraceState, - } - : null, - }; - break; - - case StartNewOrchestrationOperationAction startNewOrchestrationAction: - - action.StartNewOrchestration = new P.StartNewOrchestrationAction() - { - Name = startNewOrchestrationAction.Name, - Input = startNewOrchestrationAction.Input, - Version = startNewOrchestrationAction.Version, - InstanceId = startNewOrchestrationAction.InstanceId, - ScheduledTime = startNewOrchestrationAction.ScheduledStartTime?.ToTimestamp(), - RequestTime = startNewOrchestrationAction.RequestTime?.ToTimestamp(), - ParentTraceContext = startNewOrchestrationAction.ParentTraceContext != null ? - new P.TraceContext - { - TraceParent = startNewOrchestrationAction.ParentTraceContext.TraceParent, - TraceState = startNewOrchestrationAction.ParentTraceContext.TraceState, - } - : null, - }; - break; - } - - return action; - } - - /// - /// Converts a to a . - /// - /// The operation result to convert. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(entityBatchResult))] - internal static EntityBatchResult? ToEntityBatchResult(this P.EntityBatchResult? entityBatchResult) - { - if (entityBatchResult == null) - { - return null; - } - - return new EntityBatchResult() - { - Actions = entityBatchResult.Actions.Select(operationAction => operationAction!.ToOperationAction()).ToList(), - EntityState = entityBatchResult.EntityState, - Results = entityBatchResult.Results.Select(operationResult => operationResult!.ToOperationResult()).ToList(), - FailureDetails = entityBatchResult.FailureDetails.ToCore(), - }; - } - - /// - /// Converts a to . - /// - /// The operation result to convert. - /// The completion token, or null for the older protocol. - /// Additional information about each operation, required by DTS. - /// The converted operation result. - [return: NotNullIfNotNull(nameof(entityBatchResult))] - internal static P.EntityBatchResult? ToEntityBatchResult( - this EntityBatchResult? entityBatchResult, - string? completionToken = null, - IEnumerable? operationInfos = null) - { - if (entityBatchResult == null) - { - return null; - } - - return new P.EntityBatchResult() - { - EntityState = entityBatchResult.EntityState, - FailureDetails = entityBatchResult.FailureDetails.ToProtobuf(), - Actions = { entityBatchResult.Actions?.Select(a => a.ToOperationAction()) ?? [] }, - Results = { entityBatchResult.Results?.Select(a => a.ToOperationResult()) ?? [] }, - CompletionToken = completionToken ?? string.Empty, - OperationInfos = { operationInfos ?? [] }, - }; - } - - /// - /// Converts the gRPC representation of orchestrator entity parameters to the DT.Core representation. - /// - /// The DT.Core representation. - /// The gRPC representation. - [return: NotNullIfNotNull(nameof(parameters))] - internal static TaskOrchestrationEntityParameters? ToCore(this P.OrchestratorEntityParameters? parameters) - { - if (parameters == null) - { - return null; - } - - return new TaskOrchestrationEntityParameters() - { - EntityMessageReorderWindow = parameters.EntityMessageReorderWindow.ToTimeSpan(), - }; - } - - /// - /// Gets the approximate byte count for a . - /// - /// The failure details. - /// The approximate byte count. - internal static int GetApproximateByteCount(this P.TaskFailureDetails failureDetails) - { - // Protobuf strings are always UTF-8: https://developers.google.com/protocol-buffers/docs/proto3#scalar - Encoding encoding = Encoding.UTF8; - - int byteCount = 0; - if (failureDetails.ErrorType != null) - { - byteCount += encoding.GetByteCount(failureDetails.ErrorType); - } - - if (failureDetails.ErrorMessage != null) - { - byteCount += encoding.GetByteCount(failureDetails.ErrorMessage); - } - - if (failureDetails.StackTrace != null) - { - byteCount += encoding.GetByteCount(failureDetails.StackTrace); - } - - if (failureDetails.InnerFailure != null) - { - byteCount += failureDetails.InnerFailure.GetApproximateByteCount(); - } - - return byteCount; - } - - /// - /// Decode a protobuf message from a base64 string. - /// - /// The type to decode to. - /// The message parser. - /// The base64 encoded message. - /// The decoded message. - /// If decoding fails. - internal static T Base64Decode(this MessageParser parser, string encodedMessage) where T : IMessage - { - // Decode the base64 in a way that doesn't allocate a byte[] on each request - int encodedByteCount = Encoding.UTF8.GetByteCount(encodedMessage); - byte[] buffer = ArrayPool.Shared.Rent(encodedByteCount); - try - { - // The Base64 APIs require first converting the string into UTF-8 bytes. We then - // do an in-place conversion from base64 UTF-8 bytes to protobuf bytes so that - // we can finally decode the protobuf request. - Encoding.UTF8.GetBytes(encodedMessage, 0, encodedMessage.Length, buffer, 0); - OperationStatus status = Base64.DecodeFromUtf8InPlace( - buffer.AsSpan(0, encodedByteCount), - out int bytesWritten); - if (status != OperationStatus.Done) - { - throw new ArgumentException( - $"Failed to base64-decode the '{typeof(T).Name}' payload: {status}", nameof(encodedMessage)); - } - - return (T)parser.ParseFrom(buffer, 0, bytesWritten); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - /// - /// Converts a grpc to a . - /// - /// The failure details to convert. - /// The converted failure details. - internal static FailureDetails? ToCore(this P.TaskFailureDetails? failureDetails) - { - if (failureDetails == null) - { - return null; - } - - return new FailureDetails( - failureDetails.ErrorType, - failureDetails.ErrorMessage, - failureDetails.StackTrace, - failureDetails.InnerFailure.ToCore(), - failureDetails.IsNonRetriable, - ConvertProperties(failureDetails.Properties)); - } - - /// - /// Converts a instance to a corresponding C# object. - /// - /// The Protobuf Value to convert. - /// The corresponding C# object. - /// - /// Thrown when the Protobuf Value.KindCase is not one of the supported types. - /// - internal static object? ConvertValueToObject(Google.Protobuf.WellKnownTypes.Value value) - { - switch (value.KindCase) - { - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NullValue: - return null; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NumberValue: - return value.NumberValue; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StringValue: + } + + if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) + { + protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); + } + + break; + default: + throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); + } + + response.Actions.Add(protoAction); + } + + return response; + } + + /// + /// Converts a to a . + /// + /// The status to convert. + /// The converted status. + internal static OrchestrationStatus ToCore(this P.OrchestrationStatus status) + { + return (OrchestrationStatus)status; + } + + /// + /// Converts a to a . + /// + /// The status to convert. + /// The converted status. + [return: NotNullIfNotNull(nameof(status))] + internal static OrchestrationInstance? ToCore(this P.OrchestrationInstance? status) + { + if (status == null) + { + return null; + } + + return new OrchestrationInstance + { + InstanceId = status.InstanceId, + ExecutionId = status.ExecutionId, + }; + } + + /// + /// Converts a to a . + /// + /// The failure details to convert. + /// The converted failure details. + [return: NotNullIfNotNull(nameof(failureDetails))] + internal static TaskFailureDetails? ToTaskFailureDetails(this P.TaskFailureDetails? failureDetails) + { + if (failureDetails == null) + { + return null; + } + + return new TaskFailureDetails( + failureDetails.ErrorType, + failureDetails.ErrorMessage, + failureDetails.StackTrace, + failureDetails.InnerFailure.ToTaskFailureDetails(), + ConvertProperties(failureDetails.Properties)); + } + + /// + /// Converts a to . + /// + /// The exception to convert. + /// Optional exception properties provider. + /// The task failure details. + [return: NotNullIfNotNull(nameof(e))] + internal static P.TaskFailureDetails? ToTaskFailureDetails(this Exception? e, DTCore.IExceptionPropertiesProvider? exceptionPropertiesProvider = null) + { + if (e == null) + { + return null; + } + + IDictionary? properties = exceptionPropertiesProvider?.GetExceptionProperties(e); + + var taskFailureDetails = new P.TaskFailureDetails + { + ErrorType = e.GetType().FullName, + ErrorMessage = e.Message, + StackTrace = e.StackTrace, + InnerFailure = e.InnerException.ToTaskFailureDetails(exceptionPropertiesProvider), + }; + + if (properties != null) + { + foreach (var kvp in properties) + { + taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); + } + } + + return taskFailureDetails; + } + + /// + /// Converts a to a . + /// + /// The entity batch request to convert. + /// The converted entity batch request. + [return: NotNullIfNotNull(nameof(entityBatchRequest))] + internal static EntityBatchRequest? ToEntityBatchRequest(this P.EntityBatchRequest? entityBatchRequest) + { + if (entityBatchRequest == null) + { + return null; + } + + return new EntityBatchRequest() + { + EntityState = entityBatchRequest.EntityState, + InstanceId = entityBatchRequest.InstanceId, + Operations = entityBatchRequest.Operations.Select(r => r.ToOperationRequest()).ToList(), + }; + } + + /// + /// Converts a to a . + /// + /// The entity request to convert. + /// The converted request. + /// Additional info about each operation, required by DTS. + internal static void ToEntityBatchRequest( + this P.EntityRequest entityRequest, + out EntityBatchRequest batchRequest, + out List operationInfos) + { + batchRequest = new EntityBatchRequest() + { + EntityState = entityRequest.EntityState, + InstanceId = entityRequest.InstanceId, + Operations = [], // operations are added to this collection below + }; + + operationInfos = new(entityRequest.OperationRequests.Count); + + foreach (P.HistoryEvent? op in entityRequest.OperationRequests) + { + if (op.EntityOperationSignaled is not null) + { + batchRequest.Operations.Add(new OperationRequest + { + Id = Guid.Parse(op.EntityOperationSignaled.RequestId), + Operation = op.EntityOperationSignaled.Operation, + Input = op.EntityOperationSignaled.Input, + TraceContext = op.EntityOperationSignaled.ParentTraceContext is { } signalTc + ? new DistributedTraceContext(signalTc.TraceParent, signalTc.TraceState) + : null, + }); + operationInfos.Add(new P.OperationInfo + { + RequestId = op.EntityOperationSignaled.RequestId, + ResponseDestination = null, // means we don't send back a response to the caller + }); + } + else if (op.EntityOperationCalled is not null) + { + batchRequest.Operations.Add(new OperationRequest + { + Id = Guid.Parse(op.EntityOperationCalled.RequestId), + Operation = op.EntityOperationCalled.Operation, + Input = op.EntityOperationCalled.Input, + TraceContext = op.EntityOperationCalled.ParentTraceContext is { } calledTc + ? new DistributedTraceContext(calledTc.TraceParent, calledTc.TraceState) + : null, + }); + operationInfos.Add(new P.OperationInfo + { + RequestId = op.EntityOperationCalled.RequestId, + ResponseDestination = new P.OrchestrationInstance + { + InstanceId = op.EntityOperationCalled.ParentInstanceId, + ExecutionId = op.EntityOperationCalled.ParentExecutionId, + }, + }); + } + } + } + + /// + /// Converts a to a . + /// + /// The operation request to convert. + /// The converted operation request. + [return: NotNullIfNotNull(nameof(operationRequest))] + internal static OperationRequest? ToOperationRequest(this P.OperationRequest? operationRequest) + { + if (operationRequest == null) + { + return null; + } + + return new OperationRequest() + { + Operation = operationRequest.Operation, + Input = operationRequest.Input, + Id = Guid.Parse(operationRequest.RequestId), + TraceContext = operationRequest.TraceContext != null ? + new DistributedTraceContext( + operationRequest.TraceContext.TraceParent, + operationRequest.TraceContext.TraceState) : null, + }; + } + + /// + /// Converts a to a . + /// + /// The operation result to convert. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(operationResult))] + internal static OperationResult? ToOperationResult(this P.OperationResult? operationResult) + { + if (operationResult == null) + { + return null; + } + + switch (operationResult.ResultTypeCase) + { + case P.OperationResult.ResultTypeOneofCase.Success: + return new OperationResult() + { + Result = operationResult.Success.Result, + StartTimeUtc = operationResult.Success.StartTimeUtc?.ToDateTime(), + EndTimeUtc = operationResult.Success.EndTimeUtc?.ToDateTime(), + }; + + case P.OperationResult.ResultTypeOneofCase.Failure: + return new OperationResult() + { + FailureDetails = operationResult.Failure.FailureDetails.ToCore(), + StartTimeUtc = operationResult.Failure.StartTimeUtc?.ToDateTime(), + EndTimeUtc = operationResult.Failure.EndTimeUtc?.ToDateTime(), + }; + + default: + throw new NotSupportedException($"Deserialization of {operationResult.ResultTypeCase} is not supported."); + } + } + + /// + /// Converts a to . + /// + /// The operation result to convert. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(operationResult))] + internal static P.OperationResult? ToOperationResult(this OperationResult? operationResult) + { + if (operationResult == null) + { + return null; + } + + if (operationResult.FailureDetails == null) + { + return new P.OperationResult() + { + Success = new P.OperationResultSuccess() + { + Result = operationResult.Result, + StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), + EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), + }, + }; + } + else + { + return new P.OperationResult() + { + Failure = new P.OperationResultFailure() + { + FailureDetails = ToProtobuf(operationResult.FailureDetails), + StartTimeUtc = operationResult.StartTimeUtc?.ToTimestamp(), + EndTimeUtc = operationResult.EndTimeUtc?.ToTimestamp(), + }, + }; + } + } + + /// + /// Converts a to a . + /// + /// The operation action to convert. + /// The converted operation action. + [return: NotNullIfNotNull(nameof(operationAction))] + internal static OperationAction? ToOperationAction(this P.OperationAction? operationAction) + { + if (operationAction == null) + { + return null; + } + + switch (operationAction.OperationActionTypeCase) + { + case P.OperationAction.OperationActionTypeOneofCase.SendSignal: + + return new SendSignalOperationAction() + { + Name = operationAction.SendSignal.Name, + Input = operationAction.SendSignal.Input, + InstanceId = operationAction.SendSignal.InstanceId, + ScheduledTime = operationAction.SendSignal.ScheduledTime?.ToDateTime(), + RequestTime = operationAction.SendSignal.RequestTime?.ToDateTimeOffset(), + ParentTraceContext = operationAction.SendSignal.ParentTraceContext != null ? + new DistributedTraceContext( + operationAction.SendSignal.ParentTraceContext.TraceParent, + operationAction.SendSignal.ParentTraceContext.TraceState) : null, + }; + + case P.OperationAction.OperationActionTypeOneofCase.StartNewOrchestration: + + return new StartNewOrchestrationOperationAction() + { + Name = operationAction.StartNewOrchestration.Name, + Input = operationAction.StartNewOrchestration.Input, + InstanceId = operationAction.StartNewOrchestration.InstanceId, + Version = operationAction.StartNewOrchestration.Version, + ScheduledStartTime = operationAction.StartNewOrchestration.ScheduledTime?.ToDateTime(), + RequestTime = operationAction.StartNewOrchestration.RequestTime?.ToDateTimeOffset(), + ParentTraceContext = operationAction.StartNewOrchestration.ParentTraceContext != null ? + new DistributedTraceContext( + operationAction.StartNewOrchestration.ParentTraceContext.TraceParent, + operationAction.StartNewOrchestration.ParentTraceContext.TraceState) : null, + }; + default: + throw new NotSupportedException($"Deserialization of {operationAction.OperationActionTypeCase} is not supported."); + } + } + + /// + /// Converts a to . + /// + /// The operation action to convert. + /// The converted operation action. + [return: NotNullIfNotNull(nameof(operationAction))] + internal static P.OperationAction? ToOperationAction(this OperationAction? operationAction) + { + if (operationAction == null) + { + return null; + } + + var action = new P.OperationAction(); + + switch (operationAction) + { + case SendSignalOperationAction sendSignalAction: + + action.SendSignal = new P.SendSignalAction() + { + Name = sendSignalAction.Name, + Input = sendSignalAction.Input, + InstanceId = sendSignalAction.InstanceId, + ScheduledTime = sendSignalAction.ScheduledTime?.ToTimestamp(), + RequestTime = sendSignalAction.RequestTime?.ToTimestamp(), + ParentTraceContext = sendSignalAction.ParentTraceContext != null ? + new P.TraceContext + { + TraceParent = sendSignalAction.ParentTraceContext.TraceParent, + TraceState = sendSignalAction.ParentTraceContext.TraceState, + } + : null, + }; + break; + + case StartNewOrchestrationOperationAction startNewOrchestrationAction: + + action.StartNewOrchestration = new P.StartNewOrchestrationAction() + { + Name = startNewOrchestrationAction.Name, + Input = startNewOrchestrationAction.Input, + Version = startNewOrchestrationAction.Version, + InstanceId = startNewOrchestrationAction.InstanceId, + ScheduledTime = startNewOrchestrationAction.ScheduledStartTime?.ToTimestamp(), + RequestTime = startNewOrchestrationAction.RequestTime?.ToTimestamp(), + ParentTraceContext = startNewOrchestrationAction.ParentTraceContext != null ? + new P.TraceContext + { + TraceParent = startNewOrchestrationAction.ParentTraceContext.TraceParent, + TraceState = startNewOrchestrationAction.ParentTraceContext.TraceState, + } + : null, + }; + break; + } + + return action; + } + + /// + /// Converts a to a . + /// + /// The operation result to convert. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(entityBatchResult))] + internal static EntityBatchResult? ToEntityBatchResult(this P.EntityBatchResult? entityBatchResult) + { + if (entityBatchResult == null) + { + return null; + } + + return new EntityBatchResult() + { + Actions = entityBatchResult.Actions.Select(operationAction => operationAction!.ToOperationAction()).ToList(), + EntityState = entityBatchResult.EntityState, + Results = entityBatchResult.Results.Select(operationResult => operationResult!.ToOperationResult()).ToList(), + FailureDetails = entityBatchResult.FailureDetails.ToCore(), + }; + } + + /// + /// Converts a to . + /// + /// The operation result to convert. + /// The completion token, or null for the older protocol. + /// Additional information about each operation, required by DTS. + /// The converted operation result. + [return: NotNullIfNotNull(nameof(entityBatchResult))] + internal static P.EntityBatchResult? ToEntityBatchResult( + this EntityBatchResult? entityBatchResult, + string? completionToken = null, + IEnumerable? operationInfos = null) + { + if (entityBatchResult == null) + { + return null; + } + + return new P.EntityBatchResult() + { + EntityState = entityBatchResult.EntityState, + FailureDetails = entityBatchResult.FailureDetails.ToProtobuf(), + Actions = { entityBatchResult.Actions?.Select(a => a.ToOperationAction()) ?? [] }, + Results = { entityBatchResult.Results?.Select(a => a.ToOperationResult()) ?? [] }, + CompletionToken = completionToken ?? string.Empty, + OperationInfos = { operationInfos ?? [] }, + }; + } + + /// + /// Converts the gRPC representation of orchestrator entity parameters to the DT.Core representation. + /// + /// The DT.Core representation. + /// The gRPC representation. + [return: NotNullIfNotNull(nameof(parameters))] + internal static TaskOrchestrationEntityParameters? ToCore(this P.OrchestratorEntityParameters? parameters) + { + if (parameters == null) + { + return null; + } + + return new TaskOrchestrationEntityParameters() + { + EntityMessageReorderWindow = parameters.EntityMessageReorderWindow.ToTimeSpan(), + }; + } + + /// + /// Gets the approximate byte count for a . + /// + /// The failure details. + /// The approximate byte count. + internal static int GetApproximateByteCount(this P.TaskFailureDetails failureDetails) + { + // Protobuf strings are always UTF-8: https://developers.google.com/protocol-buffers/docs/proto3#scalar + Encoding encoding = Encoding.UTF8; + + int byteCount = 0; + if (failureDetails.ErrorType != null) + { + byteCount += encoding.GetByteCount(failureDetails.ErrorType); + } + + if (failureDetails.ErrorMessage != null) + { + byteCount += encoding.GetByteCount(failureDetails.ErrorMessage); + } + + if (failureDetails.StackTrace != null) + { + byteCount += encoding.GetByteCount(failureDetails.StackTrace); + } + + if (failureDetails.InnerFailure != null) + { + byteCount += failureDetails.InnerFailure.GetApproximateByteCount(); + } + + return byteCount; + } + + /// + /// Decode a protobuf message from a base64 string. + /// + /// The type to decode to. + /// The message parser. + /// The base64 encoded message. + /// The decoded message. + /// If decoding fails. + internal static T Base64Decode(this MessageParser parser, string encodedMessage) where T : IMessage + { + // Decode the base64 in a way that doesn't allocate a byte[] on each request + int encodedByteCount = Encoding.UTF8.GetByteCount(encodedMessage); + byte[] buffer = ArrayPool.Shared.Rent(encodedByteCount); + try + { + // The Base64 APIs require first converting the string into UTF-8 bytes. We then + // do an in-place conversion from base64 UTF-8 bytes to protobuf bytes so that + // we can finally decode the protobuf request. + Encoding.UTF8.GetBytes(encodedMessage, 0, encodedMessage.Length, buffer, 0); + OperationStatus status = Base64.DecodeFromUtf8InPlace( + buffer.AsSpan(0, encodedByteCount), + out int bytesWritten); + if (status != OperationStatus.Done) + { + throw new ArgumentException( + $"Failed to base64-decode the '{typeof(T).Name}' payload: {status}", nameof(encodedMessage)); + } + + return (T)parser.ParseFrom(buffer, 0, bytesWritten); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Converts a grpc to a . + /// + /// The failure details to convert. + /// The converted failure details. + internal static FailureDetails? ToCore(this P.TaskFailureDetails? failureDetails) + { + if (failureDetails == null) + { + return null; + } + + return new FailureDetails( + failureDetails.ErrorType, + failureDetails.ErrorMessage, + failureDetails.StackTrace, + failureDetails.InnerFailure.ToCore(), + failureDetails.IsNonRetriable, + ConvertProperties(failureDetails.Properties)); + } + + /// + /// Converts a instance to a corresponding C# object. + /// + /// The Protobuf Value to convert. + /// The corresponding C# object. + /// + /// Thrown when the Protobuf Value.KindCase is not one of the supported types. + /// + internal static object? ConvertValueToObject(Google.Protobuf.WellKnownTypes.Value value) + { + switch (value.KindCase) + { + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NullValue: + return null; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NumberValue: + return value.NumberValue; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StringValue: string stringValue = value.StringValue; - // If the value starts with the 'dt:' prefix, it may represent a DateTime value - attempt to parse it. + // If the value starts with the 'dt:' prefix, it may represent a DateTime value — attempt to parse it. if (stringValue.StartsWith("dt:", StringComparison.Ordinal)) { if (DateTime.TryParse(stringValue[3..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime date)) @@ -1075,7 +1070,7 @@ internal static T Base64Decode(this MessageParser parser, string encodedMessa } } - // If the value starts with the 'dto:' prefix, it may represent a DateTime value - attempt to parse it. + // If the value starts with the 'dto:' prefix, it may represent a DateTime value — attempt to parse it. if (stringValue.StartsWith("dto:", StringComparison.Ordinal)) { if (DateTimeOffset.TryParse(stringValue[4..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset date)) @@ -1085,110 +1080,110 @@ internal static T Base64Decode(this MessageParser parser, string encodedMessa } // Otherwise just return as string - return stringValue; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.BoolValue: - return value.BoolValue; - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StructValue: - return value.StructValue.Fields.ToDictionary( - pair => pair.Key, - pair => ConvertValueToObject(pair.Value)); - case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.ListValue: - return value.ListValue.Values.Select(ConvertValueToObject).ToList(); + return stringValue; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.BoolValue: + return value.BoolValue; + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StructValue: + return value.StructValue.Fields.ToDictionary( + pair => pair.Key, + pair => ConvertValueToObject(pair.Value)); + case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.ListValue: + return value.ListValue.Values.Select(ConvertValueToObject).ToList(); default: // Fallback: serialize the whole value to JSON string - return JsonSerializer.Serialize(value); - } + return JsonSerializer.Serialize(value); + } } - /// - /// Converts a MapFieldinto a IDictionary. - /// + /// + /// Converts a MapFieldinto a IDictionary. + /// /// The map to convert. - /// Dictionary contains the converted obejct. - internal static IDictionary ConvertProperties(MapField properties) - { - return properties.ToDictionary( - kvp => kvp.Key, - kvp => ConvertValueToObject(kvp.Value)); - } - - /// - /// Converts a C# object to a protobuf Value. - /// - /// The object to convert. - /// The converted protobuf Value. - internal static Value ConvertObjectToValue(object? obj) - { - return obj switch - { - null => Value.ForNull(), - string str => Value.ForString(str), - bool b => Value.ForBool(b), - int i => Value.ForNumber(i), - long l => Value.ForNumber(l), - float f => Value.ForNumber(f), - double d => Value.ForNumber(d), + /// Dictionary contains the converted obejct. + internal static IDictionary ConvertProperties(MapField properties) + { + return properties.ToDictionary( + kvp => kvp.Key, + kvp => ConvertValueToObject(kvp.Value)); + } + + /// + /// Converts a C# object to a protobuf Value. + /// + /// The object to convert. + /// The converted protobuf Value. + internal static Value ConvertObjectToValue(object? obj) + { + return obj switch + { + null => Value.ForNull(), + string str => Value.ForString(str), + bool b => Value.ForBool(b), + int i => Value.ForNumber(i), + long l => Value.ForNumber(l), + float f => Value.ForNumber(f), + double d => Value.ForNumber(d), decimal dec => Value.ForNumber((double)dec), - // For DateTime and DateTimeOffset, add prefix to distinguish from normal string. - DateTime dt => Value.ForString($"dt:{dt.ToString("O")}"), - DateTimeOffset dto => Value.ForString($"dto:{dto.ToString("O")}"), - IDictionary dict => Value.ForStruct(new Struct - { - Fields = { dict.ToDictionary(kvp => kvp.Key, kvp => ConvertObjectToValue(kvp.Value)) }, - }), + // For DateTime and DateTimeOffset, add prefix to distinguish from normal string. + DateTime dt => Value.ForString($"dt:{dt.ToString("O")}"), + DateTimeOffset dto => Value.ForString($"dto:{dto.ToString("O")}"), + IDictionary dict => Value.ForStruct(new Struct + { + Fields = { dict.ToDictionary(kvp => kvp.Key, kvp => ConvertObjectToValue(kvp.Value)) }, + }), IEnumerable e => Value.ForList(e.Cast().Select(ConvertObjectToValue).ToArray()), // Fallback: convert unlisted type to string. - _ => Value.ForString(obj.ToString() ?? string.Empty), - }; - } - - /// - /// Converts a to a grpc . - /// - /// The failure details to convert. - /// The converted failure details. - static P.TaskFailureDetails? ToProtobuf(this FailureDetails? failureDetails) - { - if (failureDetails == null) - { - return null; - } - - var taskFailureDetails = new P.TaskFailureDetails - { - ErrorType = failureDetails.ErrorType ?? "(unknown)", - ErrorMessage = failureDetails.ErrorMessage ?? "(unknown)", - StackTrace = failureDetails.StackTrace, - IsNonRetriable = failureDetails.IsNonRetriable, - InnerFailure = failureDetails.InnerFailure.ToProtobuf(), - }; - - // Properly populate the MapField - if (failureDetails.Properties != null) - { - foreach (var kvp in failureDetails.Properties) - { - taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); - } - } - - return taskFailureDetails; - } - - static P.OrchestrationStatus ToProtobuf(this OrchestrationStatus status) - { - return (P.OrchestrationStatus)status; - } - - static P.OrchestrationInstance ToProtobuf(this OrchestrationInstance instance) - { - return new P.OrchestrationInstance - { - InstanceId = instance.InstanceId, - ExecutionId = instance.ExecutionId, - }; + _ => Value.ForString(obj.ToString() ?? string.Empty), + }; + } + + /// + /// Converts a to a grpc . + /// + /// The failure details to convert. + /// The converted failure details. + static P.TaskFailureDetails? ToProtobuf(this FailureDetails? failureDetails) + { + if (failureDetails == null) + { + return null; + } + + var taskFailureDetails = new P.TaskFailureDetails + { + ErrorType = failureDetails.ErrorType ?? "(unknown)", + ErrorMessage = failureDetails.ErrorMessage ?? "(unknown)", + StackTrace = failureDetails.StackTrace, + IsNonRetriable = failureDetails.IsNonRetriable, + InnerFailure = failureDetails.InnerFailure.ToProtobuf(), + }; + + // Properly populate the MapField + if (failureDetails.Properties != null) + { + foreach (var kvp in failureDetails.Properties) + { + taskFailureDetails.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value); + } + } + + return taskFailureDetails; + } + + static P.OrchestrationStatus ToProtobuf(this OrchestrationStatus status) + { + return (P.OrchestrationStatus)status; + } + + static P.OrchestrationInstance ToProtobuf(this OrchestrationInstance instance) + { + return new P.OrchestrationInstance + { + InstanceId = instance.InstanceId, + ExecutionId = instance.ExecutionId, + }; } static P.HistoryEvent ToProtobuf(HistoryEvent e) @@ -1211,107 +1206,107 @@ static P.HistoryEvent ToProtobuf(HistoryEvent e) } throw new ArgumentException("Unsupported event type"); - } - - /// - /// Tracks state required for converting orchestration histories containing entity-related events. - /// - internal class EntityConversionState - { - readonly bool insertMissingEntityUnlocks; - - OrchestrationInstance? instance; - HashSet? entityRequestIds; - Dictionary? unlockObligations; - - /// - /// Initializes a new instance of the class. - /// - /// Whether to insert missing unlock events in to the history - /// when the orchestration completes. - public EntityConversionState(bool insertMissingEntityUnlocks) - { - this.ConvertFromProto = (P.HistoryEvent e) => ProtoUtils.ConvertHistoryEvent(e, this); - this.insertMissingEntityUnlocks = insertMissingEntityUnlocks; - } - - /// - /// Gets a function that converts a history event in protobuf format to a core history event. - /// - public Func ConvertFromProto { get; } - - /// - /// Gets the orchestration instance of this history. - /// - public OrchestrationInstance? CurrentInstance => this.instance; - - /// - /// Gets the set of guids that have been used as entity request ids in this history. - /// - public HashSet EntityRequestIds => this.entityRequestIds ??= new(); - - /// - /// Records the orchestration instance, which may be needed for some conversions. - /// - /// The orchestration instance. - public void SetOrchestrationInstance(OrchestrationInstance instance) - { - this.instance = instance; - } - - /// - /// Adds unlock obligations for all entities that are being locked by this request. - /// - /// The lock request. - public void AddUnlockObligations(P.EntityLockRequestedEvent request) - { - if (!this.insertMissingEntityUnlocks) - { - return; - } - - this.unlockObligations ??= new(); - - foreach (string target in request.LockSet) - { - this.unlockObligations[target] = request.CriticalSectionId; - } - } - - /// - /// Removes an unlock obligation. - /// - /// The target entity. - public void RemoveUnlockObligation(string target) - { - if (!this.insertMissingEntityUnlocks) - { - return; - } - - this.unlockObligations?.Remove(target); - } - - /// - /// Returns the remaining unlock obligations, and clears the list. - /// - /// The unlock obligations. - public IEnumerable<(string Target, string CriticalSectionId)> ResetObligations() - { - if (!this.insertMissingEntityUnlocks) - { - yield break; - } - - if (this.unlockObligations is not null) - { - foreach (var kvp in this.unlockObligations) - { - yield return (kvp.Key, kvp.Value); - } - - this.unlockObligations = null; - } - } - } -} + } + + /// + /// Tracks state required for converting orchestration histories containing entity-related events. + /// + internal class EntityConversionState + { + readonly bool insertMissingEntityUnlocks; + + OrchestrationInstance? instance; + HashSet? entityRequestIds; + Dictionary? unlockObligations; + + /// + /// Initializes a new instance of the class. + /// + /// Whether to insert missing unlock events in to the history + /// when the orchestration completes. + public EntityConversionState(bool insertMissingEntityUnlocks) + { + this.ConvertFromProto = (P.HistoryEvent e) => ProtoUtils.ConvertHistoryEvent(e, this); + this.insertMissingEntityUnlocks = insertMissingEntityUnlocks; + } + + /// + /// Gets a function that converts a history event in protobuf format to a core history event. + /// + public Func ConvertFromProto { get; } + + /// + /// Gets the orchestration instance of this history. + /// + public OrchestrationInstance? CurrentInstance => this.instance; + + /// + /// Gets the set of guids that have been used as entity request ids in this history. + /// + public HashSet EntityRequestIds => this.entityRequestIds ??= new(); + + /// + /// Records the orchestration instance, which may be needed for some conversions. + /// + /// The orchestration instance. + public void SetOrchestrationInstance(OrchestrationInstance instance) + { + this.instance = instance; + } + + /// + /// Adds unlock obligations for all entities that are being locked by this request. + /// + /// The lock request. + public void AddUnlockObligations(P.EntityLockRequestedEvent request) + { + if (!this.insertMissingEntityUnlocks) + { + return; + } + + this.unlockObligations ??= new(); + + foreach (string target in request.LockSet) + { + this.unlockObligations[target] = request.CriticalSectionId; + } + } + + /// + /// Removes an unlock obligation. + /// + /// The target entity. + public void RemoveUnlockObligation(string target) + { + if (!this.insertMissingEntityUnlocks) + { + return; + } + + this.unlockObligations?.Remove(target); + } + + /// + /// Returns the remaining unlock obligations, and clears the list. + /// + /// The unlock obligations. + public IEnumerable<(string Target, string CriticalSectionId)> ResetObligations() + { + if (!this.insertMissingEntityUnlocks) + { + yield break; + } + + if (this.unlockObligations is not null) + { + foreach (var kvp in this.unlockObligations) + { + yield return (kvp.Key, kvp.Value); + } + + this.unlockObligations = null; + } + } + } +}