From 21ecf1d893bfacde567320db826eed0170813c6c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 7 May 2025 16:17:24 -0700 Subject: [PATCH 1/8] Activity tag support (code) --- Directory.Packages.props | 2 +- .../Activities/CacheClearingActivity.cs | 15 ++++++ .../CacheClearingOrchestrator.cs | 9 +++- samples/ScheduleWebApp/Program.cs | 6 ++- src/Abstractions/TaskOptions.cs | 50 +++++++++++++++++++ src/Shared/Grpc/ProtoUtils.cs | 14 +++++- .../Shims/TaskOrchestrationContextWrapper.cs | 16 ++++-- 7 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 samples/ScheduleWebApp/Activities/CacheClearingActivity.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index cfa91868..2825a035 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,7 +28,7 @@ - + diff --git a/samples/ScheduleWebApp/Activities/CacheClearingActivity.cs b/samples/ScheduleWebApp/Activities/CacheClearingActivity.cs new file mode 100644 index 00000000..5977174f --- /dev/null +++ b/samples/ScheduleWebApp/Activities/CacheClearingActivity.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; + +namespace ScheduleWebApp.Activities; + +[DurableTask] // Optional: enables code generation for type-safe calls +public class CacheClearingActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, object input) + { + return Task.FromResult("hello"); + } +} \ No newline at end of file diff --git a/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs index 4113f07a..57469ba8 100644 --- a/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs +++ b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.DurableTask; +using ScheduleWebApp.Activities; namespace ScheduleWebApp.Orchestrations; @@ -14,8 +15,12 @@ public override async Task RunAsync(TaskOrchestrationContext context, st { logger.LogInformation("Starting CacheClearingOrchestration for schedule ID: {ScheduleId}", scheduleId); - // Simulate cache clearing - await Task.Delay(TimeSpan.FromSeconds(5)); + CallActivityOptions options = new TaskOptions().WithTags(new Dictionary + { + { "scheduleId", scheduleId } + }); + + await context.CallActivityAsync(nameof(CacheClearingActivity), options); logger.LogInformation("CacheClearingOrchestration completed for schedule ID: {ScheduleId}", scheduleId); diff --git a/samples/ScheduleWebApp/Program.cs b/samples/ScheduleWebApp/Program.cs index fd89cc93..9ab021a8 100644 --- a/samples/ScheduleWebApp/Program.cs +++ b/samples/ScheduleWebApp/Program.cs @@ -7,6 +7,7 @@ using Microsoft.DurableTask.ScheduledTasks; using Microsoft.DurableTask.Worker; using Microsoft.DurableTask.Worker.AzureManaged; +using ScheduleWebApp.Activities; using ScheduleWebApp.Orchestrations; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -19,13 +20,14 @@ // Add all the generated orchestrations and activities automatically builder.Services.AddDurableTaskWorker(builder => { + builder.UseDurableTaskScheduler(connectionString); + builder.UseScheduledTasks(); builder.AddTasks(r => { // Add your orchestrators and activities here + r.AddActivity(); r.AddOrchestrator(); }); - builder.UseDurableTaskScheduler(connectionString); - builder.UseScheduledTasks(); }); // Register the client, which can be used to start orchestrations diff --git a/src/Abstractions/TaskOptions.cs b/src/Abstractions/TaskOptions.cs index d3e06e5c..76e38fdc 100644 --- a/src/Abstractions/TaskOptions.cs +++ b/src/Abstractions/TaskOptions.cs @@ -52,6 +52,13 @@ public TaskOptions(TaskRetryOptions? retry = null) /// The instance ID to use. /// A new . public SubOrchestrationOptions WithInstanceId(string instanceId) => new(this, instanceId); + + /// + /// Returns a new with the provided tags. + /// + /// The tags to associate with the activity. + /// A new . + public CallActivityOptions WithTags(IDictionary tags) => new(this, tags); } /// @@ -109,3 +116,46 @@ public record StartOrchestrationOptions(string? InstanceId = null, DateTimeOffse /// public IReadOnlyDictionary Tags { get; init; } = ImmutableDictionary.Create(); } + +/// +/// Options for calling activities from an orchestrator. +/// +public record CallActivityOptions : TaskOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// The task retry options. + /// The tags to associate with the activity. + public CallActivityOptions(TaskRetryOptions? retry = null, IDictionary? tags = null) + : base(retry) + { + if (tags != null) + { + this.Tags = tags; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The task options to wrap. + /// The tags to associate with the activity. + public CallActivityOptions(TaskOptions options, IDictionary? tags = null) + : base(options) + { + if (tags != null) + { + this.Tags = tags; + } + else if (options is CallActivityOptions derived) + { + this.Tags = derived.Tags; + } + } + + /// + /// Gets the tags to associate with the activity instance. + /// + public IDictionary Tags { get; init; } = ImmutableDictionary.Create(); +} diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index 56907bc9..f7b1c1ba 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -5,6 +5,7 @@ using System.Buffers.Text; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using DurableTask.Core; using DurableTask.Core.Command; @@ -90,7 +91,9 @@ internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto, EntityCon proto.EventId, proto.TaskScheduled.Name, proto.TaskScheduled.Version, - proto.TaskScheduled.Input); + proto.TaskScheduled.Input) { + Tags = proto.TaskScheduled.Tags, + }; break; case P.HistoryEvent.EventTypeOneofCase.TaskCompleted: historyEvent = new TaskCompletedEvent( @@ -303,6 +306,15 @@ internal static P.OrchestratorResponse ConstructOrchestratorResponse( Version = scheduleTaskAction.Version, Input = scheduleTaskAction.Input, }; + + 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; diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index a964020d..73c8f354 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -138,6 +138,15 @@ public override async Task CallActivityAsync( try { + IDictionary? tags = null; + if (options is CallActivityOptions callActivityOptions) + { + if (callActivityOptions.Tags is not null) + { + tags = callActivityOptions.Tags; + } + } + // TODO: Cancellation (https://github.com/microsoft/durabletask-dotnet/issues/7) if (options?.Retry?.Policy is RetryPolicy policy) { @@ -145,19 +154,20 @@ public override async Task CallActivityAsync( name.Name, name.Version, policy.ToDurableTaskCoreRetryOptions(), - input); + tags: tags, + parameters: input); } else if (options?.Retry?.Handler is AsyncRetryHandler handler) { return await this.InvokeWithCustomRetryHandler( - () => this.innerContext.ScheduleTask(name.Name, name.Version, input), + () => this.innerContext.ScheduleTask(name.Name, name.Version, tags: tags, parameters: input), name.Name, handler, default); } else { - return await this.innerContext.ScheduleTask(name.Name, name.Version, input); + return await this.innerContext.ScheduleTask(name.Name, name.Version, tags: tags, parameters: input); } } catch (global::DurableTask.Core.Exceptions.TaskFailedException e) From 62d25203e545aec9340b67e21e6f272bf1c05f41 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 12 May 2025 13:14:28 -0700 Subject: [PATCH 2/8] fb --- samples/ScheduleWebApp/Activities/CacheClearingActivity.cs | 6 ++++-- src/Shared/Grpc/ProtoUtils.cs | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/samples/ScheduleWebApp/Activities/CacheClearingActivity.cs b/samples/ScheduleWebApp/Activities/CacheClearingActivity.cs index 5977174f..17a6ee0b 100644 --- a/samples/ScheduleWebApp/Activities/CacheClearingActivity.cs +++ b/samples/ScheduleWebApp/Activities/CacheClearingActivity.cs @@ -8,8 +8,10 @@ namespace ScheduleWebApp.Activities; [DurableTask] // Optional: enables code generation for type-safe calls public class CacheClearingActivity : TaskActivity { - public override Task RunAsync(TaskActivityContext context, object input) + public override async Task RunAsync(TaskActivityContext context, object input) { - return Task.FromResult("hello"); + // Simulate cache clearing + await Task.Delay(TimeSpan.FromSeconds(5)); + return "Cache cleared"; } } \ No newline at end of file diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index f7b1c1ba..baf95fad 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -4,8 +4,6 @@ using System.Buffers; using System.Buffers.Text; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; using DurableTask.Core; using DurableTask.Core.Command; @@ -91,7 +89,8 @@ internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto, EntityCon proto.EventId, proto.TaskScheduled.Name, proto.TaskScheduled.Version, - proto.TaskScheduled.Input) { + proto.TaskScheduled.Input) + { Tags = proto.TaskScheduled.Tags, }; break; From 7f8e4df0507d4d786cb34e15e3abaf3924c1fa3f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 12 May 2025 15:12:17 -0700 Subject: [PATCH 3/8] fb --- .../CacheClearingOrchestrator.cs | 2 +- src/Abstractions/TaskOptions.cs | 44 ++++++++----------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs index 57469ba8..116aaefd 100644 --- a/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs +++ b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs @@ -15,7 +15,7 @@ public override async Task RunAsync(TaskOrchestrationContext context, st { logger.LogInformation("Starting CacheClearingOrchestration for schedule ID: {ScheduleId}", scheduleId); - CallActivityOptions options = new TaskOptions().WithTags(new Dictionary + CallActivityOptions options = new CallActivityOptions(tags: new Dictionary { { "scheduleId", scheduleId } }); diff --git a/src/Abstractions/TaskOptions.cs b/src/Abstractions/TaskOptions.cs index 76e38fdc..c0757107 100644 --- a/src/Abstractions/TaskOptions.cs +++ b/src/Abstractions/TaskOptions.cs @@ -19,11 +19,27 @@ public TaskOptions(TaskRetryOptions? retry = null) this.Retry = retry; } + /// + /// Initializes a new instance of the class. + /// + /// The task retry options. + /// The tags to associate with the task. + public TaskOptions(TaskRetryOptions? retry = null, IDictionary? tags = null) + { + this.Retry = retry; + this.Tags = tags; + } + /// /// Gets the task retry options. /// public TaskRetryOptions? Retry { get; init; } + /// + /// Gets the tags to associate with the task. + /// + public IDictionary? Tags { get; init; } + /// /// Returns a new from the provided . /// @@ -52,13 +68,6 @@ public TaskOptions(TaskRetryOptions? retry = null) /// The instance ID to use. /// A new . public SubOrchestrationOptions WithInstanceId(string instanceId) => new(this, instanceId); - - /// - /// Returns a new with the provided tags. - /// - /// The tags to associate with the activity. - /// A new . - public CallActivityOptions WithTags(IDictionary tags) => new(this, tags); } /// @@ -128,12 +137,8 @@ public record CallActivityOptions : TaskOptions /// The task retry options. /// The tags to associate with the activity. public CallActivityOptions(TaskRetryOptions? retry = null, IDictionary? tags = null) - : base(retry) + : base(retry, tags) { - if (tags != null) - { - this.Tags = tags; - } } /// @@ -142,20 +147,7 @@ public CallActivityOptions(TaskRetryOptions? retry = null, IDictionaryThe task options to wrap. /// The tags to associate with the activity. public CallActivityOptions(TaskOptions options, IDictionary? tags = null) - : base(options) + : base(options.Retry, tags ?? options.Tags) { - if (tags != null) - { - this.Tags = tags; - } - else if (options is CallActivityOptions derived) - { - this.Tags = derived.Tags; - } } - - /// - /// Gets the tags to associate with the activity instance. - /// - public IDictionary Tags { get; init; } = ImmutableDictionary.Create(); } From 3fa86b88c39ddd532859c47e6335a62976efa225 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 14 May 2025 15:35:20 -0700 Subject: [PATCH 4/8] update to align with latest core api --- .../Shims/TaskOrchestrationContextWrapper.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index 73c8f354..2bb05639 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -150,24 +150,38 @@ public override async Task CallActivityAsync( // TODO: Cancellation (https://github.com/microsoft/durabletask-dotnet/issues/7) if (options?.Retry?.Policy is RetryPolicy policy) { - return await this.innerContext.ScheduleWithRetry( + return await this.innerContext.ScheduleTask( name.Name, name.Version, - policy.ToDurableTaskCoreRetryOptions(), - tags: tags, + options: ScheduleTaskOptions.CreateBuilder() + .WithRetryOptions(policy.ToDurableTaskCoreRetryOptions()) + .WithTags(tags) + .Build(), parameters: input); } else if (options?.Retry?.Handler is AsyncRetryHandler handler) { return await this.InvokeWithCustomRetryHandler( - () => this.innerContext.ScheduleTask(name.Name, name.Version, tags: tags, parameters: input), + () => this.innerContext.ScheduleTask( + name.Name, + name.Version, + options: ScheduleTaskOptions.CreateBuilder() + .WithTags(tags) + .Build(), + parameters: input), name.Name, handler, default); } else { - return await this.innerContext.ScheduleTask(name.Name, name.Version, tags: tags, parameters: input); + return await this.innerContext.ScheduleTask( + name.Name, + name.Version, + options: ScheduleTaskOptions.CreateBuilder() + .WithTags(tags) + .Build(), + parameters: input); } } catch (global::DurableTask.Core.Exceptions.TaskFailedException e) From e796a4138739b9859dccec973965d41a0b2bbaeb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:40:13 -0700 Subject: [PATCH 5/8] fix tests --- src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index 95679e7b..5fda4054 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -138,7 +138,7 @@ public override async Task CallActivityAsync( try { - IDictionary? tags = null; + IDictionary? tags = new Dictionary(); if (options is CallActivityOptions callActivityOptions) { if (callActivityOptions.Tags is not null) @@ -516,7 +516,7 @@ string GetDefaultVersion() } // Secondary choice. - if (this.Properties.TryGetValue("defaultVersion", out var propVersion) && propVersion is string v2) + if (this.Properties.TryGetValue("defaultVersion", out object? propVersion) && propVersion is string v2) { return v2; } From 93adb1d44f6730c83e4e10406e1dd81a511beaa2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:14:14 -0700 Subject: [PATCH 6/8] fb --- .../CacheClearingOrchestrator.cs | 2 +- src/Abstractions/TaskOptions.cs | 26 ------------------- .../Shims/TaskOrchestrationContextWrapper.cs | 4 +-- 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs index 116aaefd..4aad5716 100644 --- a/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs +++ b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs @@ -15,7 +15,7 @@ public override async Task RunAsync(TaskOrchestrationContext context, st { logger.LogInformation("Starting CacheClearingOrchestration for schedule ID: {ScheduleId}", scheduleId); - CallActivityOptions options = new CallActivityOptions(tags: new Dictionary + TaskOptions options = new TaskOptions(tags: new Dictionary { { "scheduleId", scheduleId } }); diff --git a/src/Abstractions/TaskOptions.cs b/src/Abstractions/TaskOptions.cs index 1f6ce251..8d4fbd63 100644 --- a/src/Abstractions/TaskOptions.cs +++ b/src/Abstractions/TaskOptions.cs @@ -135,29 +135,3 @@ public record StartOrchestrationOptions(string? InstanceId = null, DateTimeOffse /// public TaskVersion? Version { get; init; } } - -/// -/// Options for calling activities from an orchestrator. -/// -public record CallActivityOptions : TaskOptions -{ - /// - /// Initializes a new instance of the class. - /// - /// The task retry options. - /// The tags to associate with the activity. - public CallActivityOptions(TaskRetryOptions? retry = null, IDictionary? tags = null) - : base(retry, tags) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The task options to wrap. - /// The tags to associate with the activity. - public CallActivityOptions(TaskOptions options, IDictionary? tags = null) - : base(options.Retry, tags ?? options.Tags) - { - } -} diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index 5fda4054..20078ece 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -138,8 +138,8 @@ public override async Task CallActivityAsync( try { - IDictionary? tags = new Dictionary(); - if (options is CallActivityOptions callActivityOptions) + IDictionary tags = new Dictionary(); + if (options is TaskOptions callActivityOptions) { if (callActivityOptions.Tags is not null) { From f78dd205dff4fde11cc6afeac79a174ee3385f0b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:25:52 -0700 Subject: [PATCH 7/8] Refactor TaskOrchestrationContextWrapper to use ImmutableDictionary for tags --- src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index 20078ece..c7c5dea5 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using System.Globalization; using System.Security.Cryptography; using System.Text; @@ -138,7 +139,7 @@ public override async Task CallActivityAsync( try { - IDictionary tags = new Dictionary(); + IDictionary tags = ImmutableDictionary.Empty; if (options is TaskOptions callActivityOptions) { if (callActivityOptions.Tags is not null) From 7ad9f47b39e2560c84152ca2011b9f8b027475df Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:13:00 -0700 Subject: [PATCH 8/8] Add integration test for orchestration with activity tags --- .../OrchestrationPatterns.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs index b64a5fa8..2634935e 100644 --- a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs +++ b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs @@ -944,6 +944,42 @@ public async Task SubOrchestrationTaskVersionOverridesDefaultVersion(string over Assert.Equal($"Sub Orchestration version: {overrideVersion}", output); } + [Fact] + public async Task RunActivityWithTags() + { + TaskName orchestratorName = nameof(RunActivityWithTags); + TaskName taggedActivityName = "TaggedActivity"; + + await using HostTestLifetime server = await this.StartWorkerAsync(b => + { + b.AddTasks(tasks => tasks + .AddOrchestratorFunc( + orchestratorName, (ctx, input) => ctx.CallActivityAsync(taggedActivityName, input)) + .AddActivityFunc(taggedActivityName, (ctx, name) => $"Hello from tagged activity, {name}!")); + }); + + // Schedule orchestration with tags + StartOrchestrationOptions options = new() + { + Tags = new Dictionary + { + { "activityTag", "taggedExecution" }, + { "testType", "activityTagTest" } + } + }; + + string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync( + orchestratorName, input: "World", options); + + OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync( + instanceId, getInputsAndOutputs: true, this.TimeoutToken); + + Assert.NotNull(metadata); + Assert.Equal(instanceId, metadata.InstanceId); + Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus); + Assert.Equal("Hello from tagged activity, World!", metadata.ReadOutputAs()); + } + // TODO: Test for multiple external events with the same name // TODO: Test for ContinueAsNew with external events that carry over // TODO: Test for catching activity exceptions of specific types