Skip to content

Commit 43d2be4

Browse files
CopilotYunchuWang
andcommitted
Add AddTasksAsServices extension method for DurableTaskWorkerBuilder
Co-authored-by: YunchuWang <[email protected]>
1 parent 6df155b commit 43d2be4

File tree

6 files changed

+226
-0
lines changed

6 files changed

+226
-0
lines changed

src/Abstractions/DurableTaskRegistry.Activities.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ TaskName ITaskActivity singleton
4141
public DurableTaskRegistry AddActivity(TaskName name, Type type)
4242
{
4343
Check.ConcreteType<ITaskActivity>(type);
44+
this.ActivityTypes.Add(type);
4445
return this.AddActivity(name, sp => (ITaskActivity)ActivatorUtilities.GetServiceOrCreateInstance(sp, type));
4546
}
4647

src/Abstractions/DurableTaskRegistry.Entities.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public DurableTaskRegistry AddEntity(TaskName name, Type type)
2222
{
2323
// TODO: Compile a constructor expression for performance.
2424
Check.ConcreteType<ITaskEntity>(type);
25+
this.EntityTypes.Add(type);
2526
return this.AddEntity(name, sp => (ITaskEntity)ActivatorUtilities.CreateInstance(sp, type));
2627
}
2728

src/Abstractions/DurableTaskRegistry.Orchestrators.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public DurableTaskRegistry AddOrchestrator(TaskName name, Type type)
4040
{
4141
// TODO: Compile a constructor expression for performance.
4242
Check.ConcreteType<ITaskOrchestrator>(type);
43+
this.OrchestratorTypes.Add(type);
4344
return this.AddOrchestrator(name, () => (ITaskOrchestrator)Activator.CreateInstance(type));
4445
}
4546

src/Abstractions/DurableTaskRegistry.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ public sealed partial class DurableTaskRegistry
3131
internal IDictionary<TaskName, Func<IServiceProvider, ITaskEntity>> Entities { get; }
3232
= new Dictionary<TaskName, Func<IServiceProvider, ITaskEntity>>();
3333

34+
/// <summary>
35+
/// Gets the types of registered activities.
36+
/// </summary>
37+
internal HashSet<Type> ActivityTypes { get; } = [];
38+
39+
/// <summary>
40+
/// Gets the types of registered orchestrators.
41+
/// </summary>
42+
internal HashSet<Type> OrchestratorTypes { get; } = [];
43+
44+
/// <summary>
45+
/// Gets the types of registered entities.
46+
/// </summary>
47+
internal HashSet<Type> EntityTypes { get; } = [];
48+
3449
/// <summary>
3550
/// Registers an activity factory.
3651
/// </summary>

src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.DurableTask.Worker.Hosting;
55
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.DependencyInjection.Extensions;
67
using Microsoft.Extensions.Options;
78
using static Microsoft.DurableTask.Worker.DurableTaskWorkerOptions;
89

@@ -137,4 +138,66 @@ public static IDurableTaskWorkerBuilder UseOrchestrationFilter(this IDurableTask
137138
builder.Services.AddSingleton(filter);
138139
return builder;
139140
}
141+
142+
/// <summary>
143+
/// Registers all task types (activities, orchestrators, and entities) from the specified registry configuration
144+
/// as services in the dependency injection container. This allows these types to participate in container
145+
/// validation and enables early detection of dependency resolution issues.
146+
/// </summary>
147+
/// <param name="builder">The builder to register tasks for.</param>
148+
/// <param name="configure">
149+
/// A callback that provides the registry. This callback will be invoked immediately to extract registered task types.
150+
/// The same callback will also be registered with <see cref="AddTasks"/> to ensure tasks are registered with the worker.
151+
/// </param>
152+
/// <returns>The original builder, for call chaining.</returns>
153+
/// <remarks>
154+
/// <para>
155+
/// Only task types registered via type-based registration methods (e.g., <see cref="DurableTaskRegistry.AddActivity(Type)"/>)
156+
/// will be registered in the container. Tasks registered via factory methods or singleton instances will not be included.
157+
/// </para>
158+
/// <para>
159+
/// Example usage:
160+
/// <code>
161+
/// builder.Services.AddDurableTaskWorker()
162+
/// .AddTasksAsServices(tasks =>
163+
/// {
164+
/// tasks.AddActivity&lt;MyActivity&gt;();
165+
/// tasks.AddOrchestrator&lt;MyOrchestrator&gt;();
166+
/// });
167+
/// </code>
168+
/// </para>
169+
/// </remarks>
170+
public static IDurableTaskWorkerBuilder AddTasksAsServices(
171+
this IDurableTaskWorkerBuilder builder, Action<DurableTaskRegistry> configure)
172+
{
173+
Check.NotNull(builder);
174+
Check.NotNull(configure);
175+
176+
// Create a temporary registry to extract the types
177+
DurableTaskRegistry tempRegistry = new();
178+
configure(tempRegistry);
179+
180+
// Register all activity types
181+
foreach (Type activityType in tempRegistry.ActivityTypes)
182+
{
183+
builder.Services.TryAddTransient(activityType);
184+
}
185+
186+
// Register all orchestrator types
187+
foreach (Type orchestratorType in tempRegistry.OrchestratorTypes)
188+
{
189+
builder.Services.TryAddTransient(orchestratorType);
190+
}
191+
192+
// Register all entity types
193+
foreach (Type entityType in tempRegistry.EntityTypes)
194+
{
195+
builder.Services.TryAddTransient(entityType);
196+
}
197+
198+
// Also register the tasks with the builder so they're available to the worker
199+
builder.AddTasks(configure);
200+
201+
return builder;
202+
}
140203
}

test/Worker/Core.Tests/DependencyInjection/DurableTaskWorkerBuilderExtensionsTests.cs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using Microsoft.DurableTask.Entities;
45
using Microsoft.DurableTask.Worker.Hosting;
56
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Extensions.Hosting;
@@ -62,6 +63,126 @@ public void Configure_ConfiguresOptions()
6263
actual.Should().BeSameAs(expected);
6364
}
6465

66+
[Fact]
67+
public void AddTasksAsServices_RegistersActivityTypes()
68+
{
69+
// Arrange
70+
ServiceCollection services = new();
71+
DefaultDurableTaskWorkerBuilder builder = new("test", services);
72+
73+
// Act
74+
builder.AddTasksAsServices(registry =>
75+
{
76+
registry.AddActivity<TestActivity>();
77+
});
78+
79+
// Assert
80+
IServiceProvider provider = services.BuildServiceProvider();
81+
provider.GetService<TestActivity>().Should().NotBeNull();
82+
}
83+
84+
[Fact]
85+
public void AddTasksAsServices_RegistersOrchestratorTypes()
86+
{
87+
// Arrange
88+
ServiceCollection services = new();
89+
DefaultDurableTaskWorkerBuilder builder = new("test", services);
90+
91+
// Act
92+
builder.AddTasksAsServices(registry =>
93+
{
94+
registry.AddOrchestrator<TestOrchestrator>();
95+
});
96+
97+
// Assert
98+
IServiceProvider provider = services.BuildServiceProvider();
99+
provider.GetService<TestOrchestrator>().Should().NotBeNull();
100+
}
101+
102+
[Fact]
103+
public void AddTasksAsServices_RegistersEntityTypes()
104+
{
105+
// Arrange
106+
ServiceCollection services = new();
107+
DefaultDurableTaskWorkerBuilder builder = new("test", services);
108+
109+
// Act
110+
builder.AddTasksAsServices(registry =>
111+
{
112+
registry.AddEntity<TestEntity>();
113+
});
114+
115+
// Assert
116+
IServiceProvider provider = services.BuildServiceProvider();
117+
provider.GetService<TestEntity>().Should().NotBeNull();
118+
}
119+
120+
[Fact]
121+
public void AddTasksAsServices_RegistersMultipleTaskTypes()
122+
{
123+
// Arrange
124+
ServiceCollection services = new();
125+
DefaultDurableTaskWorkerBuilder builder = new("test", services);
126+
127+
// Act
128+
builder.AddTasksAsServices(registry =>
129+
{
130+
registry.AddActivity<TestActivity>();
131+
registry.AddOrchestrator<TestOrchestrator>();
132+
registry.AddEntity<TestEntity>();
133+
});
134+
135+
// Assert
136+
IServiceProvider provider = services.BuildServiceProvider();
137+
provider.GetService<TestActivity>().Should().NotBeNull();
138+
provider.GetService<TestOrchestrator>().Should().NotBeNull();
139+
provider.GetService<TestEntity>().Should().NotBeNull();
140+
}
141+
142+
[Fact]
143+
public void AddTasksAsServices_DoesNotRegisterFunctionBasedTasks()
144+
{
145+
// Arrange
146+
ServiceCollection services = new();
147+
DefaultDurableTaskWorkerBuilder builder = new("test", services);
148+
149+
// Act
150+
builder.AddTasksAsServices(registry =>
151+
{
152+
registry.AddActivityFunc("testFunc", (TaskActivityContext ctx) => Task.CompletedTask);
153+
});
154+
155+
// Assert - No exception should be thrown and no types should be registered
156+
IServiceProvider provider = services.BuildServiceProvider();
157+
// There should be no issue building the service provider
158+
provider.Should().NotBeNull();
159+
}
160+
161+
[Fact]
162+
public void AddTasksAsServices_AlsoRegistersTasksWithWorker()
163+
{
164+
// Arrange
165+
ServiceCollection services = new();
166+
DefaultDurableTaskWorkerBuilder builder = new("test", services);
167+
168+
// Act
169+
builder.AddTasksAsServices(registry =>
170+
{
171+
registry.AddActivity<TestActivity>();
172+
});
173+
174+
// Assert - Tasks should be registered both as services and with the worker
175+
IServiceProvider provider = services.BuildServiceProvider();
176+
provider.GetService<TestActivity>().Should().NotBeNull();
177+
178+
// Also verify the task is registered with the worker by checking the factory
179+
IDurableTaskFactory factory = provider.GetRequiredService<IOptionsMonitor<DurableTaskRegistry>>()
180+
.Get("test")
181+
.BuildFactory();
182+
factory.TryCreateActivity(nameof(TestActivity), provider, out ITaskActivity? activity).Should().BeTrue();
183+
activity.Should().NotBeNull();
184+
}
185+
65186
class BadBuildTarget : BackgroundService
66187
{
67188
protected override Task ExecuteAsync(CancellationToken stoppingToken)
@@ -90,4 +211,28 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)
90211
throw new NotImplementedException();
91212
}
92213
}
214+
215+
sealed class TestActivity : TaskActivity<object, object>
216+
{
217+
public override Task<object> RunAsync(TaskActivityContext context, object input)
218+
{
219+
return Task.FromResult<object>(input);
220+
}
221+
}
222+
223+
sealed class TestOrchestrator : TaskOrchestrator<object, object>
224+
{
225+
public override Task<object> RunAsync(TaskOrchestrationContext context, object input)
226+
{
227+
return Task.FromResult<object>(input);
228+
}
229+
}
230+
231+
sealed class TestEntity : TaskEntity<object>
232+
{
233+
public void Operation(object input)
234+
{
235+
// Simple operation for testing
236+
}
237+
}
93238
}

0 commit comments

Comments
 (0)