diff --git a/src/Abstractions/DurableTaskRegistry.Activities.cs b/src/Abstractions/DurableTaskRegistry.Activities.cs index ac525147..5ae03f29 100644 --- a/src/Abstractions/DurableTaskRegistry.Activities.cs +++ b/src/Abstractions/DurableTaskRegistry.Activities.cs @@ -41,6 +41,7 @@ TaskName ITaskActivity singleton public DurableTaskRegistry AddActivity(TaskName name, Type type) { Check.ConcreteType(type); + this.ActivityTypes.Add(type); return this.AddActivity(name, sp => (ITaskActivity)ActivatorUtilities.GetServiceOrCreateInstance(sp, type)); } diff --git a/src/Abstractions/DurableTaskRegistry.Entities.cs b/src/Abstractions/DurableTaskRegistry.Entities.cs index 4b4e386c..6a769f1b 100644 --- a/src/Abstractions/DurableTaskRegistry.Entities.cs +++ b/src/Abstractions/DurableTaskRegistry.Entities.cs @@ -22,6 +22,7 @@ public DurableTaskRegistry AddEntity(TaskName name, Type type) { // TODO: Compile a constructor expression for performance. Check.ConcreteType(type); + this.EntityTypes.Add(type); return this.AddEntity(name, sp => (ITaskEntity)ActivatorUtilities.CreateInstance(sp, type)); } diff --git a/src/Abstractions/DurableTaskRegistry.Orchestrators.cs b/src/Abstractions/DurableTaskRegistry.Orchestrators.cs index 7ad7583f..965fd6b7 100644 --- a/src/Abstractions/DurableTaskRegistry.Orchestrators.cs +++ b/src/Abstractions/DurableTaskRegistry.Orchestrators.cs @@ -40,6 +40,7 @@ public DurableTaskRegistry AddOrchestrator(TaskName name, Type type) { // TODO: Compile a constructor expression for performance. Check.ConcreteType(type); + this.OrchestratorTypes.Add(type); return this.AddOrchestrator(name, () => (ITaskOrchestrator)Activator.CreateInstance(type)); } diff --git a/src/Abstractions/DurableTaskRegistry.cs b/src/Abstractions/DurableTaskRegistry.cs index 03e21c45..b72a269d 100644 --- a/src/Abstractions/DurableTaskRegistry.cs +++ b/src/Abstractions/DurableTaskRegistry.cs @@ -31,6 +31,21 @@ public sealed partial class DurableTaskRegistry internal IDictionary> Entities { get; } = new Dictionary>(); + /// + /// Gets the types of registered activities. + /// + internal HashSet ActivityTypes { get; } = new(); + + /// + /// Gets the types of registered orchestrators. + /// + internal HashSet OrchestratorTypes { get; } = new(); + + /// + /// Gets the types of registered entities. + /// + internal HashSet EntityTypes { get; } = new(); + /// /// Registers an activity factory. /// diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs index 3f349b71..0cf9db30 100644 --- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs +++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.DurableTask.Worker.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using static Microsoft.DurableTask.Worker.DurableTaskWorkerOptions; @@ -137,4 +138,66 @@ public static IDurableTaskWorkerBuilder UseOrchestrationFilter(this IDurableTask builder.Services.AddSingleton(filter); return builder; } + + /// + /// Registers all task types (activities, orchestrators, and entities) from the specified registry configuration + /// as services in the dependency injection container. This allows these types to participate in container + /// validation and enables early detection of dependency resolution issues. + /// + /// The builder to register tasks for. + /// + /// A callback that provides the registry. This callback will be invoked immediately to extract registered task types. + /// The same callback will also be registered with to ensure tasks are registered with the worker. + /// + /// The original builder, for call chaining. + /// + /// + /// Only task types registered via type-based registration methods (e.g., ) + /// will be registered in the container. Tasks registered via factory methods or singleton instances will not be included. + /// + /// + /// Example usage: + /// + /// builder.Services.AddDurableTaskWorker() + /// .AddTasksAsServices(tasks => + /// { + /// tasks.AddActivity<MyActivity>(); + /// tasks.AddOrchestrator<MyOrchestrator>(); + /// }); + /// + /// + /// + public static IDurableTaskWorkerBuilder AddTasksAsServices( + this IDurableTaskWorkerBuilder builder, Action configure) + { + Check.NotNull(builder); + Check.NotNull(configure); + + // Create a temporary registry to extract the types + DurableTaskRegistry tempRegistry = new(); + configure(tempRegistry); + + // Register all activity types + foreach (Type activityType in tempRegistry.ActivityTypes) + { + builder.Services.TryAddTransient(activityType); + } + + // Register all orchestrator types + foreach (Type orchestratorType in tempRegistry.OrchestratorTypes) + { + builder.Services.TryAddTransient(orchestratorType); + } + + // Register all entity types + foreach (Type entityType in tempRegistry.EntityTypes) + { + builder.Services.TryAddTransient(entityType); + } + + // Also register the tasks with the builder so they're available to the worker + builder.AddTasks(configure); + + return builder; + } } diff --git a/test/Worker/Core.Tests/DependencyInjection/DurableTaskWorkerBuilderExtensionsTests.cs b/test/Worker/Core.Tests/DependencyInjection/DurableTaskWorkerBuilderExtensionsTests.cs index a1bd0785..6d321df6 100644 --- a/test/Worker/Core.Tests/DependencyInjection/DurableTaskWorkerBuilderExtensionsTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/DurableTaskWorkerBuilderExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.DurableTask.Entities; using Microsoft.DurableTask.Worker.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -62,6 +63,147 @@ public void Configure_ConfiguresOptions() actual.Should().BeSameAs(expected); } + [Fact] + public void AddTasksAsServices_RegistersActivityTypes() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + + // Act + builder.AddTasksAsServices(registry => + { + registry.AddActivity(); + }); + + // Assert + IServiceProvider provider = services.BuildServiceProvider(); + provider.GetService().Should().NotBeNull(); + } + + [Fact] + public void AddTasksAsServices_RegistersOrchestratorTypes() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + + // Act + builder.AddTasksAsServices(registry => + { + registry.AddOrchestrator(); + }); + + // Assert + IServiceProvider provider = services.BuildServiceProvider(); + provider.GetService().Should().NotBeNull(); + } + + [Fact] + public void AddTasksAsServices_RegistersEntityTypes() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + + // Act + builder.AddTasksAsServices(registry => + { + registry.AddEntity(); + }); + + // Assert + IServiceProvider provider = services.BuildServiceProvider(); + provider.GetService().Should().NotBeNull(); + } + + [Fact] + public void AddTasksAsServices_RegistersMultipleTaskTypes() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + + // Act + builder.AddTasksAsServices(registry => + { + registry.AddActivity(); + registry.AddOrchestrator(); + registry.AddEntity(); + }); + + // Assert + IServiceProvider provider = services.BuildServiceProvider(); + provider.GetService().Should().NotBeNull(); + provider.GetService().Should().NotBeNull(); + provider.GetService().Should().NotBeNull(); + } + + [Fact] + public void AddTasksAsServices_DoesNotRegisterFunctionBasedTasks() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + + // Act + builder.AddTasksAsServices(registry => + { + registry.AddActivityFunc("testFunc", (TaskActivityContext ctx) => Task.CompletedTask); + }); + + // Assert - No exception should be thrown and no types should be registered + IServiceProvider provider = services.BuildServiceProvider(); + // There should be no issue building the service provider + provider.Should().NotBeNull(); + } + + [Fact] + public void AddTasksAsServices_DoesNotRegisterSingletonInstances() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + TestActivity singletonActivity = new(); + + // Act + builder.AddTasksAsServices(registry => + { + registry.AddActivity(singletonActivity); + }); + + // Assert - Singleton instances should not be registered in DI + IServiceProvider provider = services.BuildServiceProvider(); + // Verify that TestActivity is not registered as a service + // (it's only registered as a singleton instance with the worker) + provider.GetService().Should().BeNull(); + } + + [Fact] + public void AddTasksAsServices_AlsoRegistersTasksWithWorker() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + + // Act + builder.AddTasksAsServices(registry => + { + registry.AddActivity(); + }); + + // Assert - Tasks should be registered both as services and with the worker + IServiceProvider provider = services.BuildServiceProvider(); + provider.GetService().Should().NotBeNull(); + + // Also verify the task is registered with the worker by checking the factory + IDurableTaskFactory factory = provider.GetRequiredService>() + .Get("test") + .BuildFactory(); + factory.TryCreateActivity(nameof(TestActivity), provider, out ITaskActivity? activity).Should().BeTrue(); + activity.Should().NotBeNull(); + } + class BadBuildTarget : BackgroundService { protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -90,4 +232,28 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) throw new NotImplementedException(); } } + + sealed class TestActivity : TaskActivity + { + public override Task RunAsync(TaskActivityContext context, object input) + { + return Task.FromResult(input); + } + } + + sealed class TestOrchestrator : TaskOrchestrator + { + public override Task RunAsync(TaskOrchestrationContext context, object input) + { + return Task.FromResult(input); + } + } + + sealed class TestEntity : TaskEntity + { + public void Operation(object input) + { + // Simple operation for testing + } + } }