diff --git a/Directory.Packages.props b/Directory.Packages.props index 21707cb..aede498 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,11 +1,11 @@ - + @@ -18,6 +18,7 @@ + @@ -41,7 +42,7 @@ - + diff --git a/samples/DurableTask.Extensions.Samples/Program.cs b/samples/DurableTask.Extensions.Samples/Program.cs index c351141..44667d1 100644 --- a/samples/DurableTask.Extensions.Samples/Program.cs +++ b/samples/DurableTask.Extensions.Samples/Program.cs @@ -3,6 +3,7 @@ using DurableTask.Core; using DurableTask.DependencyInjection; +using DurableTask.DependencyInjection.Extensions; using DurableTask.Emulator; using DurableTask.Extensions; using DurableTask.Extensions.Samples; @@ -14,17 +15,30 @@ .ConfigureServices(services => { // Can register DataConvert in service container, or in options below. - // services.AddSingleton(new StjDataConverter()); + //services.AddSingleton(new StjDataConverter()); services.AddSingleton(); services.AddHostedService(); - }) - .ConfigureTaskHubWorker((context, builder) => - { - builder.WithOrchestrationService(new LocalOrchestrationService()); - builder.AddDurableExtensions(opt => opt.DataConverter = new StjDataConverter()); - builder.AddClient(); - builder.AddOrchestrationsFromAssembly(includePrivate: true); - builder.AddActivitiesFromAssembly(includePrivate: true); + + IOrchestrationService orchestrationService = new LocalOrchestrationService(); + + services.AddTaskHubWorker((builder) => + { + builder + .UseBuildTarget() + .WithOrchestrationService(orchestrationService) + + .AddDurableExtensions(opt => opt.DataConverter = new StjDataConverter()) + + .AddOrchestrationsFromAssembly(includePrivate: true) + .AddActivitiesFromAssembly(includePrivate: true); + }); + services.AddTaskHubClient((builder) => + { + builder + .UseBuildTarget() + .WithOrchestrationService((IOrchestrationServiceClient)orchestrationService); + }); + }) .UseConsoleLifetime() .Build(); @@ -37,9 +51,9 @@ internal class TaskEnqueuer : BackgroundService private readonly IConsole _console; private readonly string _instanceId = Guid.NewGuid().ToString(); - public TaskEnqueuer(TaskHubClient client, IConsole console) + public TaskEnqueuer(ITaskHubClientProvider clientProvider, IConsole console) { - _client = client ?? throw new ArgumentNullException(nameof(client)); + _client = ((DurableTaskHubClient)clientProvider.GetClient()).Client; _console = console ?? throw new ArgumentNullException(nameof(console)); } diff --git a/samples/DurableTask.Instrumentation.Samples/Program.cs b/samples/DurableTask.Instrumentation.Samples/Program.cs index 3b4c444..5f71277 100644 --- a/samples/DurableTask.Instrumentation.Samples/Program.cs +++ b/samples/DurableTask.Instrumentation.Samples/Program.cs @@ -5,6 +5,7 @@ using DurableTask.AzureStorage; using DurableTask.Core; using DurableTask.DependencyInjection; +using DurableTask.DependencyInjection.Extensions; using DurableTask.Extensions; using DurableTask.Extensions.Samples; using DurableTask.Hosting; @@ -25,18 +26,40 @@ IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { + IOrchestrationService orchestrationService = GetOrchestrationService(); + + services.AddTaskHubWorker((builder) => + { + builder + .UseBuildTarget() + .WithOrchestrationService(orchestrationService) + + .AddDurableExtensions() + .AddDurableInstrumentation() + + .AddOrchestrationsFromAssembly(includePrivate: true) + .AddActivitiesFromAssembly(includePrivate: true); + }); + services.AddTaskHubClient((builder) => + { + builder + .UseBuildTarget() + .WithOrchestrationService((IOrchestrationServiceClient)orchestrationService); + }); + services.AddSingleton(); services.AddHostedService(); + }) - .ConfigureTaskHubWorker((context, builder) => - { - builder.WithOrchestrationService(GetOrchestrationService()); - builder.AddDurableExtensions(); - builder.AddDurableInstrumentation(); - builder.AddClient(); - builder.AddOrchestrationsFromAssembly(includePrivate: true); - builder.AddActivitiesFromAssembly(includePrivate: true); - }) + //.ConfigureTaskHubWorker((context, builder) => + //{ + // builder.WithOrchestrationService(GetOrchestrationService()); + // builder.AddDurableExtensions(); + // builder.AddDurableInstrumentation(); + // //builder.AddClient(); + // builder.AddOrchestrationsFromAssembly(includePrivate: true); + // builder.AddActivitiesFromAssembly(includePrivate: true); + //}) .UseConsoleLifetime() .Build(); @@ -60,9 +83,9 @@ internal class TaskEnqueuer : BackgroundService private readonly IConsole _console; private readonly string _instanceId = Guid.NewGuid().ToString(); - public TaskEnqueuer(TaskHubClient client, IConsole console) + public TaskEnqueuer(ITaskHubClientProvider clientProvider, IConsole console) { - _client = client ?? throw new ArgumentNullException(nameof(client)); + _client = ((DurableTaskHubClient)clientProvider.GetClient()).Client; _console = console ?? throw new ArgumentNullException(nameof(console)); } diff --git a/samples/DurableTask.Named.Samples/DurableTask.Named.Samples.csproj b/samples/DurableTask.Named.Samples/DurableTask.Named.Samples.csproj new file mode 100644 index 0000000..c2d8e0f --- /dev/null +++ b/samples/DurableTask.Named.Samples/DurableTask.Named.Samples.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + disable + 25384414-13b6-4ef1-8eeb-12236bcf0bcd + + + + + + + + + + + + + + diff --git a/samples/DurableTask.Named.Samples/Generics/GenericActivity`1.cs b/samples/DurableTask.Named.Samples/Generics/GenericActivity`1.cs new file mode 100644 index 0000000..bf4bbb5 --- /dev/null +++ b/samples/DurableTask.Named.Samples/Generics/GenericActivity`1.cs @@ -0,0 +1,19 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; + +namespace DurableTask.Named.Samples.Generics; + +/// +/// An example of an open generic activity. +/// +/// The open generic type. +public class GenericActivity : TaskActivity +{ + /// + protected override string Execute(TaskContext context, T input) + { + return $"My generic param is {typeof(T)} with value '{input}'"; + } +} diff --git a/samples/DurableTask.Named.Samples/Generics/GenericOrchestrationRunner.cs b/samples/DurableTask.Named.Samples/Generics/GenericOrchestrationRunner.cs new file mode 100644 index 0000000..28482b4 --- /dev/null +++ b/samples/DurableTask.Named.Samples/Generics/GenericOrchestrationRunner.cs @@ -0,0 +1,37 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; + +namespace DurableTask.Named.Samples.Generics; + +/// +/// Runner for generic tasks example. +/// +public class GenericOrchestrationRunner : TaskOrchestration +{ + /// + public override async Task RunTask(OrchestrationContext context, string input) + { + string result = await context.ScheduleTask(typeof(GenericActivity), 10); + await PrintAsync(context, result); + + result = await context.ScheduleTask(typeof(GenericActivity), "example"); + await PrintAsync(context, result); + + result = await context.ScheduleTask(typeof(GenericActivity), new MyClass()); + await PrintAsync(context, result); + + return string.Empty; + } + + private static Task PrintAsync(OrchestrationContext context, string input) + { + return context.ScheduleTask(typeof(PrintTask), input); + } + + private class MyClass + { + public override string ToString() => "Example private class"; + } +} diff --git a/samples/DurableTask.Named.Samples/Greetings/GetUserTask.cs b/samples/DurableTask.Named.Samples/Greetings/GetUserTask.cs new file mode 100644 index 0000000..386cfd9 --- /dev/null +++ b/samples/DurableTask.Named.Samples/Greetings/GetUserTask.cs @@ -0,0 +1,30 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; + +namespace DurableTask.Named.Samples.Greetings; + +/// +/// A task activity for getting a username from console. +/// +public class GetUserTask : TaskActivity +{ + private readonly IConsole _console; + + /// + /// Initializes a new instance of the class. + /// + /// The console output helper. + public GetUserTask(IConsole console) + { + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + + /// + protected override string Execute(TaskContext context, string input) + { + _console.WriteLine("Please enter your name:"); + return _console.ReadLine(); + } +} diff --git a/samples/DurableTask.Named.Samples/Greetings/GreetingsOrchestration.cs b/samples/DurableTask.Named.Samples/Greetings/GreetingsOrchestration.cs new file mode 100644 index 0000000..95a225f --- /dev/null +++ b/samples/DurableTask.Named.Samples/Greetings/GreetingsOrchestration.cs @@ -0,0 +1,20 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; + +namespace DurableTask.Named.Samples.Greetings; + +/// +/// A task orchestration for greeting a user. +/// +public class GreetingsOrchestration : TaskOrchestration +{ + /// + public override async Task RunTask(OrchestrationContext context, string input) + { + string user = await context.ScheduleTask(typeof(GetUserTask)); + string greeting = await context.ScheduleTask(typeof(SendGreetingTask), user); + return greeting; + } +} diff --git a/samples/DurableTask.Named.Samples/Greetings/SendGreetingTask.cs b/samples/DurableTask.Named.Samples/Greetings/SendGreetingTask.cs new file mode 100644 index 0000000..b6d132d --- /dev/null +++ b/samples/DurableTask.Named.Samples/Greetings/SendGreetingTask.cs @@ -0,0 +1,43 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; + +namespace DurableTask.Named.Samples.Greetings; + +/// +/// A task for sending a greeting. +/// +public sealed class SendGreetingTask : AsyncTaskActivity +{ + private readonly IConsole _console; + + /// + /// Initializes a new instance of the class. + /// + /// The console output helper. + public SendGreetingTask(IConsole console) + { + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + + /// + protected override async Task ExecuteAsync(TaskContext context, string user) + { + string message; + if (!string.IsNullOrWhiteSpace(user) && user.Equals("TimedOut")) + { + message = "GetUser Timed out!!!"; + _console.WriteLine(message); + } + else + { + _console.WriteLine("Sending greetings to user: " + user + "..."); + await Task.Delay(5 * 1000); + message = "Greeting sent to " + user; + _console.WriteLine(message); + } + + return message; + } +} diff --git a/samples/DurableTask.Named.Samples/PrintTask.cs b/samples/DurableTask.Named.Samples/PrintTask.cs new file mode 100644 index 0000000..6341be2 --- /dev/null +++ b/samples/DurableTask.Named.Samples/PrintTask.cs @@ -0,0 +1,30 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; + +namespace DurableTask.Named.Samples; + +/// +/// An activity to print to the console. +/// +public class PrintTask : TaskActivity +{ + private readonly IConsole _console; + + /// + /// Initializes a new instance of the class. + /// + /// The console to print to. + public PrintTask(IConsole console) + { + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + + /// + protected override string Execute(TaskContext context, string input) + { + _console.WriteLine(input); + return string.Empty; + } +} diff --git a/samples/DurableTask.Named.Samples/Program.cs b/samples/DurableTask.Named.Samples/Program.cs new file mode 100644 index 0000000..95f43ad --- /dev/null +++ b/samples/DurableTask.Named.Samples/Program.cs @@ -0,0 +1,106 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; +using DurableTask.DependencyInjection; +using DurableTask.Emulator; +using DurableTask.Hosting; +using DurableTask.Hosting.Options; +using DurableTask.Named.Samples.Generics; +using DurableTask.Named.Samples.Greetings; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using DurableTask.DependencyInjection.Extensions; + +namespace DurableTask.Named.Samples; + +/// +/// The samples program. +/// +public class Program +{ + private static readonly string s_taskHubName = "myHub"; + + /// + /// The entry point. + /// + /// The supplied arguments, if any. + /// A task that completes when this program is finished running. + public static Task Main(string[] args) + { + IHost host = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(builder => builder.AddUserSecrets()) + .ConfigureServices(services => + { + IOrchestrationService orchestrationService = UseLocalEmulator(); + + services.AddTaskHubWorker(s_taskHubName, (builder) => + { + builder + .UseBuildTarget() + .WithOrchestrationService(orchestrationService) + + .UseOrchestrationMiddleware() + .UseActivityMiddleware() + + .AddOrchestration() + .AddOrchestration() + .AddActivitiesFromAssembly(); + }); + services.AddTaskHubClient(s_taskHubName, (builder) => + { + builder + .UseBuildTarget() + .WithOrchestrationService((IOrchestrationServiceClient)orchestrationService); + }); + services.Configure(opt => + { + opt.CreateIfNotExists = true; + }); + services.AddSingleton(); + services.AddHostedService(); + }) + .UseConsoleLifetime() + .Build(); + + return host.RunAsync(); + } + + private static IOrchestrationService UseLocalEmulator() + => new LocalOrchestrationService(); + + private class TaskEnqueuer : BackgroundService + { + private readonly TaskHubClient _client; + private readonly IConsole _console; + private readonly string _instanceId = Guid.NewGuid().ToString(); + + public TaskEnqueuer(ITaskHubClientProvider clientProvider, IConsole console) + { + _client = ((DurableTaskHubClient)clientProvider.GetClient(s_taskHubName)).Client; + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + OrchestrationInstance instance = await _client.CreateOrchestrationInstanceAsync( + NameVersionHelper.GetDefaultName(typeof(GenericOrchestrationRunner)), + NameVersionHelper.GetDefaultVersion(typeof(GenericOrchestrationRunner)), + _instanceId, + null, + new Dictionary() + { + ["CorrelationId"] = Guid.NewGuid().ToString(), + }); + + OrchestrationState result = await _client.WaitForOrchestrationAsync( + instance, TimeSpan.FromSeconds(60), stoppingToken); + + _console.WriteLine(); + _console.WriteLine($"Orchestration finished."); + _console.WriteLine($"Run stats: {result.Status}"); + _console.WriteLine("Press Ctrl+C to exit"); + } + } +} diff --git a/samples/DurableTask.Named.Samples/Properties/launchSettings.json b/samples/DurableTask.Named.Samples/Properties/launchSettings.json new file mode 100644 index 0000000..f7d32b2 --- /dev/null +++ b/samples/DurableTask.Named.Samples/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "DurableTask.Named.Samples": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/DurableTask.Named.Samples/SampleMiddleware.cs b/samples/DurableTask.Named.Samples/SampleMiddleware.cs new file mode 100644 index 0000000..76f37bc --- /dev/null +++ b/samples/DurableTask.Named.Samples/SampleMiddleware.cs @@ -0,0 +1,28 @@ +using DurableTask.Core.Middleware; +using DurableTask.DependencyInjection; + +namespace DurableTask.Named.Samples; + +/// +/// Sample middleware +/// +public class SampleMiddleware : ITaskMiddleware +{ + private readonly IConsole _console; + + /// + /// Initializes a new instance of the class. + /// + /// The console output helper. + public SampleMiddleware(IConsole console) + { + _console = console; + } + + /// + public Task InvokeAsync(DispatchMiddlewareContext context, Func next) + { + _console.WriteLine("In sample middleware. Dependency Injection works."); + return next(); + } +} diff --git a/samples/DurableTask.Named.Samples/System/ConsoleWrapper.cs b/samples/DurableTask.Named.Samples/System/ConsoleWrapper.cs new file mode 100644 index 0000000..4449bd4 --- /dev/null +++ b/samples/DurableTask.Named.Samples/System/ConsoleWrapper.cs @@ -0,0 +1,19 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +namespace System; + +/// +/// Default implementation of . +/// +public class ConsoleWrapper : IConsole +{ + /// + public string ReadLine() => Console.ReadLine(); + + /// + public void WriteLine() => Console.WriteLine(); + + /// + public void WriteLine(string line) => Console.WriteLine(line); +} diff --git a/samples/DurableTask.Named.Samples/System/IConsole.cs b/samples/DurableTask.Named.Samples/System/IConsole.cs new file mode 100644 index 0000000..b9fc13f --- /dev/null +++ b/samples/DurableTask.Named.Samples/System/IConsole.cs @@ -0,0 +1,27 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +namespace System; + +/// +/// Abstraction for . +/// +public interface IConsole +{ + /// + /// . + /// + /// The line input from the console. + string ReadLine(); + + /// + /// . + /// + void WriteLine(); + + /// + /// . + /// + /// The line to write. + void WriteLine(string line); +} diff --git a/samples/DurableTask.Named.Samples/appSettings.Development.json b/samples/DurableTask.Named.Samples/appSettings.Development.json new file mode 100644 index 0000000..98d0ab2 --- /dev/null +++ b/samples/DurableTask.Named.Samples/appSettings.Development.json @@ -0,0 +1,7 @@ +{ + "DurableTask": { + "AzureStorage": { + "ConnectionString": "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://127.0.0.1:10002/" + } + } +} diff --git a/samples/DurableTask.Named.Samples/appsettings.json b/samples/DurableTask.Named.Samples/appsettings.json new file mode 100644 index 0000000..4aa06eb --- /dev/null +++ b/samples/DurableTask.Named.Samples/appsettings.json @@ -0,0 +1,5 @@ +{ + "DurableTask": { + "HubName": "SamplesHub" + } +} diff --git a/samples/DurableTask.Named.Samples/readme.md b/samples/DurableTask.Named.Samples/readme.md new file mode 100644 index 0000000..b4b625e --- /dev/null +++ b/samples/DurableTask.Named.Samples/readme.md @@ -0,0 +1,21 @@ +# Overview + +The sample includes a basic scenario of using a host builder to configure a durable task host, and register some basic orchestrations and activities. + +## Bonus Samples + +The sample includes some bonus scenarios I have found useful when working with DurableTask Framework. + +### 1. Orchestration Session Data + +#### Use Case + +Say you want some non-orchestration or activity specific data that is carried through the whole execution of a single orchestration. The orchestration or activities have no use to directly interact with this data, but it is there for some other purpose such as a logging correlation id. + +#### Solution + +DurableTask offers no official session data. However, the framework does make use of data contracts and `ExtensionData`. Using this functionality, we can store our session data in the `OrchestrationInstance.ExtensionData` and be able to access it for the whole life of the orchestration. See [OrchestrationInstanceEx.cs](./DurableTask.Samples/SessionData/OrchestrationInstanceEx.cs). + +#### UPDATE + +DurableTask [now supports](https://github.com/Azure/durabletask/pull/804) propagating orchestration tags to activities. This means they can now be used for metadata propagation instead of the workaround above. diff --git a/samples/DurableTask.Samples/Program.cs b/samples/DurableTask.Samples/Program.cs index 65504cb..5e5d206 100644 --- a/samples/DurableTask.Samples/Program.cs +++ b/samples/DurableTask.Samples/Program.cs @@ -11,14 +11,18 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using DurableTask.DependencyInjection.Extensions; +using DurableTask.Samples; -namespace DurableTask.Samples; +namespace DurableTask.Named.Samples; /// /// The samples program. /// public class Program { + private static readonly string s_taskHubName = "myHub"; + /// /// The entry point. /// diff --git a/src/.vscode/delayAndFocusProblems.bat b/src/.vscode/delayAndFocusProblems.bat new file mode 100644 index 0000000..fbb6c9d --- /dev/null +++ b/src/.vscode/delayAndFocusProblems.bat @@ -0,0 +1,3 @@ +@echo off +timeout /t 10 >nul +code -g workbench.action.problems.focus \ No newline at end of file diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json new file mode 100644 index 0000000..dbd72c6 --- /dev/null +++ b/src/.vscode/tasks.json @@ -0,0 +1,35 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "saveAll", + "command": "${command:workbench.action.files.saveAll}", + "type": "shell", + "problemMatcher": [] + }, + { + "label": "clearTerminal", + "command": "cls", + "type": "shell", + "problemMatcher": [] + }, + + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + }, + "dependsOn": [ + "clearTerminal", + "saveAll" + ] + } + ] +} \ No newline at end of file diff --git a/src/DurableTask.DependencyInjection/src/BaseTaskHubClient.cs b/src/DurableTask.DependencyInjection/src/BaseTaskHubClient.cs new file mode 100644 index 0000000..ace4456 --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/BaseTaskHubClient.cs @@ -0,0 +1,17 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using Microsoft.Extensions.Options; + +namespace DurableTask.DependencyInjection +{ + public abstract class BaseTaskHubClient + { + public BaseTaskHubClient(string name) + { + Name = name ?? Options.DefaultName; + } + + public string Name { get; } + } +} diff --git a/src/DurableTask.DependencyInjection/src/BaseTaskHubWorker.cs b/src/DurableTask.DependencyInjection/src/BaseTaskHubWorker.cs new file mode 100644 index 0000000..6e8fa17 --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/BaseTaskHubWorker.cs @@ -0,0 +1,31 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace DurableTask.DependencyInjection +{ + public abstract class BaseTaskHubWorker : IHostedService + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the worker. + protected BaseTaskHubWorker(string? name) + { + Name = name ?? Options.DefaultName; + } + + /// + /// Gets the name of this worker. + /// + protected virtual string Name { get; } + + /// + public abstract Task StartAsync(CancellationToken cancellationToken); + + /// + public abstract Task StopAsync(CancellationToken cancellationToken); + } +} diff --git a/src/DurableTask.DependencyInjection/src/DefaultTaskHubClientBuilder.cs b/src/DurableTask.DependencyInjection/src/DefaultTaskHubClientBuilder.cs new file mode 100644 index 0000000..2d5a4bf --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/DefaultTaskHubClientBuilder.cs @@ -0,0 +1,88 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; +using DurableTask.Core.Serializing; +using DurableTask.DependencyInjection.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + + +namespace DurableTask.DependencyInjection; + +/// +/// The default builder for task hub client. +/// +public class DefaultTaskHubClientBuilder : ITaskHubClientBuilder +{ + private Type? _buildTarget; + + /// + /// Initializes a new instance of the class. + /// + /// The name for this builder. + /// The current service collection, not null. + public DefaultTaskHubClientBuilder(string name, IServiceCollection services) + { + Name = name ?? Options.DefaultName; + Services = Check.NotNull(services); + + } + + /// + public string Name { get; } + + /// + public Type? BuildTarget + { + get => _buildTarget; + set + { + if (value is not null) + { + Check.ConcreteType(value); + } + + _buildTarget = value; + } + } + + /// + public IServiceCollection Services { get; } + + public Func? OrchestrationServiceFactory { get; set; } + + /// + /// Builds and returns a using the configurations from this instance. + /// + /// The service provider. + /// A new . + public BaseTaskHubClient Build(IServiceProvider serviceProvider) + { + Check.NotNull(serviceProvider); + + const string error = "No valid DurableTask client target was registered. Ensure a valid client has been" + + " configured via 'UseBuildTarget(Type target)'."; + Check.NotNull(_buildTarget, error); + + IOrchestrationServiceClient orchestrationService = serviceProvider.GetService(); + if (orchestrationService is null && OrchestrationServiceFactory is not null) + { + orchestrationService = OrchestrationServiceFactory(serviceProvider); + } + const string error2 = "No valid OrchestrationServiceClient was registered. Ensure a valid OrchestrationServiceClient has been" + + " configured via 'WithOrchestrationService(IOrchestrationServiceClient orchestrationService)'."; + Check.NotNull(orchestrationService, error2); + + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + // Options does not have to be present. + IOptions options = serviceProvider.GetService>(); + DataConverter converter = options?.Value?.DataConverter ?? JsonDataConverter.Default; + + var client = new TaskHubClient(orchestrationService, converter, loggerFactory); + + return (BaseTaskHubClient)ActivatorUtilities.CreateInstance(serviceProvider, _buildTarget, Name, client); + } +} diff --git a/src/DurableTask.DependencyInjection/src/DefaultTaskHubWorkerBuilder.cs b/src/DurableTask.DependencyInjection/src/DefaultTaskHubWorkerBuilder.cs index 50f28ea..a3bc924 100644 --- a/src/DurableTask.DependencyInjection/src/DefaultTaskHubWorkerBuilder.cs +++ b/src/DurableTask.DependencyInjection/src/DefaultTaskHubWorkerBuilder.cs @@ -8,7 +8,10 @@ using DurableTask.DependencyInjection.Orchestrations; using DurableTask.DependencyInjection.Properties; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + namespace DurableTask.DependencyInjection; @@ -17,52 +20,94 @@ namespace DurableTask.DependencyInjection; /// public class DefaultTaskHubWorkerBuilder : ITaskHubWorkerBuilder { + private Type? _buildTarget; + /// /// Initializes a new instance of the class. /// + /// The name for this builder. /// The current service collection, not null. - public DefaultTaskHubWorkerBuilder(IServiceCollection services) + public DefaultTaskHubWorkerBuilder(IServiceCollection services) : + this(Options.DefaultName, services) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name for this builder. + /// The current service collection, not null. + public DefaultTaskHubWorkerBuilder(string name, IServiceCollection services) + { + Name = name ?? Options.DefaultName; Services = Check.NotNull(services); } + /// + public string Name { get; } + /// public IServiceCollection Services { get; } /// public IOrchestrationService? OrchestrationService { get; set; } - /// - public IList ActivityMiddleware { get; } = new List + public Func? OrchestrationServiceFactory { get; set; } + + /// + public Type? BuildTarget { - new TaskMiddlewareDescriptor(typeof(ServiceProviderActivityMiddleware)), - }; + get => _buildTarget; + set + { + if (value is not null) + { + Check.ConcreteType(value); + } + + _buildTarget = value; + } + } /// - public IList OrchestrationMiddleware { get; } = new List - { - new TaskMiddlewareDescriptor(typeof(ServiceProviderOrchestrationMiddleware)), - }; + public IList ActivityMiddleware { get; } = + [ + new(typeof(ServiceProviderActivityMiddleware)), + ]; + + /// + public IList OrchestrationMiddleware { get; } = + [ + new(typeof(ServiceProviderOrchestrationMiddleware)), + ]; /// - public IList Activities { get; } = new List(); + public IList Activities { get; } = []; /// - public IList Orchestrations { get; } = new List(); + public IList Orchestrations { get; } = []; /// /// Builds and returns a using the configurations from this instance. /// /// The service provider. /// A new . - public TaskHubWorker Build(IServiceProvider serviceProvider) + public IHostedService Build(IServiceProvider serviceProvider) { Check.NotNull(serviceProvider); - if (OrchestrationService is null) + const string error = "No valid DurableTask worker target was registered. Ensure a valid worker has been" + + " configured via 'UseBuildTarget(Type target)'."; + //Check.NotNull(_buildTarget, error); + + IOrchestrationService orchestrationService = serviceProvider.GetService(); + if (orchestrationService is null && OrchestrationServiceFactory is not null) { - OrchestrationService = serviceProvider.GetRequiredService(); + orchestrationService = OrchestrationServiceFactory(serviceProvider); } + const string error2 = "No valid OrchestrationService was registered. Ensure a valid OrchestrationService has been" + + " configured via 'WithOrchestrationService(IOrchestrationService orchestrationService)'."; + Check.NotNull(orchestrationService, error2); // Verify we still have our ServiceProvider middleware if (OrchestrationMiddleware.FirstOrDefault(x => x.Type == typeof(ServiceProviderOrchestrationMiddleware)) @@ -80,7 +125,7 @@ public TaskHubWorker Build(IServiceProvider serviceProvider) ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); TaskHubWorker worker = new( - OrchestrationService, + orchestrationService, new GenericObjectManager(), new GenericObjectManager(), loggerFactory); @@ -102,7 +147,7 @@ public TaskHubWorker Build(IServiceProvider serviceProvider) worker.AddActivityDispatcherMiddleware(WrapMiddleware(middlewareDescriptor)); } - return worker; + return (IHostedService)ActivatorUtilities.CreateInstance(serviceProvider, _buildTarget, Name, worker, loggerFactory); } private static Func, Task> WrapMiddleware( diff --git a/src/DurableTask.DependencyInjection/src/DurableTask.DependencyInjection.csproj b/src/DurableTask.DependencyInjection/src/DurableTask.DependencyInjection.csproj index efb48fb..819fbd6 100644 --- a/src/DurableTask.DependencyInjection/src/DurableTask.DependencyInjection.csproj +++ b/src/DurableTask.DependencyInjection/src/DurableTask.DependencyInjection.csproj @@ -12,6 +12,7 @@ + diff --git a/src/DurableTask.DependencyInjection/src/DurableTaskClientProvider.cs b/src/DurableTask.DependencyInjection/src/DurableTaskClientProvider.cs new file mode 100644 index 0000000..fbffd84 --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/DurableTaskClientProvider.cs @@ -0,0 +1,63 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using Microsoft.Extensions.Options; + +namespace DurableTask.DependencyInjection +{ + internal class DefaultTaskHubClientProvider : ITaskHubClientProvider + { + private readonly IEnumerable _clients; + + /// + /// Initializes a new instance of the class. + /// + /// The set of clients. + public DefaultTaskHubClientProvider(IEnumerable clients) + { + _clients = clients; + } + + /// + public BaseTaskHubClient GetClient(string? name = null) + { + name ??= Options.DefaultName; + ClientContainer? client = _clients.FirstOrDefault( + x => string.Equals(name, x.Name, StringComparison.Ordinal)); // options are case sensitive. + + if (client is null) + { + string names = string.Join(", ", _clients.Select(x => $"\"{x.Name}\"")); + throw new ArgumentOutOfRangeException( + nameof(name), name, $"The value of this argument must be in the set of available clients: [{names}]."); + } + + return client.Client; + } + + /// + /// Container for holding a client in memory. + /// + internal class ClientContainer + { + /// + /// Initializes a new instance of the class. + /// + /// The client. + public ClientContainer(BaseTaskHubClient client) + { + Client = Check.NotNull(client); + } + + /// + /// Gets the client name. + /// + public string Name => Client.Name; + + /// + /// Gets the client. + /// + public BaseTaskHubClient Client { get; } + } + } +} diff --git a/src/DurableTask.DependencyInjection/src/Extensions/TaskHubClientBuilderExtensions.cs b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubClientBuilderExtensions.cs new file mode 100644 index 0000000..33fa317 --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubClientBuilderExtensions.cs @@ -0,0 +1,85 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using DurableTask.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace DurableTask.DependencyInjection.Extensions +{ + public static class TaskHubClientBuilderExtensions + { + /// + /// Sets the provided to the . + /// + /// The task hub builder. + /// The orchestration service to use. + /// The original builder, with orchestration service set. + public static ITaskHubClientBuilder WithOrchestrationService( + this ITaskHubClientBuilder builder, IOrchestrationServiceClient orchestrationService) + { + Check.NotNull(builder); + Check.NotNull(orchestrationService); + builder.OrchestrationServiceFactory = (sp) => orchestrationService; + return builder; + } + + /// + /// Sets the provided to the . + /// + /// The task hub builder. + /// The orchestration service factory to use. + /// The original builder, with orchestration service set. + public static ITaskHubClientBuilder WithOrchestrationService( + this ITaskHubClientBuilder builder, Func orchestrationServiceFactory) + { + Check.NotNull(builder); + Check.NotNull(orchestrationServiceFactory); + builder.OrchestrationServiceFactory = orchestrationServiceFactory; + return builder; + } + + /// + /// Registers this builders directly to the service container. This will allow for + /// directly importing . This can only be used for a single builder. Only + /// the first call will register. + /// + /// The builder to register the client directly of. + /// The original builder, for call chaining. + public static ITaskHubClientBuilder RegisterDirectly(this ITaskHubClientBuilder builder) + { + BaseTaskHubClient GetClient(IServiceProvider services) + { + ITaskHubClientProvider provider = services.GetRequiredService(); + return provider.GetClient(builder.Name); + } + + builder.Services.TryAddSingleton(GetClient); + return builder; + } + + /// + /// Sets the build target for this builder. + /// startup. + /// + /// The builder to set the builder target for. + /// The type of target to set. + /// The original builder, for call chaining. + public static ITaskHubClientBuilder UseBuildTarget(this ITaskHubClientBuilder builder, Type target) + { + builder.BuildTarget = target; + return builder; + } + + /// + /// Sets the build target for this builder. + /// startup. + /// + /// The builder target type. + /// The builder to set the builder target for. + /// The original builder, for call chaining. + public static ITaskHubClientBuilder UseBuildTarget(this ITaskHubClientBuilder builder) + where TTarget : BaseTaskHubClient + => builder.UseBuildTarget(typeof(TTarget)); + } +} diff --git a/src/DurableTask.DependencyInjection/src/Extensions/TaskHubClientServiceCollectionExtensions.cs b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubClientServiceCollectionExtensions.cs new file mode 100644 index 0000000..1a64bcf --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubClientServiceCollectionExtensions.cs @@ -0,0 +1,119 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using DurableTask.DependencyInjection.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace DurableTask.DependencyInjection +{ + + /// + /// Extensions for . + /// + public static class TaskHubClientServiceCollectionExtensions + { + /// + /// 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 ITaskHubClientBuilder AddTaskHubClient(this IServiceCollection services, string? name = null) + { + Check.NotNull(services); + ITaskHubClientBuilder builder = GetBuilder(services, name ?? Options.DefaultName, out bool added); + ConditionalConfigureBuilder(services, builder, added); + return builder; + } + + /// + /// Configures and adds a to the service collection. + /// + /// The services to add to. + /// The callback to configure the client. + /// The original service collection, for call chaining. + public static IServiceCollection AddTaskHubClient(this IServiceCollection services, Action configure) + { + return services.AddTaskHubClient(Options.DefaultName, configure); + } + + /// + /// Configures and adds a to the service collection. + /// + /// The services to add to. + /// Gets the name of the client to add. + /// The callback to configure the client. + /// The original service collection, for call chaining. + public static IServiceCollection AddTaskHubClient(this IServiceCollection services, string name, Action configure) + { + services.TryAddSingleton(); + ITaskHubClientBuilder builder = GetBuilder(services, name, out bool added); + configure.Invoke(builder); + ConditionalConfigureBuilder(services, builder, added); + return services; + } + + private static void ConditionalConfigureBuilder(IServiceCollection services, ITaskHubClientBuilder builder, bool configure) + { + if (!configure) + { + 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 DefaultTaskHubClientProvider.ClientContainer(builder.Build(sp))); + + if (builder.Name == Options.DefaultName) + { + // If we have the default options name here, we will inject this client directly. + builder.RegisterDirectly(); + } + } + + private static ITaskHubClientBuilder GetBuilder(IServiceCollection services, string name, out bool added) + { + // To ensure the builders are tracked with this service collection, we use a singleton service descriptor as a + // holder for all builders. + ServiceDescriptor descriptor = services.FirstOrDefault(sd => sd.ServiceType == typeof(BuilderContainer)); + + if (descriptor is null) + { + descriptor = ServiceDescriptor.Singleton(new BuilderContainer(services)); + services.Add(descriptor); + } + + var container = (BuilderContainer)descriptor.ImplementationInstance!; + return container.GetOrAdd(name, out added); + } + + /// + /// A container which is used to store and retrieve builders from within the . + /// + private class BuilderContainer + { + private readonly Dictionary _builders = []; + private readonly IServiceCollection _services; + + public BuilderContainer(IServiceCollection services) + { + _services = services; + } + + public ITaskHubClientBuilder GetOrAdd(string name, out bool added) + { + added = false; + if (!_builders.TryGetValue(name, out ITaskHubClientBuilder builder)) + { + builder = new DefaultTaskHubClientBuilder(name, _services); + _builders[name] = builder; + added = true; + } + + return builder; + } + } + } +} diff --git a/src/DurableTask.DependencyInjection/src/Extensions/TaskHubServiceCollectionExtensions.cs b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubServiceCollectionExtensions.cs index 6bb5914..2e6ef1f 100644 --- a/src/DurableTask.DependencyInjection/src/Extensions/TaskHubServiceCollectionExtensions.cs +++ b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubServiceCollectionExtensions.cs @@ -12,40 +12,40 @@ namespace DurableTask.DependencyInjection; /// public static class TaskHubServiceCollectionExtensions { - /// - /// Adds a and related services to the service collection. - /// - /// The service collection to add to. - /// The action to configure the task hub builder with. - /// The original service collection, with services added. - public static IServiceCollection AddTaskHubWorker( - this IServiceCollection services, Action configure) - { - Check.NotNull(services); - Check.NotNull(configure); - - ITaskHubWorkerBuilder builder = services.AddTaskHubWorkerCore(); - configure(builder); - - return services; - } - - private static ITaskHubWorkerBuilder AddTaskHubWorkerCore(this IServiceCollection services) - { - services.AddLogging(); - - // This is added as a singleton implementation instance as we will fetch this out of the service collection - // during subsequent calls to AddTaskHubWorker. - DefaultTaskHubWorkerBuilder builder = new(services); - services.TryAddSingleton(builder); - services.TryAddSingleton(sp => builder.Build(sp)); - - return services.GetTaskHubBuilder(); - } - - private static ITaskHubWorkerBuilder GetTaskHubBuilder(this IServiceCollection services) - { - return (ITaskHubWorkerBuilder)services.Single(sd => sd.ServiceType == typeof(ITaskHubWorkerBuilder)) - .ImplementationInstance; - } + ///// + ///// Adds a and related services to the service collection. + ///// + ///// The service collection to add to. + ///// The action to configure the task hub builder with. + ///// The original service collection, with services added. + //public static IServiceCollection AddTaskHubWorker( + // this IServiceCollection services, Action configure) + //{ + // Check.NotNull(services); + // Check.NotNull(configure); + + // ITaskHubWorkerBuilder builder = services.AddTaskHubWorkerCore(); + // configure(builder); + + // return services; + //} + + //private static ITaskHubWorkerBuilder AddTaskHubWorkerCore(this IServiceCollection services) + //{ + // services.AddLogging(); + + // // This is added as a singleton implementation instance as we will fetch this out of the service collection + // // during subsequent calls to AddTaskHubWorker. + // DefaultTaskHubWorkerBuilder builder = new(services); + // services.TryAddSingleton(builder); + // services.TryAddSingleton(sp => builder.Build(sp)); + + // return services.GetTaskHubBuilder(); + //} + + //private static ITaskHubWorkerBuilder GetTaskHubBuilder(this IServiceCollection services) + //{ + // return (ITaskHubWorkerBuilder)services.Single(sd => sd.ServiceType == typeof(ITaskHubWorkerBuilder)) + // .ImplementationInstance; + //} } diff --git a/src/DurableTask.DependencyInjection/src/Extensions/TaskHubWorkerBuilderExtensions.cs b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubWorkerBuilderExtensions.cs index da77195..282c359 100644 --- a/src/DurableTask.DependencyInjection/src/Extensions/TaskHubWorkerBuilderExtensions.cs +++ b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubWorkerBuilderExtensions.cs @@ -5,10 +5,10 @@ using DurableTask.Core.Serializing; using DurableTask.DependencyInjection.Internal; using DurableTask.DependencyInjection.Properties; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; namespace DurableTask.DependencyInjection; @@ -29,6 +29,7 @@ public static ITaskHubWorkerBuilder WithOrchestrationService( Check.NotNull(builder); Check.NotNull(orchestrationService); builder.Services.TryAddSingleton(orchestrationService); + builder.OrchestrationServiceFactory = (sp) => orchestrationService; return builder; } @@ -43,7 +44,7 @@ public static ITaskHubWorkerBuilder WithOrchestrationService( { Check.NotNull(builder); Check.NotNull(orchestrationServiceFactory); - builder.Services.TryAddSingleton(orchestrationServiceFactory); + builder.OrchestrationServiceFactory = orchestrationServiceFactory; return builder; } @@ -65,10 +66,8 @@ private static TaskHubClient ClientFactory(ITaskHubWorkerBuilder builder, IServi if (client is null) { -#pragma warning disable CS0618 // Type or member is obsolete IOrchestrationService service = builder.OrchestrationService ?? serviceProvider.GetRequiredService(); -#pragma warning restore CS0618 // Type or member is obsolete client = service as IOrchestrationServiceClient; if (client is null) diff --git a/src/DurableTask.DependencyInjection/src/Extensions/TaskHubWorkerServiceCollectionExtensions.cs b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubWorkerServiceCollectionExtensions.cs new file mode 100644 index 0000000..759f4ac --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/Extensions/TaskHubWorkerServiceCollectionExtensions.cs @@ -0,0 +1,138 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace DurableTask.DependencyInjection +{ + /// + /// Extensions for . + /// + public static class TaskHubWorkerServiceCollectionExtensions + { + /// + /// 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 ITaskHubWorkerBuilder AddTaskHubWorker(this IServiceCollection services, string? name = null) + { + Check.NotNull(services); + ITaskHubWorkerBuilder 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. + /// + /// The service collection to add to. + /// The callback to configure the builder. + /// The service collection for call chaining. + public static IServiceCollection AddTaskHubWorker( + this IServiceCollection services, Action configure) + { + Check.NotNull(services); + Check.NotNull(configure); + return services.AddTaskHubWorker(Options.DefaultName, configure); + } + + /// + /// 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 callback to configure the builder. + /// The service collection for call chaining. + public static IServiceCollection AddTaskHubWorker( + this IServiceCollection services, string name, Action configure) + { + Check.NotNull(services); + Check.NotNull(name); + Check.NotNull(configure); + + services.AddLogging(); + + ITaskHubWorkerBuilder builder = GetBuilder(services, name, out bool added); + configure.Invoke(builder); + ConditionalConfigureBuilder(services, builder, added); + return services; + } + + /// + /// Sets the build target for this builder. This is the hosted service which will ultimately be ran on host + /// startup. + /// + /// The builder to set the builder target for. + /// The type of target to set. + /// The original builder, for call chaining. + public static ITaskHubWorkerBuilder UseBuildTarget(this ITaskHubWorkerBuilder builder, Type target) + { + Check.NotNull(builder); + builder.BuildTarget = target; + return builder; + } + + /// + /// Sets the build target for this builder. This is the hosted service which will ultimately be ran on host + /// startup. + /// + /// The builder target type. + /// The builder to set the builder target for. + /// The original builder, for call chaining. + public static ITaskHubWorkerBuilder UseBuildTarget(this ITaskHubWorkerBuilder builder) + where TTarget : BaseTaskHubWorker + => builder.UseBuildTarget(typeof(TTarget)); + + private static void ConditionalConfigureBuilder( + IServiceCollection services, ITaskHubWorkerBuilder builder, bool configure) + { + if (!configure) + { + return; + } + + services.AddSingleton(sp => builder.Build(sp)); + } + + private static ITaskHubWorkerBuilder GetBuilder(IServiceCollection services, string name, out bool added) + { + // To ensure the builders are tracked with this service collection, we use a singleton service descriptor as a + // holder for all builders. + ServiceDescriptor descriptor = services.FirstOrDefault(sd => sd.ServiceType == typeof(BuilderContainer)); + + if (descriptor is null) + { + descriptor = ServiceDescriptor.Singleton(new BuilderContainer(services)); + services.Add(descriptor); + } + + var container = (BuilderContainer)descriptor.ImplementationInstance!; + return container.GetOrAdd(name, out added); + } + + /// + /// A container which is used to store and retrieve builders from within the . + /// + private class BuilderContainer(IServiceCollection services) + { + private readonly Dictionary _builders = []; + private readonly IServiceCollection _services = services; + + public ITaskHubWorkerBuilder GetOrAdd(string name, out bool added) + { + added = false; + if (!_builders.TryGetValue(name, out ITaskHubWorkerBuilder builder)) + { + builder = new DefaultTaskHubWorkerBuilder(name, _services); + _builders[name] = builder; + added = true; + } + + return builder; + } + } + } +} diff --git a/src/DurableTask.DependencyInjection/src/ITaskHubClientBuilder.cs b/src/DurableTask.DependencyInjection/src/ITaskHubClientBuilder.cs new file mode 100644 index 0000000..cb3fa4c --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/ITaskHubClientBuilder.cs @@ -0,0 +1,40 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using DurableTask.Core; +using Microsoft.Extensions.DependencyInjection; + +namespace DurableTask.DependencyInjection +{ + /// + /// A builder for configuring and adding a to the service container. + /// + public interface ITaskHubClientBuilder + { + /// + /// Gets the name of the client being built. + /// + string Name { get; } + + /// + /// Gets the service collection. + /// + IServiceCollection Services { get; } + + Func? OrchestrationServiceFactory { get; set; } + + /// + /// Gets or sets the target of this builder. The provided type must derive from + /// . This is the type that will ultimately be built by + /// . + /// + Type? BuildTarget { get; set; } + + /// + /// Builds this instance, yielding the built . + /// + /// The service provider. + /// The built client. + BaseTaskHubClient Build(IServiceProvider serviceProvider); + } +} diff --git a/src/DurableTask.DependencyInjection/src/ITaskHubClientProvider.cs b/src/DurableTask.DependencyInjection/src/ITaskHubClientProvider.cs new file mode 100644 index 0000000..514bba3 --- /dev/null +++ b/src/DurableTask.DependencyInjection/src/ITaskHubClientProvider.cs @@ -0,0 +1,21 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +namespace DurableTask.DependencyInjection +{ + /// + /// A provider for getting . + /// + /// + /// The purpose of this abstraction is that there may be multiple clients registered, so they cannot be DI'd directly. + /// + public interface ITaskHubClientProvider + { + /// + /// Gets the by name. Throws if the client by the requested name is not found. + /// + /// The name of the client to get or null to get the default client. + /// The client. + BaseTaskHubClient GetClient(string? name = null); + } +} diff --git a/src/DurableTask.DependencyInjection/src/ITaskHubWorkerBuilder.cs b/src/DurableTask.DependencyInjection/src/ITaskHubWorkerBuilder.cs index 2550ba2..1affe64 100644 --- a/src/DurableTask.DependencyInjection/src/ITaskHubWorkerBuilder.cs +++ b/src/DurableTask.DependencyInjection/src/ITaskHubWorkerBuilder.cs @@ -3,25 +3,38 @@ using DurableTask.Core; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace DurableTask.DependencyInjection; /// /// A builder for hosting a durable task worker. /// -public interface ITaskHubWorkerBuilder +public interface ITaskHubWorkerBuilder { + /// + /// Gets the name of this builder. + /// + string Name { get; } + /// /// Gets the where durable task services are configured. /// IServiceCollection Services { get; } + /// + /// Gets or sets the build target for this builder. The provided type must derive from + /// . This is the hosted service which will ultimately be ran on host startup. + /// + Type? BuildTarget { get; set; } + + IOrchestrationService? OrchestrationService { get; set; } + /// /// Gets or sets the to use. If this is null, it will be fetched from the /// service provider. /// - [Obsolete("Add IOrchestrationService to the IServiceCollection as a singleton instead.")] - IOrchestrationService? OrchestrationService { get; set; } + Func? OrchestrationServiceFactory { get; set; } /// /// Gets the activity middleware. @@ -42,4 +55,12 @@ public interface ITaskHubWorkerBuilder /// Gets the orchestrations. /// IList Orchestrations { get; } + + + /// + /// Build the hosted service which runs the worker. + /// + /// The service provider. + /// The built hosted service. + IHostedService Build(IServiceProvider serviceProvider); } diff --git a/src/DurableTask.DependencyInjection/src/Internal/TaskHubClientOptions.cs b/src/DurableTask.DependencyInjection/src/Internal/TaskHubClientOptions.cs index 0b99ef9..6d62c25 100644 --- a/src/DurableTask.DependencyInjection/src/Internal/TaskHubClientOptions.cs +++ b/src/DurableTask.DependencyInjection/src/Internal/TaskHubClientOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Jacob Viau. All rights reserved. +// Copyright (c) Jacob Viau. All rights reserved. // Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. using DurableTask.Core.Serializing; @@ -13,7 +13,7 @@ namespace DurableTask.DependencyInjection.Internal; /// /// /// This is internal because it is only a piece of the puzzle for replacing the -/// throughout all of DTFx. Please us Vio.DurableTask.Extensions package to properly replace the entire +/// throughout all of DTFx. Please use Vio.DurableTask.Extensions package to properly replace the entire /// data converter. /// public sealed class TaskHubClientOptions diff --git a/src/DurableTask.Extensions/src/Middleware/SetActivityDataMiddleware.cs b/src/DurableTask.Extensions/src/Middleware/SetActivityDataMiddleware.cs index 3ff0e26..76ee068 100644 --- a/src/DurableTask.Extensions/src/Middleware/SetActivityDataMiddleware.cs +++ b/src/DurableTask.Extensions/src/Middleware/SetActivityDataMiddleware.cs @@ -1,4 +1,4 @@ -// Copyright (c) Jacob Viau. All rights reserved. +// Copyright (c) Jacob Viau. All rights reserved. // Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. using DurableTask.Core; @@ -6,7 +6,6 @@ using DurableTask.Core.Middleware; using DurableTask.Core.Serializing; using DurableTask.DependencyInjection; -using DurableTask.Extensions.Abstractions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/DurableTask.Extensions/src/Middleware/SetOrchestrationDataMiddleware.cs b/src/DurableTask.Extensions/src/Middleware/SetOrchestrationDataMiddleware.cs index 81483a3..0d3b649 100644 --- a/src/DurableTask.Extensions/src/Middleware/SetOrchestrationDataMiddleware.cs +++ b/src/DurableTask.Extensions/src/Middleware/SetOrchestrationDataMiddleware.cs @@ -1,11 +1,10 @@ -// Copyright (c) Jacob Viau. All rights reserved. +// Copyright (c) Jacob Viau. All rights reserved. // Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. using DurableTask.Core; using DurableTask.Core.Middleware; using DurableTask.Core.Serializing; using DurableTask.DependencyInjection; -using DurableTask.Extensions.Abstractions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/DurableTask.Hosting.sln b/src/DurableTask.Hosting.sln index 578f765..68c884c 100644 --- a/src/DurableTask.Hosting.sln +++ b/src/DurableTask.Hosting.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.12.35506.116 d17.12 +VisualStudioVersion = 17.12.35506.116 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DurableTask.DependencyInjection", "DurableTask.DependencyInjection", "{F7C54D09-8F90-40A6-A7ED-DBF11FD97354}" EndProject @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Instrumentation EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Instrumentation.Samples", "..\samples\DurableTask.Instrumentation.Samples\DurableTask.Instrumentation.Samples.csproj", "{A51D0C94-E0B9-4063-A8C9-7424362CDB80}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Named.Samples", "..\samples\DurableTask.Named.Samples\DurableTask.Named.Samples.csproj", "{4CC61EE5-758E-9BA1-C895-B3055EE75096}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -207,6 +209,18 @@ Global {A51D0C94-E0B9-4063-A8C9-7424362CDB80}.Release|x64.Build.0 = Release|Any CPU {A51D0C94-E0B9-4063-A8C9-7424362CDB80}.Release|x86.ActiveCfg = Release|Any CPU {A51D0C94-E0B9-4063-A8C9-7424362CDB80}.Release|x86.Build.0 = Release|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Debug|x64.ActiveCfg = Debug|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Debug|x64.Build.0 = Debug|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Debug|x86.ActiveCfg = Debug|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Debug|x86.Build.0 = Debug|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Release|Any CPU.Build.0 = Release|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Release|x64.ActiveCfg = Release|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Release|x64.Build.0 = Release|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Release|x86.ActiveCfg = Release|Any CPU + {4CC61EE5-758E-9BA1-C895-B3055EE75096}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -224,6 +238,7 @@ Global {052B06E1-0268-446A-BA1A-57B8A8AD8B22} = {2CEE0150-2FD3-4246-82DA-2EB68A95FCF3} {E1AD91B6-6458-4502-9AC1-0FFE93D2FBE6} = {2CEE0150-2FD3-4246-82DA-2EB68A95FCF3} {A51D0C94-E0B9-4063-A8C9-7424362CDB80} = {796C6FFF-5FB6-4918-88B7-61B6EA4D2801} + {4CC61EE5-758E-9BA1-C895-B3055EE75096} = {796C6FFF-5FB6-4918-88B7-61B6EA4D2801} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65A5E590-4609-44E1-B012-0EA997E18DB7} diff --git a/src/DurableTask.Hosting/src/DurableTaskHubClient.cs b/src/DurableTask.Hosting/src/DurableTaskHubClient.cs new file mode 100644 index 0000000..307ba24 --- /dev/null +++ b/src/DurableTask.Hosting/src/DurableTaskHubClient.cs @@ -0,0 +1,19 @@ +// "Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the Apache License 2.0. See LICENSE file in the project root for full license information." + +using DurableTask.Core; +using DurableTask.DependencyInjection; + +namespace DurableTask.Hosting +{ + public class DurableTaskHubClient : BaseTaskHubClient + { + public DurableTaskHubClient(string name, TaskHubClient client) + : base(name) + { + Client = Check.NotNull(client); + } + + public TaskHubClient Client { get; } + } +} diff --git a/src/DurableTask.Hosting/src/DurableTaskHubWorker.cs b/src/DurableTask.Hosting/src/DurableTaskHubWorker.cs new file mode 100644 index 0000000..eb577d2 --- /dev/null +++ b/src/DurableTask.Hosting/src/DurableTaskHubWorker.cs @@ -0,0 +1,70 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. + +using DurableTask.Core; +using DurableTask.DependencyInjection; +using DurableTask.Hosting.Options; +using DurableTask.Hosting.Properties; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DurableTask.Hosting; + +/// +/// A dotnet hosted service for . +/// +public class DurableTaskHubWorker : BaseTaskHubWorker +{ + private readonly TaskHubWorker _worker; + private readonly ILogger _logger; + private readonly IOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the worker. + /// The task hub worker. Not null. + /// The logger factory. Not null. + /// The task hub options. + public DurableTaskHubWorker( + string name, + TaskHubWorker worker, + ILoggerFactory loggerFactory, + IOptions options) : + base(name) + { + _worker = Check.NotNull(worker); + _logger = loggerFactory.CreateLogger($"{typeof(DurableTaskHubWorker).Namespace}.{nameof(DurableTaskHubWorker)}"); + _options = Check.NotNull(options); + } + + private TaskHubOptions Options => _options.Value; + + /// + public override async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogDebug(Strings.TaskHubWorkerStarting); + + if (Options.CreateIfNotExists) + { + await _worker.orchestrationService.CreateIfNotExistsAsync().ConfigureAwait(false); + } + + await _worker.StartAsync().ConfigureAwait(false); + _worker.TaskActivityDispatcher.IncludeDetails = Options.IncludeDetails.HasFlag(IncludeDetails.Activities); + _worker.TaskOrchestrationDispatcher.IncludeDetails = Options.IncludeDetails.HasFlag(IncludeDetails.Orchestrations); + _worker.ErrorPropagationMode = Options.ErrorPropagationMode; + } + + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + var cancel = Task.Delay(Timeout.Infinite, cancellationToken); + Task task = await Task.WhenAny(_worker.StopAsync(), cancel).ConfigureAwait(false); + + if (cancel == task) + { + _logger.LogWarning(Strings.ForcedShutdown); + } + } +} diff --git a/src/DurableTask.Hosting/src/Extensions/TaskHubHostBuilderExtensions.cs b/src/DurableTask.Hosting/src/Extensions/TaskHubHostBuilderExtensions.cs index 8e65860..2a9b122 100644 --- a/src/DurableTask.Hosting/src/Extensions/TaskHubHostBuilderExtensions.cs +++ b/src/DurableTask.Hosting/src/Extensions/TaskHubHostBuilderExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Jacob Viau. All rights reserved. // Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. +using DurableTask.Core; using DurableTask.DependencyInjection; using DurableTask.Hosting.Options; using Microsoft.Extensions.DependencyInjection; @@ -87,8 +88,11 @@ public static IHostBuilder ConfigureTaskHubWorker( .Bind(context.Configuration.GetSection("TaskHub")) .Configure(configureOptions); - services.AddTaskHubWorker(taskHubBuilder => configure(context, taskHubBuilder)); - services.AddHostedService(); + services.AddTaskHubWorker(taskHubBuilder => { + taskHubBuilder.UseBuildTarget(); + configure(context, taskHubBuilder); + }); + //services.AddHostedService(); }); return builder; diff --git a/src/DurableTask.Hosting/src/TaskHubBackgroundService.cs b/src/DurableTask.Hosting/src/TaskHubBackgroundService.cs index 45dafa4..b568ff6 100644 --- a/src/DurableTask.Hosting/src/TaskHubBackgroundService.cs +++ b/src/DurableTask.Hosting/src/TaskHubBackgroundService.cs @@ -2,9 +2,9 @@ // Licensed under the APACHE 2.0. See LICENSE file in the project root for full license information. using DurableTask.Core; +using DurableTask.DependencyInjection; using DurableTask.Hosting.Options; using DurableTask.Hosting.Properties; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,7 +13,7 @@ namespace DurableTask.Hosting; /// /// A dotnet hosted service for . /// -public class TaskHubBackgroundService : IHostedService +public class TaskHubBackgroundService : BaseTaskHubWorker { private readonly TaskHubWorker _worker; private readonly ILogger _logger; @@ -26,19 +26,22 @@ public class TaskHubBackgroundService : IHostedService /// The logger. Not null. /// The task hub options. public TaskHubBackgroundService( + string? name, TaskHubWorker worker, - ILogger logger, + ILoggerFactory loggerFactory, IOptions options) + : base(name) { _worker = Check.NotNull(worker); - _logger = Check.NotNull(logger); + _logger = loggerFactory.CreateLogger($"{typeof(DurableTaskHubWorker).Namespace}.{nameof(DurableTaskHubWorker)}"); + //_logger = Check.NotNull(logger); _options = Check.NotNull(options); } private TaskHubOptions Options => _options.Value; /// - public async Task StartAsync(CancellationToken cancellationToken) + public override async Task StartAsync(CancellationToken cancellationToken) { _logger.LogDebug(Strings.TaskHubWorkerStarting); @@ -55,7 +58,7 @@ public async Task StartAsync(CancellationToken cancellationToken) } /// - public async Task StopAsync(CancellationToken cancellationToken) + public override async Task StopAsync(CancellationToken cancellationToken) { var cancel = Task.Delay(Timeout.Infinite, cancellationToken); Task task = await Task.WhenAny(_worker.StopAsync(), cancel).ConfigureAwait(false);