From c2a94f0c89cc3d6ee5e5e2dc93694ddb7c999d19 Mon Sep 17 00:00:00 2001 From: Alice Lin Date: Thu, 3 Sep 2026 18:06:18 -0700 Subject: [PATCH] Update Nexus messaging samples to use Temporal Operation handler --- .../Handler/NexusGreetingService.cs | 91 ++++++------ src/NexusMessaging/CallerPattern/README.md | 28 +++- .../Caller/CallerRemoteWorkflow.workflow.cs | 18 +++ .../Handler/GreetingWorkflow.workflow.cs | 8 + .../Handler/NexusRemoteGreetingService.cs | 138 +++++++++++------- .../INexusRemoteGreetingService.cs | 5 + src/NexusMessaging/OnDemandPattern/README.md | 53 +++++-- tests/NexusMessaging/CallerPatternTests.cs | 34 ++++- tests/NexusMessaging/OnDemandPatternTests.cs | 56 ++++--- 9 files changed, 294 insertions(+), 137 deletions(-) diff --git a/src/NexusMessaging/CallerPattern/Handler/NexusGreetingService.cs b/src/NexusMessaging/CallerPattern/Handler/NexusGreetingService.cs index 4c7aa4e..d0038ac 100644 --- a/src/NexusMessaging/CallerPattern/Handler/NexusGreetingService.cs +++ b/src/NexusMessaging/CallerPattern/Handler/NexusGreetingService.cs @@ -1,6 +1,7 @@ namespace TemporalioSamples.NexusMessaging.CallerPattern.Handler; using NexusRpc.Handlers; +using Temporalio.Client; using Temporalio.Nexus; using TemporalioSamples.NexusMessaging.CallerPattern; using TemporalioSamples.NexusMessaging.Common; @@ -11,55 +12,63 @@ namespace TemporalioSamples.NexusMessaging.CallerPattern.Handler; [NexusServiceHandler(typeof(INexusGreetingService))] public class NexusGreetingService { - // OperationHandler.Sync means the result is returned inline to the Nexus caller - // (as opposed to WorkflowRunOperationHandler, which returns an async operation token). - // The lambda may still be async internally. +#pragma warning disable VSTHRD200 // Names must match the INexusGreetingService operations, which can't take the Async suffix // Query: read-only, no state mutation — uses workflow query - [NexusOperationHandler] - public IOperationHandler GetLanguages() => - OperationHandler.Sync( - async (ctx, input) => - { - // Access the Temporal client from the Nexus operation context - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(WorkflowIdForUser(input.UserId)); - return await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported)); - }); + [TemporalOperation] + public async Task> GetLanguages( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusGreetingService.GetLanguagesInput input) + { + // Access the Temporal client from the Nexus client passed to the handler + var handle = client.TemporalClient.GetWorkflowHandle( + WorkflowIdForUser(input.UserId)); + return TemporalOperationResult.SyncResult( + await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported))); + } // Query: read-only — returns the workflow's current language - [NexusOperationHandler] - public IOperationHandler GetLanguage() => - OperationHandler.Sync( - async (ctx, input) => - { - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(WorkflowIdForUser(input.UserId)); - return await handle.QueryAsync(wf => wf.QueryLanguage()); - }); + [TemporalOperation] + public async Task> GetLanguage( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusGreetingService.GetLanguageInput input) + { + var handle = client.TemporalClient.GetWorkflowHandle( + WorkflowIdForUser(input.UserId)); + return TemporalOperationResult.SyncResult( + await handle.QueryAsync(wf => wf.QueryLanguage())); + } // Update: mutates state and returns the previous value — uses workflow update - [NexusOperationHandler] - public IOperationHandler SetLanguage() => - OperationHandler.Sync( - async (ctx, input) => - { - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(WorkflowIdForUser(input.UserId)); - return await handle.ExecuteUpdateAsync(wf => wf.SetLanguageAsync(input.Language)); - }); + [TemporalOperation] + public Task> SetLanguage( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusGreetingService.SetLanguageInput input) => + client.StartWorkflowUpdateAsync( + WorkflowIdForUser(input.UserId), + wf => wf.SetLanguageAsync(input.Language), + // An Update-backed Operation must wait for the Accepted stage. Any other stage is + // rejected with "nexus op workflow updates only support WorkflowUpdateStageAccepted + // for async updates". + new(WorkflowUpdateStage.Accepted)); // Signal: fire-and-forget, no return value needed — uses workflow signal - [NexusOperationHandler] - public IOperationHandler Approve() => - OperationHandler.Sync( - async (ctx, input) => - { - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(WorkflowIdForUser(input.UserId)); - await handle.SignalAsync(wf => wf.ApproveAsync(input.Name)); - return default; - }); + [TemporalOperation] + public async Task> Approve( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusGreetingService.ApproveInput input) + { + var handle = client.TemporalClient.GetWorkflowHandle( + WorkflowIdForUser(input.UserId)); + await handle.SignalAsync(wf => wf.ApproveAsync(input.Name)); + return TemporalOperationResult.SyncResult(default); + } + +#pragma warning restore VSTHRD200 private static string WorkflowIdForUser(string userId) => $"GreetingWorkflow_for_{userId}"; } diff --git a/src/NexusMessaging/CallerPattern/README.md b/src/NexusMessaging/CallerPattern/README.md index 083e8ab..54ff2a1 100644 --- a/src/NexusMessaging/CallerPattern/README.md +++ b/src/NexusMessaging/CallerPattern/README.md @@ -14,19 +14,24 @@ The caller Workflow: ### Running -Start a Temporal server: +This sample requires a Temporal dev server build that supports Workflow Update callbacks. Download +the compatible binary from the [Temporal CLI pre-release instructions](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support). + +Start the Temporal dev server with the required namespaces pre-created and Workflow Update +callbacks enabled: ```bash -temporal server start-dev +./temporal server start-dev \ + --dynamic-config-value history.enableUpdateCallbacks=true \ + --dynamic-config-value history.enableCHASMSignalBacklinks=true \ + --namespace nexus-messaging-handler-namespace \ + --namespace nexus-messaging-caller-namespace ``` -Create the namespaces and Nexus endpoint: +Create the Nexus endpoint: ```bash -temporal operator namespace create --namespace nexus-messaging-handler-namespace -temporal operator namespace create --namespace nexus-messaging-caller-namespace - -temporal operator nexus endpoint create \ +./temporal operator nexus endpoint create \ --name nexus-messaging-caller-pattern-endpoint \ --target-namespace nexus-messaging-handler-namespace \ --target-task-queue nexus-messaging-handler-sample @@ -49,3 +54,12 @@ In a third terminal, run the following command to start the example: ```bash dotnet run --project src/NexusMessaging -- caller-workflow ``` + +Expected output: + +``` +Supported languages: Chinese, English +Current language: English +Set language from English to Chinese +Approved workflow +``` diff --git a/src/NexusMessaging/OnDemandPattern/Caller/CallerRemoteWorkflow.workflow.cs b/src/NexusMessaging/OnDemandPattern/Caller/CallerRemoteWorkflow.workflow.cs index 39d8cd7..4e97f6e 100644 --- a/src/NexusMessaging/OnDemandPattern/Caller/CallerRemoteWorkflow.workflow.cs +++ b/src/NexusMessaging/OnDemandPattern/Caller/CallerRemoteWorkflow.workflow.cs @@ -18,6 +18,17 @@ public async Task RunAsync() var userIdOne = "user-one"; var userIdTwo = "user-two"; + // Attach information before the Workflow exists. Since AttachApprovalContext is backed + // by Signal-with-Start on the handler, this call creates the Workflow and delivers the + // note to it. + await client.ExecuteNexusOperationAsync( + svc => svc.AttachApprovalContext(new INexusRemoteGreetingService.AttachApprovalContextInput( + "queued for localization review by the nightly batch", userIdOne))); + log.Add($"Attached approval context before the workflow existed: {userIdOne}"); + + // The Workflow for this user is already running due to AttachApprovalContext. The handler + // sets the conflict policy to UseExisting, so this call attaches the Operation's + // completion callback to the running execution. var handleOne = await client.StartNexusOperationAsync( svc => svc.RunFromRemote(new INexusRemoteGreetingService.RunFromRemoteInput(userIdOne))); log.Add($"Started remote workflow for user: {userIdOne}"); @@ -26,6 +37,13 @@ public async Task RunAsync() svc => svc.RunFromRemote(new INexusRemoteGreetingService.RunFromRemoteInput(userIdTwo))); log.Add($"Started remote workflow for user: {userIdTwo}"); + // This user's Workflow was created by RunFromRemote just above, so here Signal-with-Start + // skips the start and only delivers the Signal. + await client.ExecuteNexusOperationAsync( + svc => svc.AttachApprovalContext(new INexusRemoteGreetingService.AttachApprovalContextInput( + "translation approved by the localization team", userIdTwo))); + log.Add($"Attached approval context to the running workflow: {userIdTwo}"); + // Interact with workflow one: get languages, set language, approve var languagesOne = await client.ExecuteNexusOperationAsync( svc => svc.GetLanguages(new INexusRemoteGreetingService.GetLanguagesInput(false, userIdOne))); diff --git a/src/NexusMessaging/OnDemandPattern/Handler/GreetingWorkflow.workflow.cs b/src/NexusMessaging/OnDemandPattern/Handler/GreetingWorkflow.workflow.cs index f3042d7..e9c9c1d 100644 --- a/src/NexusMessaging/OnDemandPattern/Handler/GreetingWorkflow.workflow.cs +++ b/src/NexusMessaging/OnDemandPattern/Handler/GreetingWorkflow.workflow.cs @@ -17,6 +17,7 @@ public class GreetingWorkflow private Language currentLanguage = Language.English; private bool approved; private string approvedBy = string.Empty; + private string? approvalContext; [WorkflowRun] public async Task RunAsync(string userId) @@ -79,4 +80,11 @@ public Task ApproveAsync(string name) approvedBy = name; return Task.CompletedTask; } + + [WorkflowSignal] + public Task AttachApprovalContextAsync(string note) + { + approvalContext = note; + return Task.CompletedTask; + } } diff --git a/src/NexusMessaging/OnDemandPattern/Handler/NexusRemoteGreetingService.cs b/src/NexusMessaging/OnDemandPattern/Handler/NexusRemoteGreetingService.cs index 52b5c46..f27e111 100644 --- a/src/NexusMessaging/OnDemandPattern/Handler/NexusRemoteGreetingService.cs +++ b/src/NexusMessaging/OnDemandPattern/Handler/NexusRemoteGreetingService.cs @@ -1,6 +1,8 @@ namespace TemporalioSamples.NexusMessaging.OnDemandPattern.Handler; using NexusRpc.Handlers; +using Temporalio.Api.Enums.V1; +using Temporalio.Client; using Temporalio.Nexus; using TemporalioSamples.NexusMessaging.Common; using TemporalioSamples.NexusMessaging.OnDemandPattern; @@ -11,66 +13,100 @@ namespace TemporalioSamples.NexusMessaging.OnDemandPattern.Handler; [NexusServiceHandler(typeof(INexusRemoteGreetingService))] public class NexusRemoteGreetingService { - // WorkflowRunOperationHandler starts a backing workflow and returns its handle to the - // Nexus infrastructure. The caller receives an async operation token and can poll for - // the workflow result later via GetResultAsync. - [NexusOperationHandler] - public IOperationHandler RunFromRemote() => - WorkflowRunOperationHandler.FromHandleFactory( - (WorkflowRunOperationContext context, INexusRemoteGreetingService.RunFromRemoteInput input) => - context.StartWorkflowAsync( - (GreetingWorkflow wf) => wf.RunAsync(input.UserId), - new() { Id = GetWorkflowId(input.UserId) })); +#pragma warning disable VSTHRD200 // Names must match the INexusRemoteGreetingService operations, which can't take the Async suffix - // OperationHandler.Sync means the result is returned inline to the Nexus caller - // (as opposed to WorkflowRunOperationHandler, which returns an async operation token). - // The lambda may still be async internally. - - // Query: read-only, no state mutation — uses workflow query - [NexusOperationHandler] - public IOperationHandler GetLanguages() => - OperationHandler.Sync( - async (ctx, input) => + // Starts the GreetingWorkflow for the given user, or attaches to one already running. + // StartWorkflowAsync returns an async result, which attaches a completion callback, + // so the Operation completes when the Workflow returns. + [TemporalOperation] + public Task> RunFromRemote( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusRemoteGreetingService.RunFromRemoteInput input) => + client.StartWorkflowAsync( + (GreetingWorkflow wf) => wf.RunAsync(input.UserId), + new() { - // Access the Temporal client from the Nexus operation context - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(GetWorkflowId(input.UserId)); - return await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported)); + Id = GetWorkflowId(input.UserId), + + // By default, starting a Workflow whose ID is already running fails the Operation. + // Since AttachApprovalContext below can create the GreetingWorkflow first, this + // Operation needs to attach to the running execution rather than fail. + IdConflictPolicy = WorkflowIdConflictPolicy.UseExisting, }); + // Query: read-only, no state mutation — uses workflow query + [TemporalOperation] + public async Task> GetLanguages( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusRemoteGreetingService.GetLanguagesInput input) + { + // Access the Temporal client from the Nexus client passed to the handler + var handle = client.TemporalClient.GetWorkflowHandle( + GetWorkflowId(input.UserId)); + return TemporalOperationResult.SyncResult( + await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported))); + } + // Query: read-only — returns the workflow's current language - [NexusOperationHandler] - public IOperationHandler GetLanguage() => - OperationHandler.Sync( - async (ctx, input) => - { - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(GetWorkflowId(input.UserId)); - return await handle.QueryAsync(wf => wf.QueryLanguage()); - }); + [TemporalOperation] + public async Task> GetLanguage( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusRemoteGreetingService.GetLanguageInput input) + { + var handle = client.TemporalClient.GetWorkflowHandle( + GetWorkflowId(input.UserId)); + return TemporalOperationResult.SyncResult( + await handle.QueryAsync(wf => wf.QueryLanguage())); + } // Update: mutates state and returns the previous value — uses workflow update - [NexusOperationHandler] - public IOperationHandler SetLanguage() => - OperationHandler.Sync( - async (ctx, input) => - { - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(GetWorkflowId(input.UserId)); - return await handle.ExecuteUpdateAsync(wf => wf.SetLanguageAsync(input.Language)); - }); + [TemporalOperation] + public Task> SetLanguage( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusRemoteGreetingService.SetLanguageInput input) => + client.StartWorkflowUpdateAsync( + GetWorkflowId(input.UserId), + wf => wf.SetLanguageAsync(input.Language), + // An Update-backed Operation must wait for the Accepted stage. Any other stage is + // rejected with "nexus op workflow updates only support WorkflowUpdateStageAccepted + // for async updates". + new(WorkflowUpdateStage.Accepted)); // Signal: fire-and-forget, no return value needed — uses workflow signal - [NexusOperationHandler] - public IOperationHandler Approve() => - OperationHandler.Sync( - async (ctx, input) => - { - var client = NexusOperationExecutionContext.Current.TemporalClient; - var handle = client.GetWorkflowHandle(GetWorkflowId(input.UserId)); - await handle.SignalAsync(wf => wf.ApproveAsync(input.Name)); - return default; - }); + [TemporalOperation] + public async Task> Approve( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusRemoteGreetingService.ApproveInput input) + { + var handle = client.TemporalClient.GetWorkflowHandle( + GetWorkflowId(input.UserId)); + await handle.SignalAsync(wf => wf.ApproveAsync(input.Name)); + return TemporalOperationResult.SyncResult(default); + } + + // Signal-with-Start: starts the Workflow first if it is not already running. + // When the Workflow already exists, only the Signal is delivered. + [TemporalOperation] + public async Task> AttachApprovalContext( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + INexusRemoteGreetingService.AttachApprovalContextInput input) + { + var options = new WorkflowOptions( + id: GetWorkflowId(input.UserId), + taskQueue: NexusOperationExecutionContext.Current.Info.TaskQueue); + options.SignalWithStart((GreetingWorkflow wf) => wf.AttachApprovalContextAsync(input.Note)); + await client.TemporalClient.StartWorkflowAsync( + (GreetingWorkflow wf) => wf.RunAsync(input.UserId), options); + return TemporalOperationResult.SyncResult(default); + } + +#pragma warning restore VSTHRD200 private static string GetWorkflowId(string userId) => $"GreetingWorkflow_for_{userId}"; } diff --git a/src/NexusMessaging/OnDemandPattern/INexusRemoteGreetingService.cs b/src/NexusMessaging/OnDemandPattern/INexusRemoteGreetingService.cs index d0b8121..5e3a6cf 100644 --- a/src/NexusMessaging/OnDemandPattern/INexusRemoteGreetingService.cs +++ b/src/NexusMessaging/OnDemandPattern/INexusRemoteGreetingService.cs @@ -21,6 +21,9 @@ public interface INexusRemoteGreetingService [NexusOperation] void Approve(ApproveInput input); + [NexusOperation] + void AttachApprovalContext(AttachApprovalContextInput input); + public record RunFromRemoteInput(string UserId); public record GetLanguagesInput(bool IncludeUnsupported, string UserId); @@ -32,4 +35,6 @@ public record GetLanguageInput(string UserId); public record SetLanguageInput(Language Language, string UserId); public record ApproveInput(string Name, string UserId); + + public record AttachApprovalContextInput(string Note, string UserId); } diff --git a/src/NexusMessaging/OnDemandPattern/README.md b/src/NexusMessaging/OnDemandPattern/README.md index ef99357..0643fb9 100644 --- a/src/NexusMessaging/OnDemandPattern/README.md +++ b/src/NexusMessaging/OnDemandPattern/README.md @@ -1,31 +1,41 @@ ## On-demand pattern No Workflow is pre-started. The caller creates and controls Workflow instances through Nexus -operations. `NexusRemoteGreetingService` adds a `RunFromRemote` operation that starts a new -`GreetingWorkflow`, and every other operation includes a `UserId` so the handler can derive +operations. `NexusRemoteGreetingService` adds a `RunFromRemote` operation that starts a +`GreetingWorkflow`, and every operation includes a `UserId` so the handler can derive the target Workflow ID. The caller Workflow: -1. Starts two remote `GreetingWorkflow` instances via `RunFromRemote` (backed by `WorkflowRunOperationHandler`) -2. Workflow one: queries supported languages, changes to Spanish, and approves -3. Workflow two: queries the current language, changes to French, and approves -4. Waits for each to complete and returns their results +1. Attaches approval context for the first user via `AttachApprovalContext`, before anything has + started that user's Workflow +2. Starts two remote `GreetingWorkflow` instances via `RunFromRemote` (backed by a Workflow started + through `ITemporalNexusClient.StartWorkflowAsync`) +3. Attaches approval context for the second user, whose Workflow now already exists +4. Workflow one: queries supported languages, changes to Spanish, and approves +5. Workflow two: queries the current language, changes to French, and approves +6. Waits for each to complete and returns their results ### Running -Start a Temporal server: +This sample requires a Temporal dev server build that supports Workflow Update callbacks. Download +the compatible binary from the [Temporal CLI pre-release instructions](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support). + +Start the Temporal dev server with the required namespaces pre-created, and Workflow Update +callbacks and signal-with-start from a Workflow enabled: ```bash -temporal server start-dev +./temporal server start-dev \ + --dynamic-config-value history.enableUpdateCallbacks=true \ + --dynamic-config-value history.enableCHASMSignalBacklinks=true \ + --dynamic-config-value history.enableSignalWithStartFromWorkflow=true \ + --namespace nexus-messaging-handler-namespace \ + --namespace nexus-messaging-caller-namespace ``` -Create the namespaces and Nexus endpoint: +Create the Nexus endpoint: ```bash -temporal operator namespace create --namespace nexus-messaging-handler-namespace -temporal operator namespace create --namespace nexus-messaging-caller-namespace - -temporal operator nexus endpoint create \ +./temporal operator nexus endpoint create \ --name nexus-messaging-on-demand-pattern-endpoint \ --target-namespace nexus-messaging-handler-namespace \ --target-task-queue nexus-messaging-handler-sample @@ -48,3 +58,20 @@ In a third terminal, run the following command to start the example: ```bash dotnet run --project src/NexusMessaging -- remote-caller-workflow ``` + +Expected output: + +``` +Attached approval context before the workflow existed: user-one +Started remote workflow for user: user-one +Started remote workflow for user: user-two +Attached approval context to the running workflow: user-two +[One] Supported languages: Chinese, English +[One] Set language from English to Spanish +[One] Approved +[Two] Current language: English +[Two] Set language from English to French +[Two] Approved +[One] Result: Hola, mundo (approved by CallerRemoteWorkflow) +[Two] Result: Bonjour, monde (approved by CallerRemoteWorkflow) +``` diff --git a/tests/NexusMessaging/CallerPatternTests.cs b/tests/NexusMessaging/CallerPatternTests.cs index a9938b2..5ec1770 100644 --- a/tests/NexusMessaging/CallerPatternTests.cs +++ b/tests/NexusMessaging/CallerPatternTests.cs @@ -1,6 +1,7 @@ namespace TemporalioSamples.Tests.NexusMessaging; using Temporalio.Client; +using Temporalio.Testing; using Temporalio.Worker; using TemporalioSamples.NexusMessaging.CallerPattern.Caller; using TemporalioSamples.NexusMessaging.CallerPattern.Handler; @@ -8,23 +9,40 @@ namespace TemporalioSamples.Tests.NexusMessaging; using Xunit; using Xunit.Abstractions; -public class CallerPatternTests : WorkflowEnvironmentTestBase +public class CallerPatternTests : TestBase { - public CallerPatternTests(ITestOutputHelper output, WorkflowEnvironment env) - : base(output, env) + public CallerPatternTests(ITestOutputHelper output) + : base(output) { } [Fact] public async Task RunAsync_CallerWorkflow_Succeeds() { + // SetLanguage is backed by a Workflow Update, which needs a dev server build that supports + // update callbacks. + await using var env = await WorkflowEnvironment.StartLocalAsync(new() + { + DevServerOptions = new() + { + DownloadVersion = "v1.7.4-standalone-nexus-operations", + ExtraArgs = + [ + "--dynamic-config-value", + "history.enableUpdateCallbacks=true", + "--dynamic-config-value", + "history.enableCHASMSignalBacklinks=true", + ], + }, + }); + var handlerTaskQueue = $"tq-{Guid.NewGuid()}"; - await Env.TestEnv.CreateNexusEndpointAsync(NexusEndpoints.GreetingService, handlerTaskQueue); + await env.CreateNexusEndpointAsync(NexusEndpoints.GreetingService, handlerTaskQueue); var userId = $"user-{Guid.NewGuid()}"; var workflowId = $"GreetingWorkflow_for_{userId}"; // Start entity workflow - await Client.StartWorkflowAsync( + await env.Client.StartWorkflowAsync( (GreetingWorkflow wf) => wf.RunAsync(userId), new(id: workflowId, taskQueue: handlerTaskQueue) { @@ -33,7 +51,7 @@ await Client.StartWorkflowAsync( // Run handler worker using var handlerWorker = new TemporalWorker( - Client, + env.Client, new TemporalWorkerOptions(handlerTaskQueue). AddNexusService(new NexusGreetingService()). AddWorkflow(). @@ -42,12 +60,12 @@ await handlerWorker.ExecuteAsync(async () => { // Run caller worker using var callerWorker = new TemporalWorker( - Client, + env.Client, new TemporalWorkerOptions($"tq-{Guid.NewGuid()}"). AddWorkflow()); await callerWorker.ExecuteAsync(async () => { - var result = await Client.ExecuteWorkflowAsync( + var result = await env.Client.ExecuteWorkflowAsync( (CallerWorkflow wf) => wf.RunAsync(userId), new(id: $"wf-{Guid.NewGuid()}", taskQueue: callerWorker.Options.TaskQueue!)); diff --git a/tests/NexusMessaging/OnDemandPatternTests.cs b/tests/NexusMessaging/OnDemandPatternTests.cs index 61bc21c..4b7f7b3 100644 --- a/tests/NexusMessaging/OnDemandPatternTests.cs +++ b/tests/NexusMessaging/OnDemandPatternTests.cs @@ -1,6 +1,7 @@ namespace TemporalioSamples.Tests.NexusMessaging; using Temporalio.Client; +using Temporalio.Testing; using Temporalio.Worker; using TemporalioSamples.NexusMessaging.Common; using TemporalioSamples.NexusMessaging.OnDemandPattern.Caller; @@ -8,22 +9,41 @@ namespace TemporalioSamples.Tests.NexusMessaging; using Xunit; using Xunit.Abstractions; -public class OnDemandPatternTests : WorkflowEnvironmentTestBase +public class OnDemandPatternTests : TestBase { - public OnDemandPatternTests(ITestOutputHelper output, WorkflowEnvironment env) - : base(output, env) + public OnDemandPatternTests(ITestOutputHelper output) + : base(output) { } [Fact] public async Task RunAsync_CallerRemoteWorkflow_Succeeds() { + // SetLanguage is backed by a Workflow Update and AttachApprovalContext by + // Signal-with-Start, both of which need a dev server build that supports them. + await using var env = await WorkflowEnvironment.StartLocalAsync(new() + { + DevServerOptions = new() + { + DownloadVersion = "v1.7.4-standalone-nexus-operations", + ExtraArgs = + [ + "--dynamic-config-value", + "history.enableUpdateCallbacks=true", + "--dynamic-config-value", + "history.enableCHASMSignalBacklinks=true", + "--dynamic-config-value", + "history.enableSignalWithStartFromWorkflow=true", + ], + }, + }); + var handlerTaskQueue = $"tq-{Guid.NewGuid()}"; - await Env.TestEnv.CreateNexusEndpointAsync(NexusEndpoints.RemoteGreetingService, handlerTaskQueue); + await env.CreateNexusEndpointAsync(NexusEndpoints.RemoteGreetingService, handlerTaskQueue); // Run handler worker using var handlerWorker = new TemporalWorker( - Client, + env.Client, new TemporalWorkerOptions(handlerTaskQueue). AddNexusService(new NexusRemoteGreetingService()). AddWorkflow(). @@ -32,25 +52,27 @@ await handlerWorker.ExecuteAsync(async () => { // Run caller worker using var callerWorker = new TemporalWorker( - Client, + env.Client, new TemporalWorkerOptions($"tq-{Guid.NewGuid()}"). AddWorkflow()); await callerWorker.ExecuteAsync(async () => { - var result = await Client.ExecuteWorkflowAsync( + var result = await env.Client.ExecuteWorkflowAsync( (CallerRemoteWorkflow wf) => wf.RunAsync(), new(id: $"wf-{Guid.NewGuid()}", taskQueue: callerWorker.Options.TaskQueue!)); - Assert.Contains("Started remote workflow for user: user-one", result[0]); - Assert.Contains("Started remote workflow for user: user-two", result[1]); - Assert.Contains("[One] Supported languages:", result[2]); - Assert.Contains($"[One] Set language from {Language.English} to {Language.Spanish}", result[3]); - Assert.Contains("[One] Approved", result[4]); - Assert.Contains("[Two] Current language:", result[5]); - Assert.Contains($"[Two] Set language from {Language.English} to {Language.French}", result[6]); - Assert.Contains("[Two] Approved", result[7]); - Assert.Contains("[One] Result:", result[8]); - Assert.Contains("[Two] Result:", result[9]); + Assert.Contains("Attached approval context before the workflow existed: user-one", result[0]); + Assert.Contains("Started remote workflow for user: user-one", result[1]); + Assert.Contains("Started remote workflow for user: user-two", result[2]); + Assert.Contains("Attached approval context to the running workflow: user-two", result[3]); + Assert.Contains("[One] Supported languages:", result[4]); + Assert.Contains($"[One] Set language from {Language.English} to {Language.Spanish}", result[5]); + Assert.Contains("[One] Approved", result[6]); + Assert.Contains("[Two] Current language:", result[7]); + Assert.Contains($"[Two] Set language from {Language.English} to {Language.French}", result[8]); + Assert.Contains("[Two] Approved", result[9]); + Assert.Contains("[One] Result:", result[10]); + Assert.Contains("[Two] Result:", result[11]); }); }); }