From 3ff4f0b9b7be92fb0e4e83eb27f721653e15b412 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Tue, 29 Jul 2025 15:33:24 -0700 Subject: [PATCH 1/5] Use orchestration and sub-orchestration versions explicitly specified via APIs --- .../DurableOrchestrationContext.cs | 37 ++++++-- .../FunctionNameWithVersion.cs | 47 ++++++++++ .../HttpApiHandler.cs | 5 +- .../Listener/OutOfProcOrchestrationShim.cs | 7 +- test/Common/HttpApiHandlerTests.cs | 17 ++-- test/Common/OrchestrationVersionTests.cs | 24 ++++++ test/Common/TestOrchestrations.cs | 22 +++++ .../FunctionNameWithVersionTests.cs | 33 +++++++ test/FunctionsV2/OutOfProcTests.cs | 85 +++++++++++++++++++ 9 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 src/WebJobs.Extensions.DurableTask/FunctionNameWithVersion.cs create mode 100644 test/FunctionsV2/FunctionNameWithVersionTests.cs diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableOrchestrationContext.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableOrchestrationContext.cs index 485c7210b..0c1eb6e66 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableOrchestrationContext.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableOrchestrationContext.cs @@ -310,7 +310,7 @@ async Task IDurableOrchestrationContext.CallHttpAsync(Durab private async Task ScheduleDurableHttpActivityAsync(DurableHttpRequest req) { DurableHttpResponse durableHttpResponse = await this.CallDurableTaskFunctionAsync( - functionName: HttpOptions.HttpTaskActivityReservedName, + functionNameWithVersion: HttpOptions.HttpTaskActivityReservedName, functionType: FunctionType.Activity, oneWay: false, instanceId: null, @@ -538,7 +538,7 @@ string IDurableOrchestrationContext.StartNewOrchestration(string functionName, o } internal async Task CallDurableTaskFunctionAsync( - string functionName, + string functionNameWithVersion, FunctionType functionType, bool oneWay, string instanceId, @@ -562,11 +562,7 @@ internal async Task CallDurableTaskFunctionAsync( } } - // Propagate the default version to orchestrators. - // TODO: Decide whether we want to propagate the default version to actitities and entities as well. - string version = (functionType == FunctionType.Orchestrator) - ? this.Config.Options.DefaultVersion - : string.Empty; + (string functionName, string version) = this.ResolveVersionForFunctionCall(functionNameWithVersion, functionType); this.Config.ThrowIfFunctionDoesNotExist(functionName, functionType); @@ -850,6 +846,33 @@ internal async Task CallDurableTaskFunctionAsync( return output; } + /// + /// Resolves the function name and version for a function call based on the function type. + /// + /// The function name, optionally with version suffix. + /// The type of function being called. + /// A tuple containing the function name and resolved version. + private (string functionName, string version) ResolveVersionForFunctionCall( + string functionNameWithVersion, + FunctionType functionType) + { + // Only orchestrators support versioning. + // TODO: Decide whether we want to propagate the version to activities and entities as well. + if (functionType != FunctionType.Orchestrator) + { + return (functionNameWithVersion, null); + } + + (string functionName, string version) = FunctionNameWithVersion.Parse(functionNameWithVersion); + if (version is null) + { + // Propagate the default version if no explicit version is provided. + version = this.Config.Options.DefaultVersion; + } + + return (functionName, version); + } + internal async Task WaitForEntityResponse(Guid guid, EntityId? lockToUse) { var response = await this.WaitForExternalEvent(guid.ToString(), "EntityResponse"); diff --git a/src/WebJobs.Extensions.DurableTask/FunctionNameWithVersion.cs b/src/WebJobs.Extensions.DurableTask/FunctionNameWithVersion.cs new file mode 100644 index 000000000..f50a4e207 --- /dev/null +++ b/src/WebJobs.Extensions.DurableTask/FunctionNameWithVersion.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Microsoft.Azure.WebJobs.Extensions.DurableTask +{ + /// + /// Utility class for handling function names with optional version information. + /// + internal static class FunctionNameWithVersion + { + /// + /// Delimiter used to separate function name from version in serialized format. + /// + internal const char Delimiter = '\n'; + + /// + /// Combines a function name and optional version into a single string. + /// + /// The name of the function. + /// The optional version string. If null, only the function name is returned. + /// The combined function name and version string, or just the function name if version is null. + internal static string Combine(string functionName, string version) + { + return version == null ? functionName : functionName + Delimiter + version; + } + + /// + /// Parses a combined function name and version string into separate components. + /// + /// The combined function name and version string. + /// A tuple containing the function name and version. Version will be null if no delimiter is found. + internal static (string functionName, string version) Parse(string functionNameAndVersion) + { + int delimiterIndex = functionNameAndVersion.IndexOf(Delimiter); + if (delimiterIndex < 0) + { + // No version specified + return (functionNameAndVersion, null); + } + + // Function name and version are separated by delimiter + var functionName = functionNameAndVersion.Substring(0, delimiterIndex); + var version = functionNameAndVersion.Substring(delimiterIndex + 1); + return (functionName, version); + } + } +} diff --git a/src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs b/src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs index d9e586d78..daa795dca 100644 --- a/src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs +++ b/src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs @@ -63,6 +63,7 @@ internal class HttpApiHandler : IDisposable private const string PollingInterval = "pollingInterval"; private const string SuspendOperation = "suspend"; private const string ResumeOperation = "resume"; + private const string VersionParameter = "version"; private const string EmptyEntityKeySymbol = "$"; @@ -887,12 +888,14 @@ private async Task HandleStartOrchestratorRequestAsync( ExecutionId = Guid.NewGuid().ToString(), }; + var version = queryNameValuePairs[VersionParameter] ?? this.config.Options.DefaultVersion; + // Create the ExecutionStartedEvent ExecutionStartedEvent executionStartedEvent = new ExecutionStartedEvent(-1, json) { Name = functionName, OrchestrationInstance = instance, - Version = this.config.Options.DefaultVersion, + Version = version, }; string traceParent = GetHeaderValueFromHeaders("traceparent", request.Headers); diff --git a/src/WebJobs.Extensions.DurableTask/Listener/OutOfProcOrchestrationShim.cs b/src/WebJobs.Extensions.DurableTask/Listener/OutOfProcOrchestrationShim.cs index 47d7c9092..14ceeef11 100644 --- a/src/WebJobs.Extensions.DurableTask/Listener/OutOfProcOrchestrationShim.cs +++ b/src/WebJobs.Extensions.DurableTask/Listener/OutOfProcOrchestrationShim.cs @@ -137,10 +137,10 @@ private Task InvokeAPIFromAction(AsyncAction action, SchemaVersion schema) task = this.context.CallActivityWithRetryAsync(action.FunctionName, action.RetryOptions, action.Input); break; case AsyncActionType.CallSubOrchestrator: - task = this.context.CallSubOrchestratorAsync(action.FunctionName, action.InstanceId, action.Input); + task = this.context.CallSubOrchestratorAsync(FunctionNameWithVersion.Combine(action.FunctionName, action.Version), action.InstanceId, action.Input); break; case AsyncActionType.CallSubOrchestratorWithRetry: - task = this.context.CallSubOrchestratorWithRetryAsync(action.FunctionName, action.RetryOptions, action.InstanceId, action.Input); + task = this.context.CallSubOrchestratorWithRetryAsync(FunctionNameWithVersion.Combine(action.FunctionName, action.Version), action.RetryOptions, action.InstanceId, action.Input); break; case AsyncActionType.CallEntity: { @@ -310,6 +310,9 @@ private class AsyncAction [JsonProperty("functionName")] internal string FunctionName { get; set; } + [JsonProperty("version")] + internal string Version { get; set; } + [JsonProperty("input")] internal object Input { get; set; } diff --git a/test/Common/HttpApiHandlerTests.cs b/test/Common/HttpApiHandlerTests.cs index 78c210e66..70128ca02 100644 --- a/test/Common/HttpApiHandlerTests.cs +++ b/test/Common/HttpApiHandlerTests.cs @@ -1673,14 +1673,21 @@ public async Task GetClientResponseLinks_Ignores_Forwarded_Headers_When_Disabled Assert.StartsWith("http://localhost:7071", (string)status["terminatePostUri"]); } - [Fact] + [Theory] + [InlineData(null, null, null)] // No default, no query parameter + [InlineData("4.0", null, "4.0")] // Default version used when no query parameter + [InlineData("4.0", "5.2", "5.2")] // Query parameter overrides default + [InlineData("4.0", "", "")] // Empty query parameter overrides default [Trait("Category", PlatformSpecificHelpers.TestCategory)] - public async Task StartNewInstance_Uses_DefaultVersion_And_Calls_CreateTaskOrchestrationAsync() + public async Task StartNewInstance_Calls_CreateTaskOrchestrationAsync_With_Correct_Version( + string defaultVersion, string queryParameterVersion, string expectedVersion) { var functionName = "TestOrchestrator"; var instanceId = Guid.NewGuid().ToString("N"); - var defaultVersion = "4.0"; - var requestUri = new Uri($"http://localhost/runtime/webhooks/durabletask/orchestrators/{functionName}/{instanceId}"); + var baseUri = $"http://localhost/runtime/webhooks/durabletask/orchestrators/{functionName}/{instanceId}"; + var requestUri = queryParameterVersion != null + ? new Uri($"{baseUri}?version={queryParameterVersion}") + : new Uri(baseUri); ExecutionStartedEvent capturedEvent = null; @@ -1725,7 +1732,7 @@ public async Task StartNewInstance_Uses_DefaultVersion_And_Calls_CreateTaskOrche Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); Assert.NotNull(capturedEvent); - Assert.Equal(defaultVersion, capturedEvent.Version); + Assert.Equal(expectedVersion, capturedEvent.Version); } private static DurableTaskExtension GetTestExtension() diff --git a/test/Common/OrchestrationVersionTests.cs b/test/Common/OrchestrationVersionTests.cs index 3f323f31e..6ffbe2ec1 100644 --- a/test/Common/OrchestrationVersionTests.cs +++ b/test/Common/OrchestrationVersionTests.cs @@ -86,5 +86,29 @@ ITestHost GetJobHost(string taskHubName, string defaultVersion) options: new DurableTaskOptions { DefaultVersion = defaultVersion }); } } + + [Theory] + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + [InlineData("4.0", nameof(TestOrchestrations.CallSubOrchestratorWithExplicitVersion), "\"V2.0\"")] // Explicit version overrides default + [InlineData("4.0", nameof(TestOrchestrations.CallSubOrchestratorWithoutVersion), "\"4.0\"")] // Default version propagated when no explicit version + [InlineData(null, nameof(TestOrchestrations.CallSubOrchestratorWithoutVersion), "null")] // Null version when no default configured + public async Task SubOrchestrator_VersionPropagation(string defaultVersion, string orchestratorName, string expectedVersion) + { + using (ITestHost host = TestHelpers.GetJobHost( + this.loggerProvider, + nameof(this.SubOrchestrator_VersionPropagation), + enableExtendedSessions: false, + options: new DurableTaskOptions { DefaultVersion = defaultVersion })) + { + await host.StartAsync(); + + var client = await host.StartOrchestratorAsync(orchestratorName, null, this.output); + var status = await client.WaitForCompletionAsync(this.output, timeout: TimeSpan.FromMinutes(1)); + + Assert.Equal(OrchestrationRuntimeStatus.Completed, status.RuntimeStatus); + Assert.Equal($"Sub-orchestration version: {expectedVersion}", status.Output.ToString()); + await host.StopAsync(); + } + } } } diff --git a/test/Common/TestOrchestrations.cs b/test/Common/TestOrchestrations.cs index 0580a34f8..641c89d14 100644 --- a/test/Common/TestOrchestrations.cs +++ b/test/Common/TestOrchestrations.cs @@ -1599,5 +1599,27 @@ public static async Task GetOrchestrationVersion_SubOrchestrator([Orches // Return both the orchestration version and entity info return await Task.FromResult($"{JsonSerializer.Serialize(ctx.Version)}; Sub-orchestration from entity: {entityResult}"); } + + public static async Task CallSubOrchestratorWithExplicitVersion([OrchestrationTrigger] IDurableOrchestrationContext ctx, ILogger log) + { + // Call sub-orchestrator with explicit version using FunctionNameWithVersion.Combine + string functionNameWithVersion = FunctionNameWithVersion.Combine(nameof(SimpleSubOrchestrator), "V2.0"); + string result = await ctx.CallSubOrchestratorAsync(functionNameWithVersion, null); + return result; + } + + public static async Task CallSubOrchestratorWithoutVersion([OrchestrationTrigger] IDurableOrchestrationContext ctx, ILogger log) + { + // Call sub-orchestrator without version - version resolution depends on DefaultVersion config + string result = await ctx.CallSubOrchestratorAsync(nameof(SimpleSubOrchestrator), null); + return result; + } + + public static Task SimpleSubOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext ctx, ILogger log) + { + // Simple sub-orchestrator that returns its version + string version = JsonSerializer.Serialize(ctx.Version); + return Task.FromResult($"Sub-orchestration version: {version}"); + } } } diff --git a/test/FunctionsV2/FunctionNameWithVersionTests.cs b/test/FunctionsV2/FunctionNameWithVersionTests.cs new file mode 100644 index 000000000..e516daf60 --- /dev/null +++ b/test/FunctionsV2/FunctionNameWithVersionTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Xunit; + +namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Tests +{ + public class FunctionNameWithVersionTests + { + [Theory] + [InlineData("MyOrchestrator", null)] // Without version + [InlineData("MyOrchestrator", "v2.5.1")] // With version + [InlineData("Function", "1.0.0")] // Semantic version with major.minor.patch + [InlineData("Function", "")] // Empty string version + [InlineData("Complex_Name-123", "v3.2.1-beta+build")] // Complex names and versions + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + public void Combine_And_Parse_PreserveValues(string originalName, string originalVersion) + { + string combined = FunctionNameWithVersion.Combine(originalName, originalVersion); + (string parsedName, string parsedVersion) = FunctionNameWithVersion.Parse(combined); + + Assert.Equal(originalName, parsedName); + if (originalVersion == null) + { + Assert.Null(parsedVersion); + } + else + { + Assert.Equal(originalVersion, parsedVersion); + } + } + } +} diff --git a/test/FunctionsV2/OutOfProcTests.cs b/test/FunctionsV2/OutOfProcTests.cs index e459a324c..05c0b949e 100644 --- a/test/FunctionsV2/OutOfProcTests.cs +++ b/test/FunctionsV2/OutOfProcTests.cs @@ -415,5 +415,90 @@ public void WorkerRuntimeTypeFollowsSpec(string workerRuntime) runtimeType.ToString().Equals(workerRuntime, StringComparison.OrdinalIgnoreCase); } } + + [Theory] + [InlineData(false, "2.0", null, "MySubOrchestrator\n2.0")] // Explicit version + [InlineData(false, null, null, "MySubOrchestrator")] // Null version - no delimiter + [InlineData(false, "", null, "MySubOrchestrator\n")] // Empty version - delimiter included + [InlineData(false, "1.0.0", null, "MySubOrchestrator\n1.0.0")] // Semantic version + [InlineData(false, "4.5.6-preview", null, "MySubOrchestrator\n4.5.6-preview")] // Pre-release version + [InlineData(false, "2.0-beta.1", null, "MySubOrchestrator\n2.0-beta.1")] // Beta version + [InlineData(false, "v1.2.3", null, "MySubOrchestrator\nv1.2.3")] // Version with prefix + [InlineData(true, "3.5.1", null, "MySubOrchestrator\n3.5.1")] // Explicit version with retry + [InlineData(true, null, null, "MySubOrchestrator")] // Null version with retry + [InlineData(false, "5.0", "V2", "MySubOrchestrator\n5.0")] // Schema V2 + [InlineData(false, "5.0", "V3", "MySubOrchestrator\n5.0")] // Schema V3 + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + public async Task CallSubOrchestrator_VersionHandling_OutOfProc(bool withRetry, string version, string schemaVersion, string expectedFunctionName) + { + string capturedFunctionName = null; + RetryOptions capturedRetryOptions = null; + + // Mock the CallSubOrchestratorAsync or CallSubOrchestratorWithRetryAsync API + var contextMock = new Mock(); + + if (withRetry) + { + contextMock + .Setup(ctx => ctx.CallSubOrchestratorWithRetryAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback((name, retry, instanceId, input) => + { + capturedFunctionName = name; + capturedRetryOptions = retry; + }) + .Returns(Task.CompletedTask); + } + else + { + contextMock + .Setup(ctx => ctx.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, instanceId, input) => capturedFunctionName = name) + .Returns(Task.CompletedTask); + } + + var shim = new OutOfProcOrchestrationShim(contextMock.Object); + + var actionType = withRetry ? "CallSubOrchestratorWithRetry" : "CallSubOrchestrator"; + var versionField = version == null ? string.Empty : $@"""version"": ""{version}"","; + var schemaVersionField = schemaVersion == null ? string.Empty : $@"""schemaVersion"": ""{schemaVersion}"","; + var retryField = withRetry ? @"""retryOptions"": { + ""firstRetryIntervalInMilliseconds"": 1000, + ""maxNumberOfAttempts"": 3 + }," : string.Empty; + + var executionJson = $@" +{{ + ""isDone"": false, + {schemaVersionField} + ""actions"": [ + [{{ + ""actionType"": ""{actionType}"", + ""functionName"": ""MySubOrchestrator"", + {versionField} + ""instanceId"": ""test-instance"", + {retryField} + ""input"": null + }}] + ] +}}"; + + var jsonObject = JObject.Parse(executionJson); + OrchestrationInvocationResult result = new OrchestrationInvocationResult(jsonObject); + bool moreWork = await shim.ScheduleDurableTaskEvents(result); + + Assert.True(moreWork); + Assert.NotNull(capturedFunctionName); + Assert.Equal(expectedFunctionName, capturedFunctionName); + + if (withRetry) + { + Assert.NotNull(capturedRetryOptions); + Assert.Equal(3, capturedRetryOptions.MaxNumberOfAttempts); + } + } } } From 2635efdd1651545cbbbd01c8a31fd8280465d57d Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Thu, 2 Oct 2025 18:51:02 -0700 Subject: [PATCH 2/5] Fix SubOrchestrator_VersionPropagation --- test/Common/OrchestrationVersionTests.cs | 2 +- test/Common/TestOrchestrations.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Common/OrchestrationVersionTests.cs b/test/Common/OrchestrationVersionTests.cs index 6ffbe2ec1..f6135a97f 100644 --- a/test/Common/OrchestrationVersionTests.cs +++ b/test/Common/OrchestrationVersionTests.cs @@ -89,7 +89,7 @@ ITestHost GetJobHost(string taskHubName, string defaultVersion) [Theory] [Trait("Category", PlatformSpecificHelpers.TestCategory)] - [InlineData("4.0", nameof(TestOrchestrations.CallSubOrchestratorWithExplicitVersion), "\"V2.0\"")] // Explicit version overrides default + [InlineData("4.0", nameof(TestOrchestrations.CallSubOrchestratorWithExplicitVersion), "\"2.0\"")] // Explicit version overrides default [InlineData("4.0", nameof(TestOrchestrations.CallSubOrchestratorWithoutVersion), "\"4.0\"")] // Default version propagated when no explicit version [InlineData(null, nameof(TestOrchestrations.CallSubOrchestratorWithoutVersion), "null")] // Null version when no default configured public async Task SubOrchestrator_VersionPropagation(string defaultVersion, string orchestratorName, string expectedVersion) diff --git a/test/Common/TestOrchestrations.cs b/test/Common/TestOrchestrations.cs index 641c89d14..f76cf4956 100644 --- a/test/Common/TestOrchestrations.cs +++ b/test/Common/TestOrchestrations.cs @@ -1603,7 +1603,7 @@ public static async Task GetOrchestrationVersion_SubOrchestrator([Orches public static async Task CallSubOrchestratorWithExplicitVersion([OrchestrationTrigger] IDurableOrchestrationContext ctx, ILogger log) { // Call sub-orchestrator with explicit version using FunctionNameWithVersion.Combine - string functionNameWithVersion = FunctionNameWithVersion.Combine(nameof(SimpleSubOrchestrator), "V2.0"); + string functionNameWithVersion = FunctionNameWithVersion.Combine(nameof(SimpleSubOrchestrator), "2.0"); string result = await ctx.CallSubOrchestratorAsync(functionNameWithVersion, null); return result; } From b405e83655ddd1f0a99b87a10eefd50434eb2f69 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Thu, 2 Oct 2025 20:58:24 -0700 Subject: [PATCH 3/5] Remove unnecessary tests --- test/Common/OrchestrationVersionTests.cs | 24 ------------------------ test/Common/TestOrchestrations.cs | 22 ---------------------- 2 files changed, 46 deletions(-) diff --git a/test/Common/OrchestrationVersionTests.cs b/test/Common/OrchestrationVersionTests.cs index f6135a97f..3f323f31e 100644 --- a/test/Common/OrchestrationVersionTests.cs +++ b/test/Common/OrchestrationVersionTests.cs @@ -86,29 +86,5 @@ ITestHost GetJobHost(string taskHubName, string defaultVersion) options: new DurableTaskOptions { DefaultVersion = defaultVersion }); } } - - [Theory] - [Trait("Category", PlatformSpecificHelpers.TestCategory)] - [InlineData("4.0", nameof(TestOrchestrations.CallSubOrchestratorWithExplicitVersion), "\"2.0\"")] // Explicit version overrides default - [InlineData("4.0", nameof(TestOrchestrations.CallSubOrchestratorWithoutVersion), "\"4.0\"")] // Default version propagated when no explicit version - [InlineData(null, nameof(TestOrchestrations.CallSubOrchestratorWithoutVersion), "null")] // Null version when no default configured - public async Task SubOrchestrator_VersionPropagation(string defaultVersion, string orchestratorName, string expectedVersion) - { - using (ITestHost host = TestHelpers.GetJobHost( - this.loggerProvider, - nameof(this.SubOrchestrator_VersionPropagation), - enableExtendedSessions: false, - options: new DurableTaskOptions { DefaultVersion = defaultVersion })) - { - await host.StartAsync(); - - var client = await host.StartOrchestratorAsync(orchestratorName, null, this.output); - var status = await client.WaitForCompletionAsync(this.output, timeout: TimeSpan.FromMinutes(1)); - - Assert.Equal(OrchestrationRuntimeStatus.Completed, status.RuntimeStatus); - Assert.Equal($"Sub-orchestration version: {expectedVersion}", status.Output.ToString()); - await host.StopAsync(); - } - } } } diff --git a/test/Common/TestOrchestrations.cs b/test/Common/TestOrchestrations.cs index f76cf4956..0580a34f8 100644 --- a/test/Common/TestOrchestrations.cs +++ b/test/Common/TestOrchestrations.cs @@ -1599,27 +1599,5 @@ public static async Task GetOrchestrationVersion_SubOrchestrator([Orches // Return both the orchestration version and entity info return await Task.FromResult($"{JsonSerializer.Serialize(ctx.Version)}; Sub-orchestration from entity: {entityResult}"); } - - public static async Task CallSubOrchestratorWithExplicitVersion([OrchestrationTrigger] IDurableOrchestrationContext ctx, ILogger log) - { - // Call sub-orchestrator with explicit version using FunctionNameWithVersion.Combine - string functionNameWithVersion = FunctionNameWithVersion.Combine(nameof(SimpleSubOrchestrator), "2.0"); - string result = await ctx.CallSubOrchestratorAsync(functionNameWithVersion, null); - return result; - } - - public static async Task CallSubOrchestratorWithoutVersion([OrchestrationTrigger] IDurableOrchestrationContext ctx, ILogger log) - { - // Call sub-orchestrator without version - version resolution depends on DefaultVersion config - string result = await ctx.CallSubOrchestratorAsync(nameof(SimpleSubOrchestrator), null); - return result; - } - - public static Task SimpleSubOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext ctx, ILogger log) - { - // Simple sub-orchestrator that returns its version - string version = JsonSerializer.Serialize(ctx.Version); - return Task.FromResult($"Sub-orchestration version: {version}"); - } } } From 77801ab56fea93825fcfafe6c0759b153e919efb Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Thu, 2 Oct 2025 20:58:38 -0700 Subject: [PATCH 4/5] Improve formatting --- test/FunctionsV2/OutOfProcTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/FunctionsV2/OutOfProcTests.cs b/test/FunctionsV2/OutOfProcTests.cs index 05c0b949e..d8dbb20c4 100644 --- a/test/FunctionsV2/OutOfProcTests.cs +++ b/test/FunctionsV2/OutOfProcTests.cs @@ -455,8 +455,12 @@ public async Task CallSubOrchestrator_VersionHandling_OutOfProc(bool withRetry, else { contextMock - .Setup(ctx => ctx.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((name, instanceId, input) => capturedFunctionName = name) + .Setup(ctx => ctx.CallSubOrchestratorAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback((name, instanceId, input) => + capturedFunctionName = name) .Returns(Task.CompletedTask); } From 0b999a8d6399482e60ca12c04efad35cb8696d6a Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Fri, 3 Oct 2025 13:27:08 -0700 Subject: [PATCH 5/5] Update release notes to include orchestration version override feature for PowerShell, Python, and Node.js --- release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release_notes.md b/release_notes.md index ecf2ce1f0..4db7e8f13 100644 --- a/release_notes.md +++ b/release_notes.md @@ -14,7 +14,7 @@ ### New Features -- Initialize linux logging in Managed Environment (Azure Container Apps) scenarios. Emit MS_DURABLE_FUNCTION_EVENTS_LOGS in these environments. +- Allow overriding orchestration version when starting orchestrations via APIs in PowerShell, Python, and Node.js (https://github.com/Azure/azure-functions-durable-extension/pull/3213) ### Bug Fixes