diff --git a/CHANGELOG.md b/CHANGELOG.md index 418fe8729..55f9d0172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.9.0 (unreleased) + +- Introduce default version setting to DurableTaskClient and expose to orchestrator ([#393](https://github.com/microsoft/durabletask-dotnet/pull/393)) + ## v1.8.1 - Add timeout to gRPC workitem streaming ([#390](https://github.com/microsoft/durabletask-dotnet/pull/390)) diff --git a/src/Abstractions/TaskOrchestrationContext.cs b/src/Abstractions/TaskOrchestrationContext.cs index b37b46d36..4eb087bae 100644 --- a/src/Abstractions/TaskOrchestrationContext.cs +++ b/src/Abstractions/TaskOrchestrationContext.cs @@ -59,6 +59,11 @@ public abstract class TaskOrchestrationContext /// public abstract bool IsReplaying { get; } + /// + /// Gets the version of the current orchestration instance, which was set when the instance was created. + /// + public abstract string Version { get; } + /// /// Gets the entity feature, for interacting with entities. /// diff --git a/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs b/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs index bb274ad08..5c8547592 100644 --- a/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs +++ b/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs @@ -89,4 +89,16 @@ public static IDurableTaskClientBuilder UseBuildTarget(this I }); return builder; } + + /// + /// Sets the default version for this builder. This version will be applied by default to all orchestrations if set. + /// + /// The builder to set the version for. + /// The version that will be used as the default version. + /// The original builder, for call chaining. + public static IDurableTaskClientBuilder UseDefaultVersion(this IDurableTaskClientBuilder builder, string version) + { + builder.Configure(options => options.DefaultVersion = version); + return builder; + } } diff --git a/src/Client/Core/DurableTaskClientOptions.cs b/src/Client/Core/DurableTaskClientOptions.cs index 296b750db..05f19e7bd 100644 --- a/src/Client/Core/DurableTaskClientOptions.cs +++ b/src/Client/Core/DurableTaskClientOptions.cs @@ -13,6 +13,14 @@ public class DurableTaskClientOptions DataConverter dataConverter = JsonDataConverter.Default; bool enableEntitySupport; + /// + /// Gets or sets the version of orchestrations that will be created. + /// + /// + /// Currently, this is sourced from the AzureManaged client options. + /// + public string DefaultVersion { get; set; } = string.Empty; + /// /// Gets or sets the data converter. Default value is . /// @@ -95,6 +103,11 @@ internal void ApplyTo(DurableTaskClientOptions other) { other.EnableEntitySupport = this.EnableEntitySupport; } + + if (!string.IsNullOrWhiteSpace(this.DefaultVersion)) + { + other.DefaultVersion = this.DefaultVersion; + } } } } diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index a408151d7..e7fd1da92 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -78,10 +78,20 @@ public override async Task ScheduleNewOrchestrationInstanceAsync( { Check.NotEntity(this.options.EnableEntitySupport, options?.InstanceId); + string version = string.Empty; + if (!string.IsNullOrEmpty(orchestratorName.Version)) + { + version = orchestratorName.Version; + } + else if (!string.IsNullOrEmpty(this.options.DefaultVersion)) + { + version = this.options.DefaultVersion; + } + var request = new P.CreateInstanceRequest { Name = orchestratorName.Name, - Version = orchestratorName.Version, + Version = version, InstanceId = options?.InstanceId ?? Guid.NewGuid().ToString("N"), Input = this.DataConverter.Serialize(input), }; diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index 7151a4e0f..8ce7d5125 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -81,6 +81,9 @@ public override TaskOrchestrationEntityFeature Entities } } + /// + public override string Version => this.innerContext.Version; + /// /// Gets the DataConverter to use for inputs, outputs, and entity states. /// diff --git a/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs b/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs index 8c0498eb7..59c03d98c 100644 --- a/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs +++ b/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs @@ -67,6 +67,20 @@ public void UseGrpc_Callback_Sets() options.Address.Should().BeNull(); } + [Fact] + public void UseDefaultVersion_DefaultVersion_Sets() + { + ServiceCollection services = new(); + DefaultDurableTaskClientBuilder builder = new(null, services); + builder.UseDefaultVersion("0.1") + .UseGrpc(); + + IServiceProvider provider = services.BuildServiceProvider(); + GrpcDurableTaskClientOptions options = provider.GetOptions(); + + options.DefaultVersion.Should().Be("0.1"); + } + #if NET6_0_OR_GREATER static GrpcChannel GetChannel() => GrpcChannel.ForAddress("http://localhost:9001"); #endif diff --git a/test/Grpc.IntegrationTests/IntegrationTestBase.cs b/test/Grpc.IntegrationTests/IntegrationTestBase.cs index fd63613f4..d6a915c92 100644 --- a/test/Grpc.IntegrationTests/IntegrationTestBase.cs +++ b/test/Grpc.IntegrationTests/IntegrationTestBase.cs @@ -2,13 +2,13 @@ // Licensed under the MIT License. using System.Diagnostics; -using Microsoft.DurableTask.Tests.Logging; using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Tests.Logging; using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit.Abstractions; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.DurableTask.Grpc.Tests; @@ -43,9 +43,9 @@ void IDisposable.Dispose() GC.SuppressFinalize(this); } - protected async Task StartWorkerAsync(Action configure) + protected async Task StartWorkerAsync(Action workerConfigure, Action? clientConfigure = null) { - IHost host = this.CreateHostBuilder(configure).Build(); + IHost host = this.CreateHostBuilder(workerConfigure, clientConfigure).Build(); await host.StartAsync(this.TimeoutToken); return new HostTestLifetime(host, this.TimeoutToken); } @@ -53,8 +53,9 @@ protected async Task StartWorkerAsync(Action /// Creates a configured to output logs to xunit logging infrastructure. /// - /// Configures the durable task builder. - protected IHostBuilder CreateHostBuilder(Action configure) + /// Configures the durable task worker builder. + /// Configures the durable task client builder. + protected IHostBuilder CreateHostBuilder(Action workerConfigure, Action? clientConfigure) { return Host.CreateDefaultBuilder() .ConfigureLogging(b => @@ -68,13 +69,14 @@ protected IHostBuilder CreateHostBuilder(Action confi services.AddDurableTaskWorker(b => { b.UseGrpc(this.sidecarFixture.Channel); - configure(b); + workerConfigure(b); }); services.AddDurableTaskClient(b => { b.UseGrpc(this.sidecarFixture.Channel); b.RegisterDirectly(); + clientConfigure?.Invoke(b); }); }); } diff --git a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs index 071dbad5c..3cbc0d793 100644 --- a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs +++ b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs @@ -3,11 +3,11 @@ using System.Text.Json; using System.Text.Json.Nodes; -using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Tests.Logging; +using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; -using Microsoft.DurableTask.Client; namespace Microsoft.DurableTask.Grpc.Tests; @@ -561,8 +561,37 @@ public async Task SpecialSerialization() Assert.Equal("new value", output?["newProperty"]?.ToString()); } + // TODO: Additional versioning tests + [Fact] + public async Task OrchestrationVersionPassedThroughContext() + { + 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", input: string.Empty); + var result = await server.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, this.TimeoutToken); + var output = result.ReadOutputAs(); + + Assert.NotNull(output); + Assert.Equal(output, $"Orchestration version: {version}"); + + } + // 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 - // TODO: Versioning tests }