diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9789ae6..06348972a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## vNext + +### Microsoft.DurableTask.Client + +- Add new `IDurableTaskClientBuilder AddDurableTaskClient(IServiceCollection, string?)` API + +### Microsoft.DurableTask.Worker + +- Add new `IDurableTaskWorkerBuilder AddDurableTaskWorker(IServiceCollection, string?)` API + ## v1.5.0 - Implement work item completion tokens for standalone worker scenarios ([#359](https://github.com/microsoft/durabletask-dotnet/pull/359)) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index a86daa23a..1feeda15d 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -83,6 +83,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{CECADD EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{3272C041-F81D-4C85-A4FB-2A700B5A7A9D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppMinimal", "samples\ConsoleAppMinimal\ConsoleAppMinimal.csproj", "{B48FACA9-A328-452A-BFAE-C4F60F9C7024}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -217,6 +219,10 @@ Global {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.Build.0 = Release|Any CPU + {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -258,6 +264,7 @@ Global {1E5C2E83-7B6B-425A-9C9B-0B887D273B12} = {51DC98A3-0193-4C66-964B-C26C748E25B6} {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {3272C041-F81D-4C85-A4FB-2A700B5A7A9D} = {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} + {B48FACA9-A328-452A-BFAE-C4F60F9C7024} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/samples/ConsoleApp/ConsoleApp.csproj b/samples/ConsoleApp/ConsoleApp.csproj index a11332cd0..2434e53de 100644 --- a/samples/ConsoleApp/ConsoleApp.csproj +++ b/samples/ConsoleApp/ConsoleApp.csproj @@ -2,14 +2,24 @@ Exe - net6.0 + net8.0 enable - - - + + + + + + + + + + diff --git a/samples/ConsoleApp/Program.cs b/samples/ConsoleApp/Program.cs index 5879b4c8f..64068079f 100644 --- a/samples/ConsoleApp/Program.cs +++ b/samples/ConsoleApp/Program.cs @@ -8,68 +8,55 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -IHost host = Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddDurableTaskClient(builder => - { - // Configure options for this builder. Can be omitted if no options customization is needed. - builder.Configure(opt => { }); - builder.UseGrpc(); // multiple overloads available for providing gRPC information +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); - // AddDurableTaskClient allows for multiple named clients by passing in a name as the first argument. - // When using a non-default named client, you will need to make this call below to have the - // DurableTaskClient added directly to the DI container. Otherwise IDurableTaskClientProvider must be used - // to retrieve DurableTaskClients by name from the DI container. In this case, we are using the default - // name, so the line below is NOT required as it was already called for us. - builder.RegisterDirectly(); - }); +IDurableTaskClientBuilder clientBuilder = builder.Services.AddDurableTaskClient() + .Configure(opt => { }) // configure options for this builder, if desired. + .UseGrpc(); // multiple overloads available for providing gRPC information - services.AddDurableTaskWorker(builder => - { - // Configure options for this builder. Can be omitted if no options customization is needed. - builder.Configure(opt => { }); +// OPTIONAL STEP +// AddDurableTaskClient allows for multiple named clients by passing in a name as the first argument. +// When using a non-default named client, you will need to make this call below to have the +// DurableTaskClient added directly to the DI container. Otherwise IDurableTaskClientProvider must be used +// to retrieve DurableTaskClients by name from the DI container. In this case, we are using the default +// name, so the line below is NOT required as it was already called for us. +clientBuilder.RegisterDirectly(); - // Register orchestrators and activities. - builder.AddTasks(tasks => +builder.Services.AddDurableTaskWorker() + .Configure(opt => { }) // configure options for this builder. + .AddTasks(tasks => + { + // Add tasks to the worker. + tasks.AddOrchestratorFunc("HelloSequence", async context => + { + var greetings = new List { - tasks.AddOrchestratorFunc("HelloSequence", async context => - { - var greetings = new List - { - await context.CallActivityAsync("SayHello", "Tokyo"), - await context.CallActivityAsync("SayHello", "London"), - await context.CallActivityAsync("SayHello", "Seattle"), - }; - - return greetings; - }); - - tasks.AddActivityFunc("SayHello", (context, city) => $"Hello {city}!"); - }); + await context.CallActivityAsync("SayHello", "Tokyo"), + await context.CallActivityAsync("SayHello", "London"), + await context.CallActivityAsync("SayHello", "Seattle"), + }; - builder.UseGrpc(); // multiple overloads available for providing gRPC information + return greetings; }); - // Can also configure worker and client options through all the existing options config methods. - // These are equivalent to the 'builder.Configure' calls above. - services.Configure(opt => { }); - services.Configure(opt => { }); + tasks.AddActivityFunc("SayHello", (context, city) => $"Hello {city}!"); + }) + .UseGrpc(); // multiple overloads available for providing gRPC information - // Registry can also be done via options pattern. This is equivalent to the 'builder.AddTasks' call above. - // You can use all the tools options pattern has available. For example, if you have multiple workers you could - // use ConfigureAll to add tasks to ALL workers in one go. Otherwise, you need to use - // named option configuration to register to specific workers (where the name matches the name passed to - // 'AddDurableTaskWorker(name?, builder)'). - services.Configure(registry => { }); +// OPTIONAL STEP +// Client and Worker options can also be configured through the options pattern. +// When using the options pattern, configure with the same name as the builder. +builder.Services.Configure(opt => { }); +builder.Services.Configure(opt => { }); +builder.Services.Configure(registry => { }); - // You can configure custom data converter multiple ways. One is through worker/client options configuration. - // Alternatively, data converter will be used from the service provider if available (as a singleton) AND no - // converter was explicitly set on the options. - services.AddSingleton(JsonDataConverter.Default); - }) - .Build(); +// OPTIONAL STEP +// You can configure custom data converter multiple ways. One is through worker/client options configuration. +// Alternatively, data converter will be used from the service provider if available (as a singleton) AND no +// converter was explicitly set on the options. +builder.Services.AddSingleton(JsonDataConverter.Default); +IHost host = builder.Build(); await host.StartAsync(); await using DurableTaskClient client = host.Services.GetRequiredService(); @@ -82,4 +69,4 @@ await context.CallActivityAsync("SayHello", "Seattle"), getInputsAndOutputs: true, cts.Token); -Console.WriteLine($"Instance completed: {instance}"); \ No newline at end of file +Console.WriteLine($"Instance completed: {instance}"); diff --git a/samples/ConsoleAppMinimal/ConsoleAppMinimal.csproj b/samples/ConsoleAppMinimal/ConsoleAppMinimal.csproj new file mode 100644 index 000000000..2434e53de --- /dev/null +++ b/samples/ConsoleAppMinimal/ConsoleAppMinimal.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + + + + + + + + + + + + + + + + diff --git a/samples/ConsoleAppMinimal/Program.cs b/samples/ConsoleAppMinimal/Program.cs new file mode 100644 index 000000000..089c9fa6a --- /dev/null +++ b/samples/ConsoleAppMinimal/Program.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// This app differs from samples/ConsoleApp in that we show the absolute minimum code needed to run a Durable Task application. + +using ConsoleAppMinimal; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.Hosting; + +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddDurableTaskClient().UseGrpc(); +builder.Services.AddDurableTaskWorker() + .AddTasks(tasks => + { + tasks.AddOrchestrator(); + tasks.AddActivity(); + }) + .UseGrpc(); + +IHost host = builder.Build(); +await host.StartAsync(); diff --git a/samples/ConsoleAppMinimal/Tasks.cs b/samples/ConsoleAppMinimal/Tasks.cs new file mode 100644 index 000000000..addeded56 --- /dev/null +++ b/samples/ConsoleAppMinimal/Tasks.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; + +namespace ConsoleAppMinimal; + +[DurableTask("HelloSequence")] +public class HelloSequenceOrchestrator : TaskOrchestrator> +{ + public override async Task> RunAsync(TaskOrchestrationContext context, string input) + { + IEnumerable greetings = + [ + await context.CallActivityAsync("SayHello", "Tokyo"), + await context.CallActivityAsync("SayHello", "London"), + await context.CallActivityAsync("SayHello", "Seattle"), + ]; + + return greetings; + } +} + +[DurableTask("SayHello")] +public class SayHelloActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string city) + { + return Task.FromResult($"Hello {city}!"); + } +} diff --git a/src/Client/Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Client/Core/DependencyInjection/ServiceCollectionExtensions.cs index db1eca955..89d4db19b 100644 --- a/src/Client/Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Client/Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -12,6 +12,20 @@ namespace Microsoft.DurableTask.Client; /// public static class ServiceCollectionExtensions { + /// + /// Adds and configures Durable Task worker services to the service collection. + /// + /// The service collection to add to. + /// The name of the builder to add. + /// The builder used to configured the . + public static IDurableTaskClientBuilder AddDurableTaskClient(this IServiceCollection services, string? name = null) + { + Check.NotNull(services); + IDurableTaskClientBuilder builder = GetBuilder(services, name ?? Options.DefaultName, out bool added); + ConditionalConfigureBuilder(services, builder, added); + return builder; + } + /// /// Configures and adds a to the service collection. /// @@ -37,25 +51,31 @@ public static IServiceCollection AddDurableTaskClient( services.TryAddSingleton(); IDurableTaskClientBuilder builder = GetBuilder(services, name, out bool added); configure.Invoke(builder); + ConditionalConfigureBuilder(services, builder, added); + return services; + } - if (added) + static void ConditionalConfigureBuilder( + IServiceCollection services, IDurableTaskClientBuilder builder, bool configure) + { + if (!configure) { - // The added toggle logic is because we cannot use TryAddEnumerable logic as - // we would have to dynamically compile a lambda to have it work correctly. - ConfigureDurableOptions(services, name); + return; + } - // We do not want to register DurableTaskClient type directly so we can keep a max of 1 DurableTaskClients - // registered, allowing for direct-DI of the default client. - services.AddSingleton(sp => new DefaultDurableTaskClientProvider.ClientContainer(builder.Build(sp))); + // The added toggle logic is because we cannot use TryAddEnumerable logic as + // we would have to dynamically compile a lambda to have it work correctly. + ConfigureDurableOptions(services, builder.Name); - if (name == Options.DefaultName) - { - // If we have the default options name here, we will inject this client directly. - builder.RegisterDirectly(); - } - } + // We do not want to register DurableTaskClient type directly so we can keep a max of 1 DurableTaskClients + // registered, allowing for direct-DI of the default client. + services.AddSingleton(sp => new DefaultDurableTaskClientProvider.ClientContainer(builder.Build(sp))); - return services; + if (builder.Name == Options.DefaultName) + { + // If we have the default options name here, we will inject this client directly. + builder.RegisterDirectly(); + } } static IServiceCollection ConfigureDurableOptions(IServiceCollection services, string name) diff --git a/src/Client/Core/RELEASENOTES.md b/src/Client/Core/RELEASENOTES.md index b32377e3d..dfa5b9703 100644 --- a/src/Client/Core/RELEASENOTES.md +++ b/src/Client/Core/RELEASENOTES.md @@ -1 +1 @@ -- Fix filter not being passed along in `PurgeAllInstancesAsync` (https://github.com/microsoft/durabletask-dotnet/pull/289) +- Add new `IDurableTaskClientBuilder AddDurableTaskClient(IServiceCollection, string?)` API diff --git a/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs index 8a9ef74c5..e68c550cf 100644 --- a/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.DurableTask.Worker.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -11,6 +12,20 @@ namespace Microsoft.DurableTask.Worker; /// public static class ServiceCollectionExtensions { + /// + /// Adds and configures Durable Task worker services to the service collection. + /// + /// The service collection to add to. + /// The name of the builder to add. + /// The builder used to configured the . + public static IDurableTaskWorkerBuilder AddDurableTaskWorker(this IServiceCollection services, string? name = null) + { + Check.NotNull(services); + IDurableTaskWorkerBuilder builder = GetBuilder(services, name ?? Options.DefaultName, out bool added); + ConditionalConfigureBuilder(services, builder, added); + return builder; + } + /// /// Adds and configures Durable Task worker services to the service collection. /// @@ -41,16 +56,22 @@ public static IServiceCollection AddDurableTaskWorker( IDurableTaskWorkerBuilder builder = GetBuilder(services, name, out bool added); configure.Invoke(builder); + ConditionalConfigureBuilder(services, builder, added); + return services; + } - if (added) + static void ConditionalConfigureBuilder( + IServiceCollection services, IDurableTaskWorkerBuilder builder, bool configure) + { + if (!configure) { - // The added toggle logic is because we cannot use TryAddEnumerable logic as - // we would have to dynamically compile a lambda to have it work correctly. - ConfigureDurableOptions(services, name); - services.AddSingleton(sp => builder.Build(sp)); + return; } - return services; + // The added toggle logic is because we cannot use TryAddEnumerable logic as + // we would have to dynamically compile a lambda to have it work correctly. + ConfigureDurableOptions(services, builder.Name); + services.AddSingleton(sp => builder.Build(sp)); } static IServiceCollection ConfigureDurableOptions(IServiceCollection services, string name) @@ -87,15 +108,10 @@ static IDurableTaskWorkerBuilder GetBuilder(IServiceCollection services, string /// /// A container which is used to store and retrieve builders from within the . /// - class BuilderContainer + class BuilderContainer(IServiceCollection services) { - readonly Dictionary builders = new(); - readonly IServiceCollection services; - - public BuilderContainer(IServiceCollection services) - { - this.services = services; - } + readonly Dictionary builders = []; + readonly IServiceCollection services = services; public IDurableTaskWorkerBuilder GetOrAdd(string name, out bool added) { diff --git a/src/Worker/Core/RELEASENOTES.md b/src/Worker/Core/RELEASENOTES.md index e69de29bb..c00e885ba 100644 --- a/src/Worker/Core/RELEASENOTES.md +++ b/src/Worker/Core/RELEASENOTES.md @@ -0,0 +1 @@ +- Add new `IDurableTaskWorkerBuilder AddDurableTaskWorker(IServiceCollection, string?)` API diff --git a/test/Abstractions.Tests/TaskNameTests.cs b/test/Abstractions.Tests/TaskNameTests.cs index 739227e96..b5cfc4350 100644 --- a/test/Abstractions.Tests/TaskNameTests.cs +++ b/test/Abstractions.Tests/TaskNameTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.DurableTask; +namespace Microsoft.DurableTask.Tests; public class TaskNameTests { diff --git a/test/Client/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Client/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 7347d61f4..7ddce4d5d 100644 --- a/test/Client/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/test/Client/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -8,8 +8,22 @@ namespace Microsoft.DurableTask.Client.Tests; public class ServiceCollectionExtensionsTests { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("MyBuilder")] + public void AddDurableTaskClient_SameInstance(string? name) + { + ServiceCollection services = new(); + IDurableTaskClientBuilder actual1 = services.AddDurableTaskClient(name); + IDurableTaskClientBuilder actual2 = services.AddDurableTaskClient(name); + + actual1.Should().NotBeNull(); + actual1.Should().BeSameAs(actual2); + } + [Fact] - public void AddDurableTaskClient_SameInstance() + public void AddDurableTaskClient_SameInstance2() { ServiceCollection services = new(); IDurableTaskClientBuilder? actual1 = null; @@ -21,6 +35,72 @@ public void AddDurableTaskClient_SameInstance() actual1.Should().BeSameAs(actual2); } + [Theory] + [InlineData("")] + [InlineData("MyBuilder")] + public void AddDurableTaskClient_SameInstance3(string name) + { + ServiceCollection services = new(); + IDurableTaskClientBuilder? actual1 = null; + IDurableTaskClientBuilder? actual2 = null; + services.AddDurableTaskClient(name, builder => actual1 = builder); + services.AddDurableTaskClient(name, builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual1.Should().BeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskClient_SameInstance4() + { + ServiceCollection services = new(); + IDurableTaskClientBuilder actual1 = services.AddDurableTaskClient(); + IDurableTaskClientBuilder? actual2 = null; + services.AddDurableTaskClient(builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual1.Should().BeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskClient_DifferentNames_NotSame() + { + ServiceCollection services = new(); + IDurableTaskClientBuilder actual1 = services.AddDurableTaskClient(); + IDurableTaskClientBuilder actual2 = services.AddDurableTaskClient("MyBuilder"); + + actual1.Should().NotBeNull(); + actual2.Should().NotBeNull(); + actual1.Should().NotBeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskClient_DifferentNames_NotSame2() + { + ServiceCollection services = new(); + IDurableTaskClientBuilder? actual1 = null; + IDurableTaskClientBuilder? actual2 = null; + services.AddDurableTaskClient(builder => actual1 = builder); + services.AddDurableTaskClient("MyBuilder", builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual2.Should().NotBeNull(); + actual1.Should().NotBeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskClient_DifferentNames_NotSame3() + { + ServiceCollection services = new(); + IDurableTaskClientBuilder actual1 = services.AddDurableTaskClient(); + IDurableTaskClientBuilder? actual2 = null; + services.AddDurableTaskClient("MyBuilder", builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual2.Should().NotBeNull(); + actual1.Should().NotBeSameAs(actual2); + } + [Fact] public void AddDurableTaskClient_HostedServiceAdded() { diff --git a/test/Worker/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Worker/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 672d6eeed..8657c63d0 100644 --- a/test/Worker/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -9,8 +9,22 @@ namespace Microsoft.DurableTask.Worker.Tests; public class ServiceCollectionExtensionsTests { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("MyBuilder")] + public void AddDurableTaskWorker_SameInstance(string? name) + { + ServiceCollection services = new(); + IDurableTaskWorkerBuilder actual1 = services.AddDurableTaskWorker(name); + IDurableTaskWorkerBuilder actual2 = services.AddDurableTaskWorker(name); + + actual1.Should().NotBeNull(); + actual1.Should().BeSameAs(actual2); + } + [Fact] - public void AddDurableTaskWorker_SameInstance() + public void AddDurableTaskWorker_SameInstance2() { ServiceCollection services = new(); IDurableTaskWorkerBuilder? actual1 = null; @@ -22,6 +36,72 @@ public void AddDurableTaskWorker_SameInstance() actual1.Should().BeSameAs(actual2); } + [Theory] + [InlineData("")] + [InlineData("MyBuilder")] + public void AddDurableTaskWorker_SameInstance3(string name) + { + ServiceCollection services = new(); + IDurableTaskWorkerBuilder? actual1 = null; + IDurableTaskWorkerBuilder? actual2 = null; + services.AddDurableTaskWorker(name, builder => actual1 = builder); + services.AddDurableTaskWorker(name, builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual1.Should().BeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskWorker_SameInstance4() + { + ServiceCollection services = new(); + IDurableTaskWorkerBuilder actual1 = services.AddDurableTaskWorker(); + IDurableTaskWorkerBuilder? actual2 = null; + services.AddDurableTaskWorker(builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual1.Should().BeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskWorker_DifferentNames_NotSame() + { + ServiceCollection services = new(); + IDurableTaskWorkerBuilder actual1 = services.AddDurableTaskWorker(); + IDurableTaskWorkerBuilder actual2 = services.AddDurableTaskWorker("MyBuilder"); + + actual1.Should().NotBeNull(); + actual2.Should().NotBeNull(); + actual1.Should().NotBeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskWorker_DifferentNames_NotSame2() + { + ServiceCollection services = new(); + IDurableTaskWorkerBuilder? actual1 = null; + IDurableTaskWorkerBuilder? actual2 = null; + services.AddDurableTaskWorker(builder => actual1 = builder); + services.AddDurableTaskWorker("MyBuilder", builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual2.Should().NotBeNull(); + actual1.Should().NotBeSameAs(actual2); + } + + [Fact] + public void AddDurableTaskWorker_DifferentNames_NotSame3() + { + ServiceCollection services = new(); + IDurableTaskWorkerBuilder actual1 = services.AddDurableTaskWorker(); + IDurableTaskWorkerBuilder? actual2 = null; + services.AddDurableTaskWorker("MyBuilder", builder => actual2 = builder); + + actual1.Should().NotBeNull(); + actual2.Should().NotBeNull(); + actual1.Should().NotBeSameAs(actual2); + } + [Fact] public void AddDurableTaskWorker_HostedServiceAdded() {