From 46b96fe8496b8ba16d221babd7596c0a3518353d Mon Sep 17 00:00:00 2001 From: Robert Karlsson Date: Tue, 1 Sep 2026 17:56:07 +0900 Subject: [PATCH] make attribute-based worker discovery configurable --- .../DependencyInjectionExtensions.cs | 29 ++++++++ .../Client/Worker/WorkerDiscoveryOptions.cs | 17 +++++ .../Client/Worker/WorkflowTaskCoordinator.cs | 24 ++++++- .../DependencyInjectionExtensionsTests.cs | 67 ++++++++++++++++++- docs/workers.md | 27 ++++++++ 5 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 Conductor/Client/Worker/WorkerDiscoveryOptions.cs diff --git a/Conductor/Client/Extensions/DependencyInjectionExtensions.cs b/Conductor/Client/Extensions/DependencyInjectionExtensions.cs index bd5ccacf..280d91d4 100644 --- a/Conductor/Client/Extensions/DependencyInjectionExtensions.cs +++ b/Conductor/Client/Extensions/DependencyInjectionExtensions.cs @@ -16,7 +16,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; +using System.Linq; using System.Net.Http; +using System.Reflection; namespace Conductor.Client.Extensions { @@ -63,5 +65,32 @@ public static IServiceCollection WithHostedService(this IServiceCollection servi services.AddHostedService(); return services; } + + public static IServiceCollection ConfigureConductorWorkerDiscovery(this IServiceCollection services, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + services.AddOptions().Configure(configure); + + return services; + } + + public static IServiceCollection ConfigureConductorWorkerDiscovery(this IServiceCollection services, WorkerDiscoveryOptions discoveryOptions) + { + if (discoveryOptions == null) + { + throw new ArgumentNullException(nameof(discoveryOptions)); + } + + var assemblies = discoveryOptions.Assemblies?.ToArray() ?? Array.Empty(); + + return services.ConfigureConductorWorkerDiscovery(options => + { + options.Assemblies = assemblies; + }); + } } } diff --git a/Conductor/Client/Worker/WorkerDiscoveryOptions.cs b/Conductor/Client/Worker/WorkerDiscoveryOptions.cs new file mode 100644 index 00000000..245d33ab --- /dev/null +++ b/Conductor/Client/Worker/WorkerDiscoveryOptions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Conductor.Client.Worker +{ + + public sealed class WorkerDiscoveryOptions + { + public bool EnableAttributeDiscovery { get; set; } = true; + + /// + /// Empty means: preserve current behaviour and scan all loaded assemblies. + /// + public IReadOnlyCollection Assemblies { get; set; } = Array.Empty(); + } +} \ No newline at end of file diff --git a/Conductor/Client/Worker/WorkflowTaskCoordinator.cs b/Conductor/Client/Worker/WorkflowTaskCoordinator.cs index 323c8b0c..af7887d0 100644 --- a/Conductor/Client/Worker/WorkflowTaskCoordinator.cs +++ b/Conductor/Client/Worker/WorkflowTaskCoordinator.cs @@ -13,8 +13,10 @@ using Conductor.Client.Interfaces; using Conductor.Client.Telemetry; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -29,8 +31,14 @@ internal class WorkflowTaskCoordinator : IWorkflowTaskCoordinator private readonly HashSet _workers; private readonly IWorkflowTaskClient _client; private readonly MetricsCollector _metrics; + private readonly WorkerDiscoveryOptions _workerDiscoveryOptions; - public WorkflowTaskCoordinator(IWorkflowTaskClient client, ILogger logger, ILogger loggerWorkflowTaskExecutor, ILogger loggerWorkflowTaskMonitor, MetricsCollector metrics = null) + public WorkflowTaskCoordinator(IWorkflowTaskClient client, + ILogger logger, + ILogger loggerWorkflowTaskExecutor, + ILogger loggerWorkflowTaskMonitor, + MetricsCollector metrics = null, + IOptions workerDiscoveryOptions = null) { _logger = logger; _client = client; @@ -38,6 +46,8 @@ public WorkflowTaskCoordinator(IWorkflowTaskClient client, ILogger(); foreach (var worker in _workers) { @@ -72,7 +87,9 @@ public void RegisterWorker(IWorkflowTask worker) private void DiscoverWorkers() { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + var assemblies = _workerDiscoveryOptions.Assemblies?.Any() == true ? _workerDiscoveryOptions.Assemblies : AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var assembly in assemblies) { foreach (var type in assembly.GetTypes()) { @@ -80,6 +97,7 @@ private void DiscoverWorkers() { continue; } + foreach (var method in type.GetMethods()) { var workerTask = method.GetCustomAttribute(); diff --git a/Tests/Extensions/DependencyInjectionExtensionsTests.cs b/Tests/Extensions/DependencyInjectionExtensionsTests.cs index 5d2b99a3..d3bf1579 100644 --- a/Tests/Extensions/DependencyInjectionExtensionsTests.cs +++ b/Tests/Extensions/DependencyInjectionExtensionsTests.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright 2024 Conductor Authors. *

* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with @@ -12,7 +12,11 @@ */ using Conductor.Client.Extensions; using Conductor.Client.Telemetry; +using Conductor.Client.Worker; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Reflection; using Xunit; namespace Tests.Extensions @@ -88,5 +92,66 @@ public void AddConductorWorker_WithNullConfiguration_CreatesDefault() var provider = services.BuildServiceProvider(); Assert.NotNull(provider.GetService()); } + + [Fact] + public void ConfigureConductorWorkerDiscovery_WithoutConfiguration_UsesEmptyAssemblyCollection() + { + var services = new ServiceCollection(); + + services.AddConductorWorker(); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + Assert.Empty(options.Assemblies); + } + + [Fact] + public void ConfigureConductorWorkerDiscovery_WithAction_ConfiguresAssemblyCollection() + { + var services = new ServiceCollection(); + var assembly = typeof(DependencyInjectionExtensionsTests).Assembly; + + services.ConfigureConductorWorkerDiscovery(options => + options.Assemblies = new[] { assembly }); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + Assert.Contains(assembly, options.Assemblies); + } + + [Fact] + public void ConfigureConductorWorkerDiscovery_WithAttributeDiscoveryDisabled_ConfiguresOptions() + { + var services = new ServiceCollection(); + + services.ConfigureConductorWorkerDiscovery(options => + options.EnableAttributeDiscovery = false); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + Assert.False(options.EnableAttributeDiscovery); + } + + [Fact] + public void ConfigureConductorWorkerDiscovery_WithOptions_CopiesAssemblyCollection() + { + var services = new ServiceCollection(); + var assembly = typeof(DependencyInjectionExtensionsTests).Assembly; + var assemblies = new List { assembly }; + + services.ConfigureConductorWorkerDiscovery(new WorkerDiscoveryOptions + { + Assemblies = assemblies + }); + assemblies.Clear(); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + Assert.Contains(assembly, options.Assemblies); + } } } diff --git a/docs/workers.md b/docs/workers.md index f40451cc..cb9f4a93 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -94,6 +94,33 @@ settings. See [deployment-scaling.md](deployment-scaling.md) for sizing guidance `IServiceCollection`, so workers can take constructor dependencies and participate in the host's lifetime. This is the preferred shape for anything beyond a sample. +### Choose one registration path per worker + +The SDK supports two worker registration paths: + +- Register an `IWorkflowTask` explicitly in the service collection, for example with + `AddConductorWorkflowTask` or `ServiceDescriptor.Singleton()`. +- Discover methods annotated with `[WorkerTask]` when the worker host starts. + +Choose one path for each worker. Do not explicitly register a worker that is also +discovered through `[WorkerTask]`, or the host creates two polling workers for it. + +By default, attribute discovery scans all assemblies loaded in the process. To limit +the scan to the assembly containing your annotated workers, configure discovery while +building the service collection: + +```csharp +services.AddConductorWorker(configuration); +services.ConfigureConductorWorkerDiscovery(options => +{ + options.Assemblies = new[] { typeof(MyAnnotatedWorker).Assembly }; +}); +services.WithHostedService(); +``` + +This setting affects only `[WorkerTask]` discovery. It does not affect any +`IWorkflowTask` registered in the service collection. + ## Metrics The worker framework records polling, execution, update, and error metrics via