diff --git a/CHANGELOG.md b/CHANGELOG.md
index afc52e960..ffc961a04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- Add user agent header to gRPC called in ([#417](https://github.com/microsoft/durabletask-dotnet/pull/417))
- Enrich User-Agent Header in gRPC Metadata to indicate Client or Worker as caller ([#421](https://github.com/microsoft/durabletask-dotnet/pull/421))
- Add extension methods for registering entities by type ([#427](https://github.com/microsoft/durabletask-dotnet/pull/427))
+- Add TaskVersion and utilize it for version overrides when starting orchestrations ([#416](https://github.com/microsoft/durabletask-dotnet/pull/416))
## v1.10.0
diff --git a/src/Abstractions/TaskName.cs b/src/Abstractions/TaskName.cs
index 62d36f562..2ee9dcdf4 100644
--- a/src/Abstractions/TaskName.cs
+++ b/src/Abstractions/TaskName.cs
@@ -16,6 +16,7 @@ namespace Microsoft.DurableTask;
/// The name of the task. Providing null will yield the default struct.
public TaskName(string name)
{
+#pragma warning disable 0618
if (name is null)
{
// Force the default struct when null is passed in.
@@ -27,6 +28,7 @@ public TaskName(string name)
this.Name = name;
this.Version = string.Empty; // expose setting Version only when we actually consume it.
}
+#pragma warning restore 0618
}
///
@@ -44,6 +46,7 @@ public TaskName(string name)
/// Task versions is currently locked to as it is not yet integrated into task
/// identification. This is being left here as we intend to support it soon.
///
+ [Obsolete("Refer to TaskVersion instead.")]
public string Version { get; }
///
@@ -122,6 +125,7 @@ public override int GetHashCode()
/// The name and optional version of the current instance.
public override string ToString()
{
+#pragma warning disable 0618
if (string.IsNullOrEmpty(this.Version))
{
return this.Name;
@@ -130,5 +134,6 @@ public override string ToString()
{
return this.Name + ":" + this.Version;
}
+#pragma warning restore 0618
}
}
diff --git a/src/Abstractions/TaskOptions.cs b/src/Abstractions/TaskOptions.cs
index d3e06e5ca..6afd5f708 100644
--- a/src/Abstractions/TaskOptions.cs
+++ b/src/Abstractions/TaskOptions.cs
@@ -90,6 +90,11 @@ public SubOrchestrationOptions(TaskOptions options, string? instanceId = null)
/// Gets the orchestration instance ID.
///
public string? InstanceId { get; init; }
+
+ ///
+ /// Gets the version to associate with the sub-orchestration instance.
+ ///
+ public TaskVersion Version { get; init; } = default!;
}
///
@@ -108,4 +113,9 @@ public record StartOrchestrationOptions(string? InstanceId = null, DateTimeOffse
/// Gets the tags to associate with the orchestration instance.
///
public IReadOnlyDictionary Tags { get; init; } = ImmutableDictionary.Create();
+
+ ///
+ /// Gets the version to associate with the orchestration instance.
+ ///
+ public TaskVersion? Version { get; init; }
}
diff --git a/src/Abstractions/TaskVersion.cs b/src/Abstractions/TaskVersion.cs
new file mode 100644
index 000000000..5d6d1bfec
--- /dev/null
+++ b/src/Abstractions/TaskVersion.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.DurableTask;
+
+///
+/// The version of a durable task.
+///
+public readonly struct TaskVersion : IEquatable
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The version of the task. Providing null will result in the default struct.
+ public TaskVersion(string version)
+ {
+ if (version == null)
+ {
+ this.Version = null!;
+ }
+ else
+ {
+ this.Version = version;
+ }
+ }
+
+ ///
+ /// Gets the version of a task.
+ ///
+ public string Version { get; }
+
+ ///
+ /// Implicitly converts a into a of the property value.
+ ///
+ /// The to be converted into a .
+ public static implicit operator string(TaskVersion value) => value.Version;
+
+ ///
+ /// Implicitly converts a into a .
+ ///
+ /// The to convert into a .
+ public static implicit operator TaskVersion(string value) => new TaskVersion(value);
+
+ ///
+ /// Compares two structs for equality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ /// true if the two objects are equal; otherwise false.
+ public static bool operator ==(TaskVersion a, TaskVersion b)
+ {
+ return a.Equals(b);
+ }
+
+ ///
+ /// Compares two structs for inequality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ /// false if the two objects are equal; otherwise true.
+ public static bool operator !=(TaskVersion a, TaskVersion b)
+ {
+ return !a.Equals(b);
+ }
+
+ ///
+ /// Gets a value indicating whether to objects
+ /// are equal using value semantics.
+ ///
+ /// The other to compare to.
+ /// true if the two are equal using value semantics; otherwise false.
+ public bool Equals(TaskVersion other)
+ {
+ return string.Equals(this.Version, other.Version, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Gets a value indicating whether to objects
+ /// are equal using value semantics.
+ ///
+ /// The other object to compare to.
+ /// true if the two objects are equal using value semantics; otherwise false.
+ public override bool Equals(object? obj)
+ {
+ if (obj is not TaskVersion other)
+ {
+ return false;
+ }
+
+ return this.Equals(other);
+ }
+
+ ///
+ /// Calculates a hash code value for the current instance.
+ ///
+ /// A 32-bit hash code value.
+ public override int GetHashCode()
+ {
+ return StringComparer.OrdinalIgnoreCase.GetHashCode(this.Version);
+ }
+}
diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs
index a4960ae93..6246435be 100644
--- a/src/Client/Grpc/GrpcDurableTaskClient.cs
+++ b/src/Client/Grpc/GrpcDurableTaskClient.cs
@@ -78,10 +78,11 @@ public override async Task ScheduleNewOrchestrationInstanceAsync(
{
Check.NotEntity(this.options.EnableEntitySupport, options?.InstanceId);
+ // We're explicitly OK with an empty version from the options as that had to be explicitly set. It should take precedence over the default.
string version = string.Empty;
- if (!string.IsNullOrEmpty(orchestratorName.Version))
+ if (options?.Version is { } v)
{
- version = orchestratorName.Version;
+ version = v;
}
else if (!string.IsNullOrEmpty(this.options.DefaultVersion))
{
diff --git a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs
index fe7625a88..bcf8eb98a 100644
--- a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs
+++ b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs
@@ -172,7 +172,7 @@ public override async Task ScheduleNewOrchestrationInstanceAsync(
Event = new ExecutionStartedEvent(-1, serializedInput)
{
Name = orchestratorName.Name,
- Version = orchestratorName.Version,
+ Version = options?.Version ?? string.Empty,
OrchestrationInstance = instance,
ScheduledStartTime = options?.StartAt?.UtcDateTime,
Tags = options?.Tags != null ? options.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) : null,
diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs
index 322e7d403..ee065f528 100644
--- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs
+++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs
@@ -102,6 +102,7 @@ public static IDurableTaskWorkerBuilder UseVersioning(this IDurableTaskWorkerBui
options.Versioning = new VersioningOptions
{
Version = versionOptions.Version,
+ DefaultVersion = versionOptions.DefaultVersion,
MatchStrategy = versionOptions.MatchStrategy,
FailureStrategy = versionOptions.FailureStrategy,
};
diff --git a/src/Worker/Core/DurableTaskWorkerOptions.cs b/src/Worker/Core/DurableTaskWorkerOptions.cs
index 395666c9a..b2bdca04b 100644
--- a/src/Worker/Core/DurableTaskWorkerOptions.cs
+++ b/src/Worker/Core/DurableTaskWorkerOptions.cs
@@ -203,6 +203,11 @@ public class VersioningOptions
///
public string Version { get; set; } = string.Empty;
+ ///
+ /// Gets or sets the default version that will be used for starting new orchestrations.
+ ///
+ public string DefaultVersion { get; set; } = string.Empty;
+
///
/// Gets or sets the versioning strategy for the Durable Task worker.
///
diff --git a/src/Worker/Core/Shims/TaskEntityShim.cs b/src/Worker/Core/Shims/TaskEntityShim.cs
index 800b42bb6..8f47a88b2 100644
--- a/src/Worker/Core/Shims/TaskEntityShim.cs
+++ b/src/Worker/Core/Shims/TaskEntityShim.cs
@@ -221,7 +221,7 @@ public override string ScheduleNewOrchestration(TaskName name, object? input = n
this.operationActions.Add(new StartNewOrchestrationOperationAction()
{
Name = name.Name,
- Version = name.Version,
+ Version = options?.Version ?? string.Empty,
InstanceId = instanceId,
Input = this.dataConverter.Serialize(input),
ScheduledStartTime = options?.StartAt?.UtcDateTime,
diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
index a964020de..205ebb763 100644
--- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
+++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
@@ -139,6 +139,7 @@ public override async Task CallActivityAsync(
try
{
// TODO: Cancellation (https://github.com/microsoft/durabletask-dotnet/issues/7)
+#pragma warning disable 0618
if (options?.Retry?.Policy is RetryPolicy policy)
{
return await this.innerContext.ScheduleWithRetry(
@@ -165,6 +166,7 @@ public override async Task CallActivityAsync(
// Hide the core DTFx types and instead use our own
throw new TaskFailedException(name, e.ScheduleId, e);
}
+#pragma warning restore 0618
}
///
@@ -177,10 +179,12 @@ public override async Task CallSubOrchestratorAsync(
static string? GetInstanceId(TaskOptions? options)
=> options is SubOrchestrationOptions derived ? derived.InstanceId : null;
string instanceId = GetInstanceId(options) ?? this.NewGuid().ToString("N");
+ string defaultVersion = this.invocationContext.Options?.Versioning?.DefaultVersion ?? string.Empty;
+ string version = options is SubOrchestrationOptions subOptions ? subOptions.Version : defaultVersion;
Check.NotEntity(this.invocationContext.Options.EnableEntitySupport, instanceId);
- // if this orchestration uses entities, first validate that the suborchsestration call is allowed in the current context
+ // if this orchestration uses entities, first validate that the suborchestration call is allowed in the current context
if (this.entityFeature != null && !this.entityFeature.EntityContext.ValidateSuborchestrationTransition(out string? errorMsg))
{
throw new InvalidOperationException(errorMsg);
@@ -192,7 +196,7 @@ public override async Task CallSubOrchestratorAsync(
{
return await this.innerContext.CreateSubOrchestrationInstanceWithRetry(
orchestratorName.Name,
- orchestratorName.Version,
+ version,
instanceId,
policy.ToDurableTaskCoreRetryOptions(),
input);
@@ -202,7 +206,7 @@ public override async Task CallSubOrchestratorAsync(
return await this.InvokeWithCustomRetryHandler(
() => this.innerContext.CreateSubOrchestrationInstance(
orchestratorName.Name,
- orchestratorName.Version,
+ version,
instanceId,
input),
orchestratorName.Name,
@@ -213,7 +217,7 @@ public override async Task CallSubOrchestratorAsync(
{
return await this.innerContext.CreateSubOrchestrationInstance(
orchestratorName.Name,
- orchestratorName.Version,
+ version,
instanceId,
input);
}
diff --git a/test/Abstractions.Tests/TaskNameTests.cs b/test/Abstractions.Tests/TaskNameTests.cs
index b5cfc4350..fff8530d6 100644
--- a/test/Abstractions.Tests/TaskNameTests.cs
+++ b/test/Abstractions.Tests/TaskNameTests.cs
@@ -17,7 +17,9 @@ public void Ctor_EmptyName_Okay()
{
TaskName name = new(string.Empty);
name.Name.Should().Be(string.Empty);
+#pragma warning disable 0618
name.Version.Should().Be(string.Empty);
+#pragma warning restore 0618
}
[Theory]
diff --git a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs
index a9a5d7caa..5eba17ede 100644
--- a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs
+++ b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs
@@ -400,7 +400,7 @@ static TaskMessage MatchStartExecutionMessage(TaskName name, object? input, Star
}
return Guid.TryParse(m.OrchestrationInstance.ExecutionId, out _)
- && @event.Name == name.Name && @event.Version == name.Version
+ && @event.Name == name.Name && @event.Version == (options?.Version ?? string.Empty)
&& @event.OrchestrationInstance == m.OrchestrationInstance
&& @event.EventId == -1
&& @event.Input == JsonDataConverter.Default.Serialize(input);
diff --git a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs
index 634427bc6..b64a5fa87 100644
--- a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs
+++ b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs
@@ -3,6 +3,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
+using Microsoft.DurableTask.Abstractions;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Tests.Logging;
using Microsoft.DurableTask.Worker;
@@ -213,7 +214,7 @@ public async Task SingleActivity()
IReadOnlyCollection workerLogs = this.GetLogs("Microsoft.DurableTask.Worker");
Assert.NotEmpty(workerLogs);
-
+
// Validate logs.
Assert.Single(workerLogs, log => MatchLog(
log,
@@ -652,7 +653,6 @@ public async Task OrchestrationVersionPassedThroughContext()
Assert.NotNull(output);
Assert.Equal(output, $"Orchestration version: {version}");
-
}
[Fact]
@@ -834,6 +834,116 @@ public async Task OrchestrationVersioning_MatchTypeCurrentOrOlder_VersionSuccess
Assert.Equal(output, $"Orchestration version: {clientVersion}");
}
+ [Fact]
+ public async Task SubOrchestrationInheritsDefaultVersion()
+ {
+ var version = "0.1";
+ await using HostTestLifetime server = await this.StartWorkerAsync(b =>
+ {
+ b.AddTasks(tasks => tasks
+ .AddOrchestratorFunc("Versioned_Orchestration", (ctx, input) =>
+ {
+ return ctx.CallSubOrchestratorAsync("Versioned_Sub_Orchestration");
+ })
+ .AddOrchestratorFunc("Versioned_Sub_Orchestration", (ctx, input) =>
+ {
+ return ctx.CallActivityAsync("Versioned_Activity", ctx.Version);
+ })
+ .AddActivityFunc("Versioned_Activity", (ctx, input) =>
+ {
+ return $"Sub Orchestration version: {input}";
+ }));
+ b.UseVersioning(new DurableTaskWorkerOptions.VersioningOptions
+ {
+ DefaultVersion = version
+ });
+ }, c =>
+ {
+ c.UseDefaultVersion(version);
+ });
+
+ var instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync("Versioned_Orchestration", input: string.Empty);
+ var result = await server.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, this.TimeoutToken);
+ var output = result.ReadOutputAs();
+
+ Assert.NotNull(output);
+ Assert.Equal($"Sub Orchestration version: {version}", output);
+ }
+
+ [Theory]
+ [InlineData("0.2")]
+ [InlineData("")]
+ public async Task OrchestrationTaskVersionOverridesDefaultVersion(string overrideVersion)
+ {
+ var version = "0.1";
+ await using HostTestLifetime server = await this.StartWorkerAsync(b =>
+ {
+ b.AddTasks(tasks => tasks
+ .AddOrchestratorFunc("Versioned_Orchestration", (ctx, input) =>
+ {
+ return ctx.CallActivityAsync("Versioned_Activity", ctx.Version);
+ })
+ .AddActivityFunc("Versioned_Activity", (ctx, input) =>
+ {
+ return $"Orchestration version: {input}";
+ }));
+ }, c =>
+ {
+ c.UseDefaultVersion(version);
+ });
+
+ var instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync("Versioned_Orchestration", string.Empty, new StartOrchestrationOptions
+ {
+ Version = overrideVersion
+ });
+ var result = await server.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, this.TimeoutToken);
+ var output = result.ReadOutputAs();
+
+ Assert.NotNull(output);
+ Assert.Equal($"Orchestration version: {overrideVersion}", output);
+ }
+
+ [Theory]
+ [InlineData("0.2")]
+ [InlineData("")]
+ public async Task SubOrchestrationTaskVersionOverridesDefaultVersion(string overrideVersion)
+ {
+ var version = "0.1";
+ await using HostTestLifetime server = await this.StartWorkerAsync(b =>
+ {
+ b.AddTasks(tasks => tasks
+ .AddOrchestratorFunc("Versioned_Orchestration", (ctx, input) =>
+ {
+ return ctx.CallSubOrchestratorAsync("Versioned_Sub_Orchestration", new SubOrchestrationOptions
+ {
+ Version = overrideVersion
+ });
+ })
+ .AddOrchestratorFunc("Versioned_Sub_Orchestration", (ctx, input) =>
+ {
+ return ctx.CallActivityAsync("Versioned_Activity", ctx.Version);
+ })
+ .AddActivityFunc("Versioned_Activity", (ctx, input) =>
+ {
+ return $"Sub Orchestration version: {input}";
+ }));
+ b.UseVersioning(new DurableTaskWorkerOptions.VersioningOptions
+ {
+ DefaultVersion = version,
+ });
+ }, c =>
+ {
+ c.UseDefaultVersion(version);
+ });
+
+ var instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync("Versioned_Orchestration", input: string.Empty);
+ var result = await server.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, this.TimeoutToken);
+ var output = result.ReadOutputAs();
+
+ Assert.NotNull(output);
+ Assert.Equal($"Sub Orchestration version: {overrideVersion}", output);
+ }
+
// 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