Skip to content
1 change: 1 addition & 0 deletions src/Abstractions/DurableTaskRegistry.Activities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ TaskName ITaskActivity singleton
public DurableTaskRegistry AddActivity(TaskName name, Type type)
{
Check.ConcreteType<ITaskActivity>(type);
this.ActivityTypes.Add(type);
return this.AddActivity(name, sp => (ITaskActivity)ActivatorUtilities.GetServiceOrCreateInstance(sp, type));
}

Expand Down
1 change: 1 addition & 0 deletions src/Abstractions/DurableTaskRegistry.Entities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public DurableTaskRegistry AddEntity(TaskName name, Type type)
{
// TODO: Compile a constructor expression for performance.
Check.ConcreteType<ITaskEntity>(type);
this.EntityTypes.Add(type);
return this.AddEntity(name, sp => (ITaskEntity)ActivatorUtilities.CreateInstance(sp, type));
}

Expand Down
1 change: 1 addition & 0 deletions src/Abstractions/DurableTaskRegistry.Orchestrators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public DurableTaskRegistry AddOrchestrator(TaskName name, Type type)
{
// TODO: Compile a constructor expression for performance.
Check.ConcreteType<ITaskOrchestrator>(type);
this.OrchestratorTypes.Add(type);
return this.AddOrchestrator(name, () => (ITaskOrchestrator)Activator.CreateInstance(type));
}

Expand Down
15 changes: 15 additions & 0 deletions src/Abstractions/DurableTaskRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ public sealed partial class DurableTaskRegistry
internal IDictionary<TaskName, Func<IServiceProvider, ITaskEntity>> Entities { get; }
= new Dictionary<TaskName, Func<IServiceProvider, ITaskEntity>>();

/// <summary>
/// Gets the types of registered activities.
/// </summary>
internal HashSet<Type> ActivityTypes { get; } = new();

/// <summary>
/// Gets the types of registered orchestrators.
/// </summary>
internal HashSet<Type> OrchestratorTypes { get; } = new();

/// <summary>
/// Gets the types of registered entities.
/// </summary>
internal HashSet<Type> EntityTypes { get; } = new();

/// <summary>
/// Registers an activity factory.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -137,4 +138,66 @@ public static IDurableTaskWorkerBuilder UseOrchestrationFilter(this IDurableTask
builder.Services.AddSingleton(filter);
return builder;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="builder">The builder to register tasks for.</param>
/// <param name="configure">
/// 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 <see cref="AddTasks"/> to ensure tasks are registered with the worker.
/// </param>
/// <returns>The original builder, for call chaining.</returns>
/// <remarks>
/// <para>
/// Only task types registered via type-based registration methods (e.g., <see cref="DurableTaskRegistry.AddActivity(Type)"/>)
/// will be registered in the container. Tasks registered via factory methods or singleton instances will not be included.
/// </para>
/// <para>
/// Example usage:
/// <code>
/// builder.Services.AddDurableTaskWorker()
/// .AddTasksAsServices(tasks =>
/// {
/// tasks.AddActivity&lt;MyActivity&gt;();
/// tasks.AddOrchestrator&lt;MyOrchestrator&gt;();
/// });
/// </code>
/// </para>
/// </remarks>
public static IDurableTaskWorkerBuilder AddTasksAsServices(
this IDurableTaskWorkerBuilder builder, Action<DurableTaskRegistry> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<TestActivity>();
});

// Assert
IServiceProvider provider = services.BuildServiceProvider();
provider.GetService<TestActivity>().Should().NotBeNull();
}

[Fact]
public void AddTasksAsServices_RegistersOrchestratorTypes()
{
// Arrange
ServiceCollection services = new();
DefaultDurableTaskWorkerBuilder builder = new("test", services);

// Act
builder.AddTasksAsServices(registry =>
{
registry.AddOrchestrator<TestOrchestrator>();
});

// Assert
IServiceProvider provider = services.BuildServiceProvider();
provider.GetService<TestOrchestrator>().Should().NotBeNull();
}

[Fact]
public void AddTasksAsServices_RegistersEntityTypes()
{
// Arrange
ServiceCollection services = new();
DefaultDurableTaskWorkerBuilder builder = new("test", services);

// Act
builder.AddTasksAsServices(registry =>
{
registry.AddEntity<TestEntity>();
});

// Assert
IServiceProvider provider = services.BuildServiceProvider();
provider.GetService<TestEntity>().Should().NotBeNull();
}

[Fact]
public void AddTasksAsServices_RegistersMultipleTaskTypes()
{
// Arrange
ServiceCollection services = new();
DefaultDurableTaskWorkerBuilder builder = new("test", services);

// Act
builder.AddTasksAsServices(registry =>
{
registry.AddActivity<TestActivity>();
registry.AddOrchestrator<TestOrchestrator>();
registry.AddEntity<TestEntity>();
});

// Assert
IServiceProvider provider = services.BuildServiceProvider();
provider.GetService<TestActivity>().Should().NotBeNull();
provider.GetService<TestOrchestrator>().Should().NotBeNull();
provider.GetService<TestEntity>().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<TestActivity>().Should().BeNull();
}

[Fact]
public void AddTasksAsServices_AlsoRegistersTasksWithWorker()
{
// Arrange
ServiceCollection services = new();
DefaultDurableTaskWorkerBuilder builder = new("test", services);

// Act
builder.AddTasksAsServices(registry =>
{
registry.AddActivity<TestActivity>();
});

// Assert - Tasks should be registered both as services and with the worker
IServiceProvider provider = services.BuildServiceProvider();
provider.GetService<TestActivity>().Should().NotBeNull();

// Also verify the task is registered with the worker by checking the factory
IDurableTaskFactory factory = provider.GetRequiredService<IOptionsMonitor<DurableTaskRegistry>>()
.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)
Expand Down Expand Up @@ -90,4 +232,28 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)
throw new NotImplementedException();
}
}

sealed class TestActivity : TaskActivity<object, object>
{
public override Task<object> RunAsync(TaskActivityContext context, object input)
{
return Task.FromResult<object>(input);
}
}

sealed class TestOrchestrator : TaskOrchestrator<object, object>
{
public override Task<object> RunAsync(TaskOrchestrationContext context, object input)
{
return Task.FromResult<object>(input);
}
}

sealed class TestEntity : TaskEntity<object>
{
public void Operation(object input)
{
// Simple operation for testing
}
}
}
Loading