Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/AzureFunctions.PowerShell.Durable.SDK.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ function Start-DurableOrchestration {

[Parameter(
ValueFromPipelineByPropertyName=$true)]
[string] $InstanceId
[string] $InstanceId,

[Parameter(
ValueFromPipelineByPropertyName=$true)]
[string] $Version
)

$ErrorActionPreference = 'Stop'
Expand All @@ -126,6 +130,12 @@ function Start-DurableOrchestration {
$UriTemplate.Replace('{functionName}', $FunctionName).Replace('[/{instanceId}]', "/$InstanceId")
}

# Add version parameter to query string if provided
if ($Version) {
$separator = if ($Uri.Contains('?')) { '&' } else { '?' }
$Uri += "$separator" + "version=$([System.Web.HttpUtility]::UrlEncode($Version))"
}

$Body = $InputObject | ConvertTo-Json -Compress -Depth 100

$null = Invoke-RestMethod -Uri $Uri -Method 'POST' -ContentType 'application/json' -Body $Body -Headers $headers
Expand Down
8 changes: 7 additions & 1 deletion src/DurableEngine/Actions/CallSubOrchestratorAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@ internal class CallSubOrchestratorAction : OrchestrationAction
/// </summary>
public readonly object Input;

internal CallSubOrchestratorAction(string functionName, object input, string instanceId)
/// <summary>
/// The version of the sub-orchestrator function.
/// </summary>
public readonly string Version;

internal CallSubOrchestratorAction(string functionName, object input, string instanceId, string version)
: base(ActionType.CallSubOrchestrator)
{
FunctionName = functionName;
Input = input;
InstanceId = instanceId;
Version = version;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ internal class CallSubOrchestratorWithRetryAction : OrchestrationAction
/// </summary>
public readonly Dictionary<string, object> RetryOptions;

internal CallSubOrchestratorWithRetryAction(string functionName, object input, string instanceId, RetryPolicy retryOptions)
/// <summary>
/// The version of the sub-orchestrator function.
/// </summary>
public readonly string Version;

internal CallSubOrchestratorWithRetryAction(string functionName, object input, string instanceId, RetryPolicy retryOptions, string version)
: base(ActionType.CallSubOrchestratorWithRetry)
{
FunctionName = functionName;
InstanceId = instanceId;
Input = input;
RetryOptions = retryOptions.RetryPolicyDictionary;
Version = version;
}
}
}
4 changes: 2 additions & 2 deletions src/DurableEngine/DurableEngine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.DurableTask.Client" Version="1.11.0" />
<PackageReference Include="Microsoft.DurableTask.Worker" Version="1.11.0" />
<PackageReference Include="Microsoft.DurableTask.Client" Version="1.15.1" />
<PackageReference Include="Microsoft.DurableTask.Worker" Version="1.15.1" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.4.10" PrivateAssets="all" />
</ItemGroup>
</Project>
15 changes: 12 additions & 3 deletions src/DurableEngine/Tasks/SubOrchestratorTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ public class SubOrchestratorTask : DurableTask

private RetryPolicy RetryOptions { get; }

internal string Version { get; }

public SubOrchestratorTask(
string functionName,
string instanceId,
object functionInput,
RetryPolicy retryOptions,
string version,
SwitchParameter noWait,
Hashtable privateData) : base(noWait, privateData)
{
FunctionName = functionName;
InstanceId = instanceId;
Input = functionInput;
RetryOptions = retryOptions;
Version = version;
}

internal override Task<object> CreateDTFxTask()
Expand All @@ -45,14 +49,19 @@ internal override Task<object> CreateDTFxTask()
? taskOptions :
taskOptions.WithInstanceId(InstanceId);

return DTFxContext.CallSubOrchestratorAsync<object>(FunctionName, Input, taskOptions);
var subOrchestrationOptions = new SubOrchestrationOptions(taskOptions, InstanceId)
{
Version = this.Version
};

return DTFxContext.CallSubOrchestratorAsync<object>(FunctionName, Input, subOrchestrationOptions);
}

internal override OrchestrationAction CreateOrchestrationAction()
{
return RetryOptions == null
? new CallSubOrchestratorAction(FunctionName, Input, InstanceId)
: new CallSubOrchestratorWithRetryAction(FunctionName, Input, InstanceId, RetryOptions);
? new CallSubOrchestratorAction(FunctionName, Input, InstanceId, Version)
: new CallSubOrchestratorWithRetryAction(FunctionName, Input, InstanceId, RetryOptions, Version);
}
}
}
9 changes: 8 additions & 1 deletion src/DurableSDK/Commands/APIs/InvokeSubOrchestratorCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public class InvokeSubOrchestratorCommand : DurableSDKCmdlet
[ValidateNotNull]
public RetryPolicy RetryOptions { get; set; }

/// <summary>
/// Version of the SubOrchestrator to invoke.
/// </summary>
[Parameter]
[ValidateNotNull]
public string Version { get; set; }

/// <summary>
/// If provided, the Task will block and be scheduled immediately.
/// Otherwise, a Task object is returned and the Task is not scheduled yet.
Expand All @@ -53,7 +60,7 @@ public class InvokeSubOrchestratorCommand : DurableSDKCmdlet
internal override DurableTask CreateDurableTask()
{
var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData;
SubOrchestratorTask task = new SubOrchestratorTask(FunctionName, InstanceId, Input, RetryOptions, NoWait, privateData);
SubOrchestratorTask task = new SubOrchestratorTask(FunctionName, InstanceId, Input, RetryOptions, Version, NoWait, privateData);
return task;
}
}
Expand Down
28 changes: 27 additions & 1 deletion src/Help/Invoke-DurableSubOrchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Invokes a sub-orchestrator function.
## SYNTAX

```
Invoke-DurableSubOrchestrator -FunctionName <String> [-InstanceId <String>] [-Input <Object>] [-RetryOptions <RetryPolicy>] [-NoWait] [<CommonParameters>]
Invoke-DurableSubOrchestrator -FunctionName <String> [-InstanceId <String>] [-Input <Object>] [-RetryOptions <RetryPolicy>] [-Version <String>] [-NoWait] [<CommonParameters>]
```

## DESCRIPTION
Expand Down Expand Up @@ -46,6 +46,15 @@ Write-Host "Sub-orchestrator completed with result: $batchResult"

This example shows how to invoke a sub-orchestrator function asynchronously using -NoWait, which returns a task object that can be awaited later.

### Example 3 - Specifying a version

```powershell
$result = Invoke-DurableSubOrchestrator -FunctionName "ChildOrchestrator" -Version "2.0" -Input @{ ProcessId = "proc456" }
Write-Host "Sub-orchestrator (version 2.0) completed with result: $result"
```

This example shows how to invoke a sub-orchestrator with a specific version, overriding the default version configured in host.json.

## PARAMETERS

### -FunctionName
Expand Down Expand Up @@ -96,6 +105,22 @@ Accept pipeline input: False
Accept wildcard characters: False
```

### -Version

An optional version string for the sub-orchestrator function. When specified, this version overrides the default version configured in host.json for this specific sub-orchestrator invocation. This allows you to invoke specific versions of orchestrator functions.

```yaml
Type: String
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```

### -NoWait

When specified, the cmdlet returns a task object immediately without waiting for completion. By default, the cmdlet blocks and waits for the sub-orchestrator to complete before returning the result.
Expand Down Expand Up @@ -152,6 +177,7 @@ Returns the result of the sub-orchestrator execution by default. If -NoWait is s
- Use the -NoWait parameter when you need to invoke multiple sub-orchestrators concurrently.
- Sub-orchestrators inherit the fault-tolerance and replay characteristics of the parent orchestration.
- The sub-orchestrator function name must match a function defined in your Azure Functions app with an orchestration trigger.
- The -Version parameter allows you to invoke specific versions of orchestrator functions, overriding the default version from host.json.
- Consider using sub-orchestrators to break down complex workflows into manageable, reusable components.

## RELATED LINKS
Expand Down
24 changes: 23 additions & 1 deletion src/Help/Start-DurableOrchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Start a durable orchestration.
## SYNTAX

```
Start-DurableOrchestration [-FunctionName] <String> [[-InputObject] <Object>] [-DurableClient <Object>] [-InstanceId <String>] [<CommonParameters>]
Start-DurableOrchestration [-FunctionName] <String> [[-InputObject] <Object>] [-DurableClient <Object>] [-InstanceId <String>] [-Version <String>] [<CommonParameters>]
```

## DESCRIPTION
Expand Down Expand Up @@ -99,6 +99,24 @@ Accept pipeline input: True (ByPropertyName)
Accept wildcard characters: False
```

### -Version

Optional orchestration version.
The provided value will be available as `$Context.Version` within the orchestrator function context.
If not specified, the default version specified by the `defaultVersion` property in the Function app's host.json will be used.

```yaml
Type: String
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: None
Accept pipeline input: True (ByPropertyName)
Accept wildcard characters: False
```

### CommonParameters

This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
Expand All @@ -117,6 +135,10 @@ You can pipe objects to the -InputObject parameter to provide input data for the

You can pipe strings to the -InstanceId parameter to specify a custom instance ID for the orchestration.

### System.String (Version)

You can pipe strings to the -Version parameter to specify a version for the orchestration function.

## OUTPUTS

### System.String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task ActivityCanHaveQueueBinding()
Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode);

var initialResponseBodyString = await initialResponse.Content.ReadAsStringAsync();
dynamic initialResponseBody = JsonConvert.DeserializeObject(initialResponseBodyString);
dynamic initialResponseBody = JsonConvert.DeserializeObject(initialResponseBodyString)!;
var statusQueryGetUri = (string)initialResponseBody.statusQueryGetUri;

var startTime = DateTime.UtcNow;
Expand All @@ -41,7 +41,7 @@ public async Task ActivityCanHaveQueueBinding()
{
if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout)
{
Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}");
Assert.Fail($"The orchestration has not completed after {_orchestrationCompletionTimeout}");
}
await Task.Delay(TimeSpan.FromSeconds(2));
break;
Expand All @@ -56,7 +56,7 @@ public async Task ActivityCanHaveQueueBinding()
}

default:
Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}");
Assert.Fail($"Unexpected orchestration status code: {statusResponse.StatusCode}");
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class Constants
public static class Queue
{
public static string QueueName = "outqueue";
public static string StorageConnectionStringSetting = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
public static string StorageConnectionStringSetting = Environment.GetEnvironmentVariable("AzureWebJobsStorage") ?? "AzureWebJobsStorage placeholder";
public static string OutputBindingName = "test-output-ps";
public static string InputBindingName = "test-input-ps";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using AzureFunctions.PowerShell.Durable.SDK.Tests.E2E;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net;
using Xunit;

Expand Down Expand Up @@ -260,5 +261,57 @@ await ValidateDurableWorkflowResults(
Assert.Equal("SecondExternalEvent", finalStatusResponseBody.output[1].ToString());
});
}

[Theory]
[InlineData(null, null, "1.0", "1.0")] // No version specified, should use defaultVersion from host.json for both
[InlineData("0.5", null, "0.5", "1.0")] // Version specified for orchestrator, orchestrator should use it, suborchestrator should use defaultVersion
[InlineData(null, "0.7", "1.0", "0.7")] // Version specified for suborchestrator only, orchestrator should use defaultVersion, suborchestrator should use specified version
[InlineData("0.5", "0.7", "0.5", "0.7")] // Both versions specified, each should use their respective versions
public async Task OrchestrationVersionIsPropagatedToContext(
string orchestratorVersion,
string subOrchestratorVersion,
string expectedOrchestratorVersion,
string expectedSubOrchestratorVersion)
{
var queryParams = new List<string>();
if (orchestratorVersion != null)
queryParams.Add($"Version={orchestratorVersion}");
if (subOrchestratorVersion != null)
queryParams.Add($"SubOrchestratorVersion={subOrchestratorVersion}");

string queryString = queryParams.Count > 0 ? "?" + string.Join("&", queryParams) : string.Empty;

var initialResponse = await Utilities.GetHttpStartResponse("VersionedOrchestrator", queryString);
Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode);

var location = initialResponse.Headers.Location;
Assert.NotNull(location);

await ValidateDurableWorkflowResults(
initialResponse,
validateInitialResponse: (dynamic initialResponseBody) =>
{
Assert.NotNull(initialResponseBody.id);
var statusQueryGetUri = (string)initialResponseBody.statusQueryGetUri;
Assert.Equal(location?.ToString(), statusQueryGetUri);
Assert.NotNull(initialResponseBody.sendEventPostUri);
Assert.NotNull(initialResponseBody.purgeHistoryDeleteUri);
Assert.NotNull(initialResponseBody.terminatePostUri);
Assert.NotNull(initialResponseBody.rewindPostUri);
},
validateIntermediateResponse: (dynamic intermediateStatusResponseBody) =>
{
var runtimeStatus = (string)intermediateStatusResponseBody.runtimeStatus;
Assert.True(
runtimeStatus == "Running" || runtimeStatus == "Pending",
$"Unexpected runtime status: {runtimeStatus}");
},
validateFinalResponse: (dynamic finalStatusResponseBody) =>
{
Assert.Equal("Completed", (string)finalStatusResponseBody.runtimeStatus);
Assert.Equal(expectedOrchestratorVersion, finalStatusResponseBody.output[0].ToString());
Assert.Equal(expectedSubOrchestratorVersion, finalStatusResponseBody.output[1].ToString());
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected internal async Task ValidateDurableWorkflowResults(
Action<dynamic>? validateFinalResponse = null)
{
var initialResponseBodyString = await initialResponse.Content.ReadAsStringAsync();
dynamic initialResponseBody = JsonConvert.DeserializeObject(initialResponseBodyString);
dynamic initialResponseBody = JsonConvert.DeserializeObject(initialResponseBodyString)!;
var statusQueryGetUri = (string)initialResponseBody.statusQueryGetUri;

validateInitialResponse?.Invoke(initialResponseBody);
Expand All @@ -63,7 +63,7 @@ protected internal async Task ValidateDurableWorkflowResults(
{
if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout)
{
Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}");
Assert.Fail($"The orchestration has not completed after {_orchestrationCompletionTimeout}");
}

validateIntermediateResponse?.Invoke(statusResponseBody);
Expand All @@ -78,7 +78,7 @@ protected internal async Task ValidateDurableWorkflowResults(
}

default:
Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}");
Assert.Fail($"Unexpected orchestration status code: {statusResponse.StatusCode}");
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ await ValidateDurableWorkflowResults(
sendExternalEvents: async (HttpClient httpClient) =>
{
var initialResponseBodyString = await initialResponse.Content.ReadAsStringAsync();
dynamic initialResponseBody = JsonConvert.DeserializeObject(initialResponseBodyString);
dynamic initialResponseBody = JsonConvert.DeserializeObject(initialResponseBodyString)!;
var raiseEventUri = (string)initialResponseBody.sendEventPostUri;

raiseEventUri = raiseEventUri.Replace("{eventName}", "TESTEVENTNAME");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static async Task<HttpResponseMessage> GetHttpStartResponse(
public static async Task<dynamic> GetResponseBodyAsync(HttpResponseMessage response)
{
var responseBody = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject(responseBody);
return JsonConvert.DeserializeObject(responseBody)!;
}
}
}
Loading
Loading