Skip to content

Commit 0f8256d

Browse files
committed
Introduce versioning to the DurableTaskClient
This change adds a DefaultVersion to the DurableTaskClient builder options. When no other version is set, which is currently always the case, this value is used as the version of any new orchestration started. The value is then passed down to workers via the TaskOrchestrationContext. This allows users to specify a version during their app setup and then key off of the version during orchestration. Using conditional logic, changes can then be made to the orchestration without harming in-progress orchestrations. Signed-off-by: halspang <[email protected]>
1 parent a371ce6 commit 0f8256d

File tree

7 files changed

+97
-9
lines changed

7 files changed

+97
-9
lines changed

src/Abstractions/TaskOrchestrationContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public abstract class TaskOrchestrationContext
5959
/// </value>
6060
public abstract bool IsReplaying { get; }
6161

62+
/// <summary>
63+
/// Gets the version.
64+
/// </summary>
65+
public abstract string Version { get; }
66+
6267
/// <summary>
6368
/// Gets the entity feature, for interacting with entities.
6469
/// </summary>

src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,17 @@ public static IDurableTaskClientBuilder UseBuildTarget<TTarget, TOptions>(this I
8989
});
9090
return builder;
9191
}
92+
93+
/// <summary>
94+
/// Sets the default version for this builder. This version will be applied by default to all orchestrations if set.
95+
/// </summary>
96+
/// <param name="builder">The builder to set the version for.</param>
97+
/// <param name="version">The version that will be used as the default version.</param>
98+
/// <returns>The original builder, for call chaining.</returns>
99+
public static IDurableTaskClientBuilder UseDefaultVersion(this IDurableTaskClientBuilder builder, string version)
100+
{
101+
builder.Services.AddOptions<DurableTaskClientOptions>(builder.Name)
102+
.Configure(options => options.DefaultVersion = version);
103+
return builder;
104+
}
92105
}

src/Client/Core/DurableTaskClientOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ public class DurableTaskClientOptions
1313
DataConverter dataConverter = JsonDataConverter.Default;
1414
bool enableEntitySupport;
1515

16+
/// <summary>
17+
/// Gets or sets the version of orchestrations that will be created.
18+
/// <remarks>
19+
/// Currently, this is sourced from the AzureManaged client options.
20+
/// </remarks>
21+
/// </summary>
22+
public string DefaultVersion { get; set; } = string.Empty;
23+
1624
/// <summary>
1725
/// Gets or sets the data converter. Default value is <see cref="JsonDataConverter.Default" />.
1826
/// </summary>
@@ -95,6 +103,11 @@ internal void ApplyTo(DurableTaskClientOptions other)
95103
{
96104
other.EnableEntitySupport = this.EnableEntitySupport;
97105
}
106+
107+
if (!string.IsNullOrWhiteSpace(this.DefaultVersion))
108+
{
109+
other.DefaultVersion = this.DefaultVersion;
110+
}
98111
}
99112
}
100113
}

src/Client/Grpc/GrpcDurableTaskClient.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,20 @@ public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
7878
{
7979
Check.NotEntity(this.options.EnableEntitySupport, options?.InstanceId);
8080

81+
var version = string.Empty;
82+
if (!string.IsNullOrEmpty(orchestratorName.Version))
83+
{
84+
version = orchestratorName.Version;
85+
}
86+
else if (!string.IsNullOrEmpty(this.options.DefaultVersion))
87+
{
88+
version = this.options.DefaultVersion;
89+
}
90+
8191
var request = new P.CreateInstanceRequest
8292
{
8393
Name = orchestratorName.Name,
84-
Version = orchestratorName.Version,
94+
Version = version,
8595
InstanceId = options?.InstanceId ?? Guid.NewGuid().ToString("N"),
8696
Input = this.DataConverter.Serialize(input),
8797
};

test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ public void UseGrpc_Callback_Sets()
6767
options.Address.Should().BeNull();
6868
}
6969

70+
[Fact]
71+
public void UseDefaultVersion_DefaultVersion_Sets()
72+
{
73+
ServiceCollection services = new();
74+
DefaultDurableTaskClientBuilder builder = new(null, services);
75+
builder.UseDefaultVersion("0.1")
76+
.UseGrpc();
77+
78+
var provider = services.BuildServiceProvider();
79+
var options = provider.GetOptions<GrpcDurableTaskClientOptions>();
80+
81+
options.DefaultVersion.Should().Be("0.1");
82+
}
83+
7084
#if NET6_0_OR_GREATER
7185
static GrpcChannel GetChannel() => GrpcChannel.ForAddress("http://localhost:9001");
7286
#endif

test/Grpc.IntegrationTests/IntegrationTestBase.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
// Licensed under the MIT License.
33

44
using System.Diagnostics;
5-
using Microsoft.DurableTask.Tests.Logging;
65
using Microsoft.DurableTask.Client;
6+
using Microsoft.DurableTask.Tests.Logging;
77
using Microsoft.DurableTask.Worker;
8+
using Microsoft.Extensions.DependencyInjection;
89
using Microsoft.Extensions.Hosting;
910
using Microsoft.Extensions.Logging;
1011
using Xunit.Abstractions;
11-
using Microsoft.Extensions.DependencyInjection;
1212

1313
namespace Microsoft.DurableTask.Grpc.Tests;
1414

@@ -43,9 +43,9 @@ void IDisposable.Dispose()
4343
GC.SuppressFinalize(this);
4444
}
4545

46-
protected async Task<HostTestLifetime> StartWorkerAsync(Action<IDurableTaskWorkerBuilder> configure)
46+
protected async Task<HostTestLifetime> StartWorkerAsync(Action<IDurableTaskWorkerBuilder> configure, Action<IDurableTaskClientBuilder>? clientConfigure = null)
4747
{
48-
IHost host = this.CreateHostBuilder(configure).Build();
48+
IHost host = this.CreateHostBuilder(configure, clientConfigure).Build();
4949
await host.StartAsync(this.TimeoutToken);
5050
return new HostTestLifetime(host, this.TimeoutToken);
5151
}
@@ -54,7 +54,7 @@ protected async Task<HostTestLifetime> StartWorkerAsync(Action<IDurableTaskWorke
5454
/// Creates a <see cref="IHostBuilder"/> configured to output logs to xunit logging infrastructure.
5555
/// </summary>
5656
/// <param name="configure">Configures the durable task builder.</param>
57-
protected IHostBuilder CreateHostBuilder(Action<IDurableTaskWorkerBuilder> configure)
57+
protected IHostBuilder CreateHostBuilder(Action<IDurableTaskWorkerBuilder> configure, Action<IDurableTaskClientBuilder>? clientConfigure)
5858
{
5959
return Host.CreateDefaultBuilder()
6060
.ConfigureLogging(b =>
@@ -75,6 +75,10 @@ protected IHostBuilder CreateHostBuilder(Action<IDurableTaskWorkerBuilder> confi
7575
{
7676
b.UseGrpc(this.sidecarFixture.Channel);
7777
b.RegisterDirectly();
78+
if (clientConfigure != null)
79+
{
80+
clientConfigure!(b);
81+
}
7882
});
7983
});
8084
}

test/Grpc.IntegrationTests/OrchestrationPatterns.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
using System.Text.Json;
55
using System.Text.Json.Nodes;
6-
using Microsoft.DurableTask.Worker;
6+
using Microsoft.DurableTask.Client;
77
using Microsoft.DurableTask.Tests.Logging;
8+
using Microsoft.DurableTask.Worker;
89
using Microsoft.Extensions.DependencyInjection;
910
using Xunit.Abstractions;
10-
using Microsoft.DurableTask.Client;
1111

1212
namespace Microsoft.DurableTask.Grpc.Tests;
1313

@@ -561,8 +561,37 @@ public async Task SpecialSerialization()
561561
Assert.Equal("new value", output?["newProperty"]?.ToString());
562562
}
563563

564+
// TODO: Additional versioning tests
565+
[Fact]
566+
public async Task OrchestrationVersionPassedThroughContext()
567+
{
568+
var version = "0.1";
569+
await using HostTestLifetime server = await this.StartWorkerAsync(b =>
570+
{
571+
b.AddTasks(tasks => tasks
572+
.AddOrchestratorFunc<string, string>("Versioned_Orchestration", (ctx, input) =>
573+
{
574+
return ctx.CallActivityAsync<string>("Versioned_Activity", ctx.Version);
575+
})
576+
.AddActivityFunc<string, string>("Versioned_Activity", (ctx, input) =>
577+
{
578+
return $"Orchestration version: {input}";
579+
}));
580+
}, c =>
581+
{
582+
c.UseDefaultVersion(version);
583+
});
584+
585+
var instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync("Versioned_Orchestration", input: string.Empty);
586+
var result = await server.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, this.TimeoutToken);
587+
var output = result.ReadOutputAs<string>();
588+
589+
Assert.NotNull(output);
590+
Assert.Equal(output, $"Orchestration version: {version}");
591+
592+
}
593+
564594
// TODO: Test for multiple external events with the same name
565595
// TODO: Test for ContinueAsNew with external events that carry over
566596
// TODO: Test for catching activity exceptions of specific types
567-
// TODO: Versioning tests
568597
}

0 commit comments

Comments
 (0)