Skip to content

Commit 425f27f

Browse files
alliscodealliscodeCopilot
authored
.NET: Fixing issue where OpenTelemetry span is never exported in .NET in-process workflow execution (#4196)
* 1. Add reproduction test for issue #4155: workflow.run Activity never stopped in streaming OffThread path The WorkflowRunActivity_IsStopped_Streaming_OffThread test demonstrates that the workflow.run OpenTelemetry Activity created in StreamingRunEventStream.RunLoopAsync is started but never stopped when using the OffThread/Default streaming execution. The background run loop keeps running after event consumption completes, so the using Activity? declaration never disposes until explicit StopAsync() is called. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 2. Fix workflow.run Activity never stopped in streaming OffThread execution (#4155) The workflow.run OpenTelemetry Activity in StreamingRunEventStream.RunLoopAsync was scoped to the method lifetime via 'using'. Since the run loop only exits on cancellation, the Activity was never stopped/exported until explicit disposal. Fix: Remove 'using' and explicitly dispose the Activity when the workflow reaches Idle status (all supersteps complete). A safety-net disposal in the finally block handles cancellation and error paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add root-level workflow.session activity spanning run loop lifetime\n\nImplements two-level telemetry hierarchy per PR feedback from lokitoth:\n- workflow.session: spans the entire run loop / stream lifetime\n- workflow_invoke: per input-to-halt cycle, nested within the session\n\nThis ensures the session activity stays open across multiple turns,\nwhile individual run activities are created and disposed per cycle.\n\nAlso fixes linkedSource CancellationTokenSource disposal leak in\nStreamingRunEventStream (added using declaration)." * Address Copilot review: fix Activity/CTS disposal, rename activity, add error tag\n\n1. LockstepRunEventStream: Remove 'using' from Activity in async iterator\n and manually dispose in finally block (fixes #4155 pattern). Also dispose\n linkedSource CTS in finally to prevent leak.\n2. Tags.cs: Add ErrorMessage (\"error.message\") tag for runtime errors,\n distinct from BuildErrorMessage (\"build.error.message\").\n3. ActivityNames: Rename WorkflowRun from \"workflow_invoke\" to \"workflow.run\"\n for cross-language consistency.\n4. WorkflowTelemetryContext: Fix XML doc to say \"outer/parent span\" instead\n of \"root-level span\".\n5. ObservabilityTests: Assert WorkflowSession absence when DisableWorkflowRun\n is true.\n6. WorkflowRunActivityStopTests: Fix streaming test race by disposing\n StreamingRun before asserting activities are stopped.\n7. StreamingRunEventStream/LockstepRunEventStream: Use Tags.ErrorMessage\n instead of Tags.BuildErrorMessage for runtime error events." * Review fixes: revert workflow_invoke rename, use 'using' for linkedSource, move SessionStarted earlier\n\n- Revert ActivityNames.WorkflowRun back to \"workflow_invoke\" (OTEL semantic convention contract)\n- Use 'using' declaration for linkedSource CTS in LockstepRunEventStream (no timing sensitivity)\n- Move SessionStarted event before WaitForInputAsync in StreamingRunEventStream to match Lockstep behavior" * Improve naming and comments in WorkflowRunActivityStopTests" * Prevent session Activity.Current leak in lockstep mode, add nesting test Save and restore Activity.Current in LockstepRunEventStream.Start() so the session activity doesn't leak into caller code via AsyncLocal. Re-establish Activity.Current = sessionActivity before creating the run activity in TakeEventStreamAsync to preserve parent-child nesting. Add test verifying app activities after RunAsync are not parented under the session, and that the workflow_invoke activity nests under the session." * Fix stale XML doc: WorkflowRun -> WorkflowInvoke in ObservabilityTests --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7d56a5a commit 425f27f

File tree

8 files changed

+480
-36
lines changed

8 files changed

+480
-36
lines changed

dotnet/src/Microsoft.Agents.AI.Workflows/Execution/LockstepRunEventStream.cs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
1818
private int _isDisposed;
1919

2020
private readonly ISuperStepRunner _stepRunner;
21+
private Activity? _sessionActivity;
2122

2223
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default) => new(this.RunStatus);
2324

@@ -30,7 +31,16 @@ public LockstepRunEventStream(ISuperStepRunner stepRunner)
3031

3132
public void Start()
3233
{
33-
// No-op for lockstep execution
34+
// Save and restore Activity.Current so the long-lived session activity
35+
// doesn't leak into caller code via AsyncLocal.
36+
Activity? previousActivity = Activity.Current;
37+
38+
this._sessionActivity = this._stepRunner.TelemetryContext.StartWorkflowSessionActivity();
39+
this._sessionActivity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId)
40+
.SetTag(Tags.SessionId, this._stepRunner.SessionId);
41+
this._sessionActivity?.AddEvent(new ActivityEvent(EventNames.SessionStarted));
42+
43+
Activity.Current = previousActivity;
3444
}
3545

3646
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default)
@@ -44,19 +54,23 @@ public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPe
4454
}
4555
#endif
4656

47-
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(this._stopCancellation.Token, cancellationToken);
57+
using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(this._stopCancellation.Token, cancellationToken);
4858

4959
ConcurrentQueue<WorkflowEvent> eventSink = [];
5060

5161
this._stepRunner.OutgoingEvents.EventRaised += OnWorkflowEventAsync;
5262

53-
using Activity? activity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
54-
activity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.SessionId, this._stepRunner.SessionId);
63+
// Re-establish session as parent so the run activity nests correctly.
64+
Activity.Current = this._sessionActivity;
65+
66+
// Not 'using' — must dispose explicitly in finally for deterministic export.
67+
Activity? runActivity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
68+
runActivity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.SessionId, this._stepRunner.SessionId);
5569

5670
try
5771
{
5872
this.RunStatus = RunStatus.Running;
59-
activity?.AddEvent(new ActivityEvent(EventNames.WorkflowStarted));
73+
runActivity?.AddEvent(new ActivityEvent(EventNames.WorkflowStarted));
6074

6175
do
6276
{
@@ -65,7 +79,7 @@ public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPe
6579
{
6680
// Because we may be yielding out of this function, we need to ensure that the Activity.Current
6781
// is set to our activity for the duration of this loop iteration.
68-
Activity.Current = activity;
82+
Activity.Current = runActivity;
6983

7084
// Drain SuperSteps while there are steps to run
7185
try
@@ -75,13 +89,13 @@ public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPe
7589
catch (OperationCanceledException)
7690
{
7791
}
78-
catch (Exception ex) when (activity is not null)
92+
catch (Exception ex) when (runActivity is not null)
7993
{
80-
activity.AddEvent(new ActivityEvent(EventNames.WorkflowError, tags: new() {
94+
runActivity.AddEvent(new ActivityEvent(EventNames.WorkflowError, tags: new() {
8195
{ Tags.ErrorType, ex.GetType().FullName },
82-
{ Tags.BuildErrorMessage, ex.Message },
96+
{ Tags.ErrorMessage, ex.Message },
8397
}));
84-
activity.CaptureException(ex);
98+
runActivity.CaptureException(ex);
8599
throw;
86100
}
87101

@@ -129,12 +143,16 @@ public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPe
129143
}
130144
} while (!ShouldBreak());
131145

132-
activity?.AddEvent(new ActivityEvent(EventNames.WorkflowCompleted));
146+
runActivity?.AddEvent(new ActivityEvent(EventNames.WorkflowCompleted));
133147
}
134148
finally
135149
{
136150
this.RunStatus = this._stepRunner.HasUnservicedRequests ? RunStatus.PendingRequests : RunStatus.Idle;
137151
this._stepRunner.OutgoingEvents.EventRaised -= OnWorkflowEventAsync;
152+
153+
// Explicitly dispose the Activity so Activity.Stop fires deterministically,
154+
// regardless of how the async iterator enumerator is disposed.
155+
runActivity?.Dispose();
138156
}
139157

140158
ValueTask OnWorkflowEventAsync(object? sender, WorkflowEvent e)
@@ -172,6 +190,14 @@ public ValueTask DisposeAsync()
172190
{
173191
this._stopCancellation.Cancel();
174192

193+
// Stop the session activity
194+
if (this._sessionActivity is not null)
195+
{
196+
this._sessionActivity.AddEvent(new ActivityEvent(EventNames.SessionCompleted));
197+
this._sessionActivity.Dispose();
198+
this._sessionActivity = null;
199+
}
200+
175201
this._stopCancellation.Dispose();
176202
this._inputWaiter.Dispose();
177203
}

dotnet/src/Microsoft.Agents.AI.Workflows/Execution/StreamingRunEventStream.cs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,20 @@ public void Start()
5555
private async Task RunLoopAsync(CancellationToken cancellationToken)
5656
{
5757
using CancellationTokenSource errorSource = new();
58-
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(errorSource.Token, cancellationToken);
58+
using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(errorSource.Token, cancellationToken);
5959

6060
// Subscribe to events - they will flow directly to the channel as they're raised
6161
this._stepRunner.OutgoingEvents.EventRaised += OnEventRaisedAsync;
6262

63-
using Activity? activity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
64-
activity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.SessionId, this._stepRunner.SessionId);
63+
// Start the session-level activity that spans the entire run loop lifetime.
64+
// Individual run-stage activities are nested within this session activity.
65+
Activity? sessionActivity = this._stepRunner.TelemetryContext.StartWorkflowSessionActivity();
66+
sessionActivity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId)
67+
.SetTag(Tags.SessionId, this._stepRunner.SessionId);
68+
69+
Activity? runActivity = null;
70+
71+
sessionActivity?.AddEvent(new ActivityEvent(EventNames.SessionStarted));
6572

6673
try
6774
{
@@ -70,10 +77,15 @@ private async Task RunLoopAsync(CancellationToken cancellationToken)
7077
await this._inputWaiter.WaitForInputAsync(cancellationToken: linkedSource.Token).ConfigureAwait(false);
7178

7279
this._runStatus = RunStatus.Running;
73-
activity?.AddEvent(new ActivityEvent(EventNames.WorkflowStarted));
7480

7581
while (!linkedSource.Token.IsCancellationRequested)
7682
{
83+
// Start a new run-stage activity for this input→processing→halt cycle
84+
runActivity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
85+
runActivity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId)
86+
.SetTag(Tags.SessionId, this._stepRunner.SessionId);
87+
runActivity?.AddEvent(new ActivityEvent(EventNames.WorkflowStarted));
88+
7789
// Run all available supersteps continuously
7890
// Events are streamed out in real-time as they happen via the event handler
7991
while (this._stepRunner.HasUnprocessedMessages && !linkedSource.Token.IsCancellationRequested)
@@ -93,6 +105,15 @@ private async Task RunLoopAsync(CancellationToken cancellationToken)
93105
RunStatus capturedStatus = this._runStatus;
94106
await this._eventChannel.Writer.WriteAsync(new InternalHaltSignal(currentEpoch, capturedStatus), linkedSource.Token).ConfigureAwait(false);
95107

108+
// Close the run-stage activity when processing halts.
109+
// A new run activity will be created when the next input arrives.
110+
if (runActivity is not null)
111+
{
112+
runActivity.AddEvent(new ActivityEvent(EventNames.WorkflowCompleted));
113+
runActivity.Dispose();
114+
runActivity = null;
115+
}
116+
96117
// Wait for next input from the consumer
97118
// Works for both Idle (no work) and PendingRequests (waiting for responses)
98119
await this._inputWaiter.WaitForInputAsync(TimeSpan.FromSeconds(1), linkedSource.Token).ConfigureAwait(false);
@@ -107,14 +128,26 @@ private async Task RunLoopAsync(CancellationToken cancellationToken)
107128
}
108129
catch (Exception ex)
109130
{
110-
if (activity != null)
131+
// Record error on the run-stage activity if one is active
132+
if (runActivity is not null)
133+
{
134+
runActivity.AddEvent(new ActivityEvent(EventNames.WorkflowError, tags: new() {
135+
{ Tags.ErrorType, ex.GetType().FullName },
136+
{ Tags.ErrorMessage, ex.Message },
137+
}));
138+
runActivity.CaptureException(ex);
139+
}
140+
141+
// Record error on the session activity
142+
if (sessionActivity is not null)
111143
{
112-
activity.AddEvent(new ActivityEvent(EventNames.WorkflowError, tags: new() {
144+
sessionActivity.AddEvent(new ActivityEvent(EventNames.SessionError, tags: new() {
113145
{ Tags.ErrorType, ex.GetType().FullName },
114-
{ Tags.BuildErrorMessage, ex.Message },
146+
{ Tags.ErrorMessage, ex.Message },
115147
}));
116-
activity.CaptureException(ex);
148+
sessionActivity.CaptureException(ex);
117149
}
150+
118151
await this._eventChannel.Writer.WriteAsync(new WorkflowErrorEvent(ex), linkedSource.Token).ConfigureAwait(false);
119152
}
120153
finally
@@ -124,7 +157,20 @@ private async Task RunLoopAsync(CancellationToken cancellationToken)
124157

125158
// Mark as ended when run loop exits
126159
this._runStatus = RunStatus.Ended;
127-
activity?.AddEvent(new ActivityEvent(EventNames.WorkflowCompleted));
160+
161+
// Stop the run-stage activity if not already stopped (e.g. on cancellation or error)
162+
if (runActivity is not null)
163+
{
164+
runActivity.AddEvent(new ActivityEvent(EventNames.WorkflowCompleted));
165+
runActivity.Dispose();
166+
}
167+
168+
// Stop the session activity — the session always ends when the run loop exits
169+
if (sessionActivity is not null)
170+
{
171+
sessionActivity.AddEvent(new ActivityEvent(EventNames.SessionCompleted));
172+
sessionActivity.Dispose();
173+
}
128174
}
129175

130176
async ValueTask OnEventRaisedAsync(object? sender, WorkflowEvent e)

dotnet/src/Microsoft.Agents.AI.Workflows/Observability/ActivityNames.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ namespace Microsoft.Agents.AI.Workflows.Observability;
55
internal static class ActivityNames
66
{
77
public const string WorkflowBuild = "workflow.build";
8-
public const string WorkflowRun = "workflow_invoke";
8+
public const string WorkflowSession = "workflow.session";
9+
public const string WorkflowInvoke = "workflow_invoke";
910
public const string MessageSend = "message.send";
1011
public const string ExecutorProcess = "executor.process";
1112
public const string EdgeGroupProcess = "edge_group.process";

dotnet/src/Microsoft.Agents.AI.Workflows/Observability/EventNames.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ internal static class EventNames
88
public const string BuildValidationCompleted = "build.validation_completed";
99
public const string BuildCompleted = "build.completed";
1010
public const string BuildError = "build.error";
11+
public const string SessionStarted = "session.started";
12+
public const string SessionCompleted = "session.completed";
13+
public const string SessionError = "session.error";
1114
public const string WorkflowStarted = "workflow.started";
1215
public const string WorkflowCompleted = "workflow.completed";
1316
public const string WorkflowError = "workflow.error";

dotnet/src/Microsoft.Agents.AI.Workflows/Observability/Tags.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal static class Tags
1111
public const string BuildErrorMessage = "build.error.message";
1212
public const string BuildErrorType = "build.error.type";
1313
public const string ErrorType = "error.type";
14+
public const string ErrorMessage = "error.message";
1415
public const string SessionId = "session.id";
1516
public const string ExecutorId = "executor.id";
1617
public const string ExecutorType = "executor.type";

dotnet/src/Microsoft.Agents.AI.Workflows/Observability/WorkflowTelemetryContext.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,25 @@ public WorkflowTelemetryContext(WorkflowTelemetryOptions options, ActivitySource
8888
}
8989

9090
/// <summary>
91-
/// Starts a workflow run activity if enabled.
91+
/// Starts a workflow session activity if enabled. This is the outer/parent span
92+
/// that represents the entire lifetime of a workflow execution (from start
93+
/// until stop, cancellation, or error) within the current trace.
94+
/// Individual run stages are typically nested within it.
95+
/// </summary>
96+
/// <returns>An activity if workflow run telemetry is enabled, otherwise null.</returns>
97+
public Activity? StartWorkflowSessionActivity()
98+
{
99+
if (!this.IsEnabled || this.Options.DisableWorkflowRun)
100+
{
101+
return null;
102+
}
103+
104+
return this.ActivitySource.StartActivity(ActivityNames.WorkflowSession);
105+
}
106+
107+
/// <summary>
108+
/// Starts a workflow run activity if enabled. This represents a single
109+
/// input-to-halt cycle within a workflow session.
92110
/// </summary>
93111
/// <returns>An activity if workflow run telemetry is enabled, otherwise null.</returns>
94112
public Activity? StartWorkflowRunActivity()
@@ -98,7 +116,7 @@ public WorkflowTelemetryContext(WorkflowTelemetryOptions options, ActivitySource
98116
return null;
99117
}
100118

101-
return this.ActivitySource.StartActivity(ActivityNames.WorkflowRun);
119+
return this.ActivitySource.StartActivity(ActivityNames.WorkflowInvoke);
102120
}
103121

104122
/// <summary>

dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,16 @@ public ObservabilityTests()
4343
/// Create a sample workflow for testing.
4444
/// </summary>
4545
/// <remarks>
46-
/// This workflow is expected to create 8 activities that will be captured by the tests
46+
/// This workflow is expected to create 9 activities that will be captured by the tests
4747
/// - ActivityNames.WorkflowBuild
48-
/// - ActivityNames.WorkflowRun
49-
/// -- ActivityNames.EdgeGroupProcess
50-
/// -- ActivityNames.ExecutorProcess (UppercaseExecutor)
51-
/// --- ActivityNames.MessageSend
52-
/// ---- ActivityNames.EdgeGroupProcess
53-
/// -- ActivityNames.ExecutorProcess (ReverseTextExecutor)
54-
/// --- ActivityNames.MessageSend
48+
/// - ActivityNames.WorkflowSession
49+
/// -- ActivityNames.WorkflowInvoke
50+
/// --- ActivityNames.EdgeGroupProcess
51+
/// --- ActivityNames.ExecutorProcess (UppercaseExecutor)
52+
/// ---- ActivityNames.MessageSend
53+
/// ----- ActivityNames.EdgeGroupProcess
54+
/// --- ActivityNames.ExecutorProcess (ReverseTextExecutor)
55+
/// ---- ActivityNames.MessageSend
5556
/// </remarks>
5657
/// <returns>The created workflow.</returns>
5758
private static Workflow CreateWorkflow()
@@ -74,7 +75,8 @@ private static Dictionary<string, int> GetExpectedActivityNameCounts() =>
7475
new()
7576
{
7677
{ ActivityNames.WorkflowBuild, 1 },
77-
{ ActivityNames.WorkflowRun, 1 },
78+
{ ActivityNames.WorkflowSession, 1 },
79+
{ ActivityNames.WorkflowInvoke, 1 },
7880
{ ActivityNames.EdgeGroupProcess, 2 },
7981
{ ActivityNames.ExecutorProcess, 2 },
8082
{ ActivityNames.MessageSend, 2 }
@@ -113,7 +115,7 @@ private async Task TestWorkflowEndToEndActivitiesAsync(string executionEnvironme
113115

114116
// Assert
115117
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
116-
capturedActivities.Should().HaveCount(8, "Exactly 8 activities should be created.");
118+
capturedActivities.Should().HaveCount(9, "Exactly 9 activities should be created.");
117119

118120
// Make sure all expected activities exist and have the correct count
119121
foreach (var kvp in GetExpectedActivityNameCounts())
@@ -125,7 +127,7 @@ private async Task TestWorkflowEndToEndActivitiesAsync(string executionEnvironme
125127
}
126128

127129
// Verify WorkflowRun activity events include workflow lifecycle events
128-
var workflowRunActivity = capturedActivities.First(a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal));
130+
var workflowRunActivity = capturedActivities.First(a => a.OperationName.StartsWith(ActivityNames.WorkflowInvoke, StringComparison.Ordinal));
129131
var activityEvents = workflowRunActivity.Events.ToList();
130132
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowStarted, "activity should have workflow started event");
131133
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event");
@@ -273,8 +275,11 @@ public async Task DisableWorkflowRun_PreventsWorkflowRunActivityAsync()
273275
// Assert
274276
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
275277
capturedActivities.Should().NotContain(
276-
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
278+
a => a.OperationName.StartsWith(ActivityNames.WorkflowInvoke, StringComparison.Ordinal),
277279
"WorkflowRun activity should be disabled.");
280+
capturedActivities.Should().NotContain(
281+
a => a.OperationName.StartsWith(ActivityNames.WorkflowSession, StringComparison.Ordinal),
282+
"WorkflowSession activity should also be disabled when DisableWorkflowRun is true.");
278283
capturedActivities.Should().Contain(
279284
a => a.OperationName.StartsWith(ActivityNames.WorkflowBuild, StringComparison.Ordinal),
280285
"Other activities should still be created.");
@@ -303,7 +308,7 @@ public async Task DisableExecutorProcess_PreventsExecutorProcessActivityAsync()
303308
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
304309
"ExecutorProcess activity should be disabled.");
305310
capturedActivities.Should().Contain(
306-
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
311+
a => a.OperationName.StartsWith(ActivityNames.WorkflowInvoke, StringComparison.Ordinal),
307312
"Other activities should still be created.");
308313
}
309314

0 commit comments

Comments
 (0)