From 8b43e7f8d4c96641b11c26b761775e5fcce8c8da Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Wed, 14 Jan 2026 17:11:39 -0800 Subject: [PATCH 01/10] Introduce WorkItemFilters into worker flow This change adds WorkItemFilters into the grpc worker. This includes builder methods to specify them and the connection into the GetWorkItems flow inside the worker processor. Signed-off-by: Hal Spang --- src/Grpc/orchestrator_service.proto | 21 ++ .../DurableTaskWorkerBuilderExtensions.cs | 28 +++ .../Core/DurableTaskWorkerWorkItemFilters.cs | 126 ++++++++++++ .../Grpc/GrpcDurableTaskWorker.Processor.cs | 2 + src/Worker/Grpc/GrpcDurableTaskWorker.cs | 6 +- ...rableTaskWorkerWorkItemFiltersExtension.cs | 52 +++++ .../UseWorkItemFiltersTests.cs | 189 ++++++++++++++++++ 7 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs create mode 100644 src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs create mode 100644 test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 8ef46a4a7..0c34d986d 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -822,6 +822,7 @@ message GetWorkItemsRequest { int32 maxConcurrentEntityWorkItems = 3; repeated WorkerCapability capabilities = 10; + WorkItemFilters workItemFilters = 11; } enum WorkerCapability { @@ -844,6 +845,26 @@ enum WorkerCapability { WORKER_CAPABILITY_LARGE_PAYLOADS = 3; } +message WorkItemFilters { + repeated OrchestrationFilter orchestrations = 1; + repeated ActivityFilter activities = 2; + repeated EntityFilter entities = 3; +} + +message OrchestrationFilter { + string name = 1; + repeated string versions = 2; +} + +message ActivityFilter { + string name = 1; + repeated string versions = 2; +} + +message EntityFilter { + string name = 1; +} + message WorkItem { oneof request { OrchestratorRequest orchestratorRequest = 1; diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs index 3f349b710..0a83e9d52 100644 --- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs +++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs @@ -137,4 +137,32 @@ public static IDurableTaskWorkerBuilder UseOrchestrationFilter(this IDurableTask builder.Services.AddSingleton(filter); return builder; } + + /// + /// Adds to the specified . + /// + /// The builder to set the builder target for. + /// The instance of a to use. + /// The same instance, allowing for method chaining. + /// If this is called without specified filters, the filters will be constructed from the registered orchestrations, activities, and entities. + public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWorkerBuilder builder, DurableTaskWorkerWorkItemFilters? workItemFilters = null) + { + Check.NotNull(builder); + if (workItemFilters != null) + { + builder.Services.AddSingleton(workItemFilters); + } + else + { + // Auto-generate the filters from registered orchestrations, activities, and entitites. + builder.Services.AddSingleton(provider => + { + DurableTaskRegistry registry = provider.GetRequiredService>().Get(builder.Name); + DurableTaskWorkerOptions? options = provider.GetOptions(builder.Name); + return new DurableTaskWorkerWorkItemFilters(registry, options); + }); + } + + return builder; + } } diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs new file mode 100644 index 000000000..25fc14254 --- /dev/null +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.Worker; + +/// +/// A class that represents work item filters for a Durable Task Worker. These filters are passed to the backend +/// and only work items matching the filters will be processed by the worker. If no filters are provided, +/// the worker will process all work items. +/// +public class DurableTaskWorkerWorkItemFilters +{ + /// + /// Initializes a new instance of the class. + /// + public DurableTaskWorkerWorkItemFilters() + { + this.Orchestrations = []; + this.Activities = []; + this.Entities = []; + } + + /// + /// Initializes a new instance of the class. + /// + /// to construct the filter from. + /// that optionally provides versioning information. + internal DurableTaskWorkerWorkItemFilters(DurableTaskRegistry registry, DurableTaskWorkerOptions? workerOptions) + { + List orchestrationActions = new(); + foreach (var orchestration in registry.Orchestrators) + { + orchestrationActions.Add(new OrchestrationFilter + { + Name = orchestration.Key, + + // TODO: Support multiple orchestration versions, for now, utilize the Worker's version. + Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.DefaultVersion] : [], + }); + } + + this.Orchestrations = orchestrationActions; + List activityActions = new(); + foreach (var activity in registry.Activities) + { + activityActions.Add(new ActivityFilter + { + Name = activity.Key, + + // TODO: Support multiple activity versions, for now, utilize the Worker's version. + Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.DefaultVersion] : [], + }); + } + + this.Activities = activityActions; + List entityActions = new(); + foreach (var entity in registry.Entities) + { + entityActions.Add(new EntityFilter + { + // Entity names are normalized to lowercase in the backend. + Name = entity.Key.ToString().ToLowerInvariant(), + }); + } + + this.Entities = entityActions; + } + + /// + /// Gets or initializes the orchestration filters. + /// + public IReadOnlyList Orchestrations { get; init; } + + /// + /// Gets or initializes the activity filters. + /// + public IReadOnlyList Activities { get; init; } + + /// + /// Gets or initializes the entity filters. + /// + public IReadOnlyList Entities { get; init; } + + /// + /// Struct specifying an orchestration filter. + /// + public struct OrchestrationFilter + { + /// + /// Gets or initializes the name of the orchestration to filter. + /// + public string Name { get; init; } + + /// + /// Gets or initializes the versions of the orchestration to filter. + /// + public List Versions { get; init; } + } + + /// + /// Struct specifying an activity filter. + /// + public struct ActivityFilter + { + /// + /// Gets or initializes the name of the activity to filter. + /// + public string Name { get; init; } + + /// + /// Gets or initializes the versions of the activity to filter. + /// + public List Versions { get; init; } + } + + /// + /// Struct specifying an entity filter. + /// + public struct EntityFilter + { + /// + /// Gets or initializes the name of the entity to filter. + /// + public string Name { get; init; } + } +} diff --git a/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs b/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs index 0049bb0af..732e838a2 100644 --- a/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs +++ b/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs @@ -12,6 +12,7 @@ using Microsoft.DurableTask.Abstractions; using Microsoft.DurableTask.Entities; using Microsoft.DurableTask.Tracing; +using Microsoft.DurableTask.Worker.Grpc.Internal; using Microsoft.DurableTask.Worker.Shims; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -257,6 +258,7 @@ async ValueTask BuildRuntimeStateAsync( MaxConcurrentEntityWorkItems = workerOptions.Concurrency.MaximumConcurrentEntityWorkItems, Capabilities = { this.worker.grpcOptions.Capabilities }, + WorkItemFilters = this.worker?.workItemFilters?.ToGrpcWorkItemFilters(), }, cancellationToken: cancellation); } diff --git a/src/Worker/Grpc/GrpcDurableTaskWorker.cs b/src/Worker/Grpc/GrpcDurableTaskWorker.cs index 93875961b..e2d875ae6 100644 --- a/src/Worker/Grpc/GrpcDurableTaskWorker.cs +++ b/src/Worker/Grpc/GrpcDurableTaskWorker.cs @@ -18,6 +18,7 @@ sealed partial class GrpcDurableTaskWorker : DurableTaskWorker readonly ILoggerFactory loggerFactory; readonly ILogger logger; readonly IOrchestrationFilter? orchestrationFilter; + readonly DurableTaskWorkerWorkItemFilters? workItemFilters; /// /// Initializes a new instance of the class. @@ -30,6 +31,7 @@ sealed partial class GrpcDurableTaskWorker : DurableTaskWorker /// The logger. /// The optional used to filter orchestration execution. /// The custom exception properties provider that help build failure details. + /// The optional used to filter work items in the backend. public GrpcDurableTaskWorker( string name, IDurableTaskFactory factory, @@ -38,7 +40,8 @@ public GrpcDurableTaskWorker( IServiceProvider services, ILoggerFactory loggerFactory, IOrchestrationFilter? orchestrationFilter = null, - IExceptionPropertiesProvider? exceptionPropertiesProvider = null) + IExceptionPropertiesProvider? exceptionPropertiesProvider = null, + DurableTaskWorkerWorkItemFilters? workItemFilters = null) : base(name, factory) { this.grpcOptions = Check.NotNull(grpcOptions).Get(name); @@ -48,6 +51,7 @@ public GrpcDurableTaskWorker( this.logger = CreateLogger(loggerFactory, this.workerOptions); this.orchestrationFilter = orchestrationFilter; this.ExceptionPropertiesProvider = exceptionPropertiesProvider; + this.workItemFilters = workItemFilters; } /// diff --git a/src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs b/src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs new file mode 100644 index 000000000..75bc47ac6 --- /dev/null +++ b/src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using P = Microsoft.DurableTask.Protobuf; + +namespace Microsoft.DurableTask.Worker.Grpc.Internal; + +/// +/// Extension for to convert to gRPC types. +/// +public static class DurableTaskWorkerWorkItemFiltersExtensions +{ + /// + /// Converts a to a gRPC . + /// + /// The to convert. + /// A gRPC . + public static P.WorkItemFilters ToGrpcWorkItemFilters(this DurableTaskWorkerWorkItemFilters workItemFilter) + { + var grpcWorkItemFilters = new P.WorkItemFilters(); + foreach (var orchestrationFilter in workItemFilter.Orchestrations) + { + var grpcOrchestrationFilter = new P.OrchestrationFilter + { + Name = orchestrationFilter.Name, + }; + grpcOrchestrationFilter.Versions.AddRange(orchestrationFilter.Versions); + grpcWorkItemFilters.Orchestrations.Add(grpcOrchestrationFilter); + } + + foreach (var activityFilter in workItemFilter.Activities) + { + var grpcActivityAction = new P.ActivityFilter + { + Name = activityFilter.Name, + }; + grpcActivityAction.Versions.AddRange(activityFilter.Versions); + grpcWorkItemFilters.Activities.Add(grpcActivityAction); + } + + foreach (var entityFilter in workItemFilter.Entities) + { + var grpcEntityAction = new P.EntityFilter + { + Name = entityFilter.Name, + }; + grpcWorkItemFilters.Entities.Add(grpcEntityAction); + } + + return grpcWorkItemFilters; + } +} diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs new file mode 100644 index 000000000..59c68a1f1 --- /dev/null +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Entities; +using Microsoft.DurableTask.Worker.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.DurableTask.Worker.Tests; + +public class UseWorkItemFiltersTests +{ + [Fact] + public void UseWorkItemFilters_NullBuilder_Throws() + { + // Arrange + IDurableTaskWorkerBuilder builder = null!; + + // Act + Action act = () => builder.UseWorkItemFilters(); + + // Assert + act.Should().ThrowExactly().WithParameterName("builder"); + } + + [Fact] + public void UseWorkItemFilters_WithExplicitFilters_RegistersFilters() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + DurableTaskRegistry registry = new(); + DurableTaskWorkerWorkItemFilters filters = new(registry, null); + + // Act + builder.UseWorkItemFilters(filters); + ServiceProvider provider = services.BuildServiceProvider(); + DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + + // Assert + actual.Should().BeSameAs(filters); + } + + [Fact] + public void UseWorkItemFilters_WithoutFilters_AutoGeneratesFromRegistry() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + + // Act + builder.UseWorkItemFilters(); + ServiceProvider provider = services.BuildServiceProvider(); + DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + + // Assert + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + } + + [Fact] + public void UseWorkItemFilters_WithVersioning_IncludesVersionInFilters() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.Configure(options => + { + options.Versioning = new DurableTaskWorkerOptions.VersioningOptions + { + DefaultVersion = "1.0" + }; + }); + + // Act + builder.UseWorkItemFilters(); + ServiceProvider provider = services.BuildServiceProvider(); + DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + + // Assert + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Contains("1.0")); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity) && a.Versions.Contains("1.0")); + } + + [Fact] + public void UseWorkItemFilters_WithEntity_IncludesEntityInFilters() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + builder.AddTasks(registry => + { + registry.AddEntity(); + }); + + // Act + builder.UseWorkItemFilters(); + ServiceProvider provider = services.BuildServiceProvider(); + DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + + // Assert + actual.Entities.Should().ContainSingle(e => e.Name == nameof(TestEntity).ToLowerInvariant()); + } + + [Fact] + public void UseWorkItemFilters_ReturnsBuilder_ForChaining() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + + // Act + IDurableTaskWorkerBuilder result = builder.UseWorkItemFilters(); + + // Assert + result.Should().BeSameAs(builder); + } + + [Fact] + public void UseWorkItemFilters_EmptyRegistry_CreatesEmptyFilters() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder = new("test", services); + builder.AddTasks(_ => { }); + + // Act + builder.UseWorkItemFilters(); + ServiceProvider provider = services.BuildServiceProvider(); + DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + + // Assert + actual.Orchestrations.Should().BeEmpty(); + actual.Activities.Should().BeEmpty(); + actual.Entities.Should().BeEmpty(); + } + + [Fact] + public void UseWorkItemFilters_NamedBuilders_HaveUniqueFilters() + { + // Arrange + ServiceCollection services = new(); + DefaultDurableTaskWorkerBuilder builder1 = new("worker1", services); + builder1.AddTasks(registry => registry.AddOrchestrator()); + builder1.UseWorkItemFilters(); + + DefaultDurableTaskWorkerBuilder builder2 = new("worker2", services); + builder2.AddTasks(registry => registry.AddActivity()); + builder2.UseWorkItemFilters(); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IEnumerable allFilters = provider.GetServices(); + + // Assert + allFilters.Should().HaveCount(2); + allFilters.Should().Contain(f => f.Orchestrations.Any(o => o.Name == nameof(TestOrchestrator)) && !f.Activities.Any()); + allFilters.Should().Contain(f => f.Activities.Any(a => a.Name == nameof(TestActivity)) && !f.Orchestrations.Any()); + } + + class TestOrchestrator : TaskOrchestrator + { + public override Task RunAsync(TaskOrchestrationContext context, object input) + { + throw new NotImplementedException(); + } + } + + class TestActivity : TaskActivity + { + public override Task RunAsync(TaskActivityContext context, object input) + { + throw new NotImplementedException(); + } + } + + class TestEntity : TaskEntity + { + } +} From 540aaf39d07d55871f380ca95c5cbe3f2ce07b65 Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Thu, 15 Jan 2026 13:25:41 -0800 Subject: [PATCH 02/10] Add tests, update filter construction Signed-off-by: Hal Spang --- .../DurableTaskWorkerBuilderExtensions.cs | 4 +- .../Core/DurableTaskWorkerWorkItemFilters.cs | 79 ++---- .../Grpc/GrpcDurableTaskWorker.Processor.cs | 2 +- ...rableTaskWorkerWorkItemFiltersExtension.cs | 13 +- .../UseWorkItemFiltersTests.cs | 3 +- ...TaskWorkerWorkItemFiltersExtensionTests.cs | 263 ++++++++++++++++++ 6 files changed, 300 insertions(+), 64 deletions(-) create mode 100644 test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs index 0a83e9d52..66a8521bd 100644 --- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs +++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs @@ -154,12 +154,12 @@ public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWork } else { - // Auto-generate the filters from registered orchestrations, activities, and entitites. + // Auto-generate the filters from registered orchestrations, activities, and entities. builder.Services.AddSingleton(provider => { DurableTaskRegistry registry = provider.GetRequiredService>().Get(builder.Name); DurableTaskWorkerOptions? options = provider.GetOptions(builder.Name); - return new DurableTaskWorkerWorkItemFilters(registry, options); + return DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, options); }); } diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index 25fc14254..57270f0e7 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -11,76 +11,49 @@ namespace Microsoft.DurableTask.Worker; public class DurableTaskWorkerWorkItemFilters { /// - /// Initializes a new instance of the class. + /// Gets or initializes the orchestration filters. /// - public DurableTaskWorkerWorkItemFilters() - { - this.Orchestrations = []; - this.Activities = []; - this.Entities = []; - } + public IReadOnlyList Orchestrations { get; init; } = []; + + /// + /// Gets or initializes the activity filters. + /// + public IReadOnlyList Activities { get; init; } = []; /// - /// Initializes a new instance of the class. + /// Gets or initializes the entity filters. + /// + public IReadOnlyList Entities { get; init; } = []; + + /// + /// Creates a new instance of the class. /// /// to construct the filter from. /// that optionally provides versioning information. - internal DurableTaskWorkerWorkItemFilters(DurableTaskRegistry registry, DurableTaskWorkerOptions? workerOptions) + /// A new instance of constructed from the provided registry. + internal static DurableTaskWorkerWorkItemFilters FromDurableTaskRegistry(DurableTaskRegistry registry, DurableTaskWorkerOptions? workerOptions) { - List orchestrationActions = new(); - foreach (var orchestration in registry.Orchestrators) + // TODO: Support multiple versions per orchestration/activity. For now, grab the worker version from the options. + return new DurableTaskWorkerWorkItemFilters { - orchestrationActions.Add(new OrchestrationFilter + Orchestrations = registry.Orchestrators.Select(orchestration => new OrchestrationFilter { Name = orchestration.Key, - - // TODO: Support multiple orchestration versions, for now, utilize the Worker's version. Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.DefaultVersion] : [], - }); - } - - this.Orchestrations = orchestrationActions; - List activityActions = new(); - foreach (var activity in registry.Activities) - { - activityActions.Add(new ActivityFilter + }).ToList(), + Activities = registry.Activities.Select(activity => new ActivityFilter { Name = activity.Key, - - // TODO: Support multiple activity versions, for now, utilize the Worker's version. Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.DefaultVersion] : [], - }); - } - - this.Activities = activityActions; - List entityActions = new(); - foreach (var entity in registry.Entities) - { - entityActions.Add(new EntityFilter + }).ToList(), + Entities = registry.Entities.Select(entity => new EntityFilter { // Entity names are normalized to lowercase in the backend. Name = entity.Key.ToString().ToLowerInvariant(), - }); - } - - this.Entities = entityActions; + }).ToList(), + }; } - /// - /// Gets or initializes the orchestration filters. - /// - public IReadOnlyList Orchestrations { get; init; } - - /// - /// Gets or initializes the activity filters. - /// - public IReadOnlyList Activities { get; init; } - - /// - /// Gets or initializes the entity filters. - /// - public IReadOnlyList Entities { get; init; } - /// /// Struct specifying an orchestration filter. /// @@ -94,7 +67,7 @@ public struct OrchestrationFilter /// /// Gets or initializes the versions of the orchestration to filter. /// - public List Versions { get; init; } + public IReadOnlyList Versions { get; init; } } /// @@ -110,7 +83,7 @@ public struct ActivityFilter /// /// Gets or initializes the versions of the activity to filter. /// - public List Versions { get; init; } + public IReadOnlyList Versions { get; init; } } /// diff --git a/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs b/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs index 732e838a2..a3aa3dab0 100644 --- a/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs +++ b/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs @@ -258,7 +258,7 @@ async ValueTask BuildRuntimeStateAsync( MaxConcurrentEntityWorkItems = workerOptions.Concurrency.MaximumConcurrentEntityWorkItems, Capabilities = { this.worker.grpcOptions.Capabilities }, - WorkItemFilters = this.worker?.workItemFilters?.ToGrpcWorkItemFilters(), + WorkItemFilters = this.worker.workItemFilters?.ToGrpcWorkItemFilters(), }, cancellationToken: cancellation); } diff --git a/src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs b/src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs index 75bc47ac6..176d376c1 100644 --- a/src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs +++ b/src/Worker/Grpc/Internal/DurableTaskWorkerWorkItemFiltersExtension.cs @@ -8,7 +8,7 @@ namespace Microsoft.DurableTask.Worker.Grpc.Internal; /// /// Extension for to convert to gRPC types. /// -public static class DurableTaskWorkerWorkItemFiltersExtensions +public static class DurableTaskWorkerWorkItemFiltersExtension { /// /// Converts a to a gRPC . @@ -17,6 +17,7 @@ public static class DurableTaskWorkerWorkItemFiltersExtensions /// A gRPC . public static P.WorkItemFilters ToGrpcWorkItemFilters(this DurableTaskWorkerWorkItemFilters workItemFilter) { + Check.NotNull(workItemFilter); var grpcWorkItemFilters = new P.WorkItemFilters(); foreach (var orchestrationFilter in workItemFilter.Orchestrations) { @@ -30,21 +31,21 @@ public static P.WorkItemFilters ToGrpcWorkItemFilters(this DurableTaskWorkerWork foreach (var activityFilter in workItemFilter.Activities) { - var grpcActivityAction = new P.ActivityFilter + var grpcActivityFilter = new P.ActivityFilter { Name = activityFilter.Name, }; - grpcActivityAction.Versions.AddRange(activityFilter.Versions); - grpcWorkItemFilters.Activities.Add(grpcActivityAction); + grpcActivityFilter.Versions.AddRange(activityFilter.Versions); + grpcWorkItemFilters.Activities.Add(grpcActivityFilter); } foreach (var entityFilter in workItemFilter.Entities) { - var grpcEntityAction = new P.EntityFilter + var grpcEntityFilter = new P.EntityFilter { Name = entityFilter.Name, }; - grpcWorkItemFilters.Entities.Add(grpcEntityAction); + grpcWorkItemFilters.Entities.Add(grpcEntityFilter); } return grpcWorkItemFilters; diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs index 59c68a1f1..7404e8f2d 100644 --- a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Microsoft.DurableTask.Entities; -using Microsoft.DurableTask.Worker.Hosting; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.DurableTask.Worker.Tests; @@ -29,7 +28,7 @@ public void UseWorkItemFilters_WithExplicitFilters_RegistersFilters() ServiceCollection services = new(); DefaultDurableTaskWorkerBuilder builder = new("test", services); DurableTaskRegistry registry = new(); - DurableTaskWorkerWorkItemFilters filters = new(registry, null); + DurableTaskWorkerWorkItemFilters filters = DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, null); // Act builder.UseWorkItemFilters(filters); diff --git a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs new file mode 100644 index 000000000..fa669df7e --- /dev/null +++ b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Worker.Grpc.Internal; +using P = Microsoft.DurableTask.Protobuf; + +namespace Microsoft.DurableTask.Worker.Grpc.Tests; + +public class DurableTaskWorkerWorkItemFiltersExtensionTests +{ + [Fact] + public void ToGrpcWorkItemFilters_EmptyFilters_ReturnsEmptyGrpcFilters() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = [], + Activities = [], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Orchestrations.Should().BeEmpty(); + result.Activities.Should().BeEmpty(); + result.Entities.Should().BeEmpty(); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithOrchestration_ConvertsName() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = + [ + new DurableTaskWorkerWorkItemFilters.OrchestrationFilter + { + Name = "TestOrchestrator", + Versions = [], + }, + ], + Activities = [], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Orchestrations.Should().ContainSingle(); + result.Orchestrations[0].Name.Should().Be("TestOrchestrator"); + result.Orchestrations[0].Versions.Should().BeEmpty(); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithOrchestrationVersions_ConvertsVersions() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = + [ + new DurableTaskWorkerWorkItemFilters.OrchestrationFilter + { + Name = "TestOrchestrator", + Versions = ["1.0", "2.0"], + }, + ], + Activities = [], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Orchestrations.Should().ContainSingle(); + result.Orchestrations[0].Versions.Should().BeEquivalentTo(["1.0", "2.0"]); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithActivity_ConvertsName() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = [], + Activities = + [ + new DurableTaskWorkerWorkItemFilters.ActivityFilter + { + Name = "TestActivity", + Versions = [], + }, + ], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Activities.Should().ContainSingle(); + result.Activities[0].Name.Should().Be("TestActivity"); + result.Activities[0].Versions.Should().BeEmpty(); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithActivityVersions_ConvertsVersions() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = [], + Activities = + [ + new DurableTaskWorkerWorkItemFilters.ActivityFilter + { + Name = "TestActivity", + Versions = ["v1", "v2", "v3"], + }, + ], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Activities.Should().ContainSingle(); + result.Activities[0].Versions.Should().BeEquivalentTo(["v1", "v2", "v3"]); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithEntity_ConvertsName() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = [], + Activities = [], + Entities = + [ + new DurableTaskWorkerWorkItemFilters.EntityFilter + { + Name = "testentity", + }, + ], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Entities.Should().ContainSingle(); + result.Entities[0].Name.Should().Be("testentity"); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithMultipleOrchestrations_ConvertsAll() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = + [ + new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "Orch1", Versions = ["1.0"] }, + new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "Orch2", Versions = ["2.0"] }, + new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "Orch3", Versions = [] }, + ], + Activities = [], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Orchestrations.Should().HaveCount(3); + result.Orchestrations.Select(o => o.Name).Should().BeEquivalentTo(["Orch1", "Orch2", "Orch3"]); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithMultipleActivities_ConvertsAll() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = [], + Activities = + [ + new DurableTaskWorkerWorkItemFilters.ActivityFilter { Name = "Activity1", Versions = [] }, + new DurableTaskWorkerWorkItemFilters.ActivityFilter { Name = "Activity2", Versions = [] }, + ], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Activities.Should().HaveCount(2); + result.Activities.Select(a => a.Name).Should().BeEquivalentTo(["Activity1", "Activity2"]); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithMultipleEntities_ConvertsAll() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = [], + Activities = [], + Entities = + [ + new DurableTaskWorkerWorkItemFilters.EntityFilter { Name = "entity1" }, + new DurableTaskWorkerWorkItemFilters.EntityFilter { Name = "entity2" }, + new DurableTaskWorkerWorkItemFilters.EntityFilter { Name = "entity3" }, + ], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Entities.Should().HaveCount(3); + result.Entities.Select(e => e.Name).Should().BeEquivalentTo(["entity1", "entity2", "entity3"]); + } + + [Fact] + public void ToGrpcWorkItemFilters_WithMixedFilters_ConvertsAll() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = + [ + new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "MyOrchestrator", Versions = ["1.0"] }, + ], + Activities = + [ + new DurableTaskWorkerWorkItemFilters.ActivityFilter { Name = "MyActivity", Versions = ["1.0", "2.0"] }, + ], + Entities = + [ + new DurableTaskWorkerWorkItemFilters.EntityFilter { Name = "myentity" }, + ], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Orchestrations.Should().ContainSingle().Which.Name.Should().Be("MyOrchestrator"); + result.Orchestrations[0].Versions.Should().BeEquivalentTo(["1.0"]); + result.Activities.Should().ContainSingle().Which.Name.Should().Be("MyActivity"); + result.Activities[0].Versions.Should().BeEquivalentTo(["1.0", "2.0"]); + result.Entities.Should().ContainSingle().Which.Name.Should().Be("myentity"); + } +} From 704b03fa1d26dd48d1f1d16592fa15ff21563893 Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Mon, 9 Feb 2026 14:40:46 -0800 Subject: [PATCH 03/10] Update comments, handle named builder Signed-off-by: Hal Spang --- .../DurableTaskWorkerBuilderExtensions.cs | 27 +++++-- .../Core/DurableTaskWorkerWorkItemFilters.cs | 18 ++--- src/Worker/Grpc/GrpcDurableTaskWorker.cs | 6 +- .../UseWorkItemFiltersTests.cs | 78 ++++++++++++++++--- 4 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs index 66a8521bd..d07f53b2b 100644 --- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs +++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs @@ -150,17 +150,30 @@ public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWork Check.NotNull(builder); if (workItemFilters != null) { - builder.Services.AddSingleton(workItemFilters); + // Use the options pattern with the builder's name to support named builders + builder.Services.AddOptions(builder.Name) + .Configure(opts => + { + opts.Orchestrations = workItemFilters.Orchestrations; + opts.Activities = workItemFilters.Activities; + opts.Entities = workItemFilters.Entities; + }); } else { // Auto-generate the filters from registered orchestrations, activities, and entities. - builder.Services.AddSingleton(provider => - { - DurableTaskRegistry registry = provider.GetRequiredService>().Get(builder.Name); - DurableTaskWorkerOptions? options = provider.GetOptions(builder.Name); - return DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, options); - }); + builder.Services.AddOptions(builder.Name) + .Configure, IOptionsMonitor>( + (opts, registryMonitor, workerOptionsMonitor) => + { + DurableTaskRegistry registry = registryMonitor.Get(builder.Name); + DurableTaskWorkerOptions workerOptions = workerOptionsMonitor.Get(builder.Name); + DurableTaskWorkerWorkItemFilters generated = + DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, workerOptions); + opts.Orchestrations = generated.Orchestrations; + opts.Activities = generated.Activities; + opts.Entities = generated.Entities; + }); } return builder; diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index 57270f0e7..5ed264870 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -11,19 +11,19 @@ namespace Microsoft.DurableTask.Worker; public class DurableTaskWorkerWorkItemFilters { /// - /// Gets or initializes the orchestration filters. + /// Gets or sets the orchestration filters. /// - public IReadOnlyList Orchestrations { get; init; } = []; + public IReadOnlyList Orchestrations { get; set; } = []; /// - /// Gets or initializes the activity filters. + /// Gets or sets the activity filters. /// - public IReadOnlyList Activities { get; init; } = []; + public IReadOnlyList Activities { get; set; } = []; /// - /// Gets or initializes the entity filters. + /// Gets or sets the entity filters. /// - public IReadOnlyList Entities { get; init; } = []; + public IReadOnlyList Entities { get; set; } = []; /// /// Creates a new instance of the class. @@ -55,7 +55,7 @@ internal static DurableTaskWorkerWorkItemFilters FromDurableTaskRegistry(Durable } /// - /// Struct specifying an orchestration filter. + /// Specifies an orchestration filter. /// public struct OrchestrationFilter { @@ -71,7 +71,7 @@ public struct OrchestrationFilter } /// - /// Struct specifying an activity filter. + /// Specifies an activity filter. /// public struct ActivityFilter { @@ -87,7 +87,7 @@ public struct ActivityFilter } /// - /// Struct specifying an entity filter. + /// Specifies an entity filter. /// public struct EntityFilter { diff --git a/src/Worker/Grpc/GrpcDurableTaskWorker.cs b/src/Worker/Grpc/GrpcDurableTaskWorker.cs index e2d875ae6..1d03d96ae 100644 --- a/src/Worker/Grpc/GrpcDurableTaskWorker.cs +++ b/src/Worker/Grpc/GrpcDurableTaskWorker.cs @@ -31,7 +31,7 @@ sealed partial class GrpcDurableTaskWorker : DurableTaskWorker /// The logger. /// The optional used to filter orchestration execution. /// The custom exception properties provider that help build failure details. - /// The optional used to filter work items in the backend. + /// The optional used to filter work items in the backend. public GrpcDurableTaskWorker( string name, IDurableTaskFactory factory, @@ -41,7 +41,7 @@ public GrpcDurableTaskWorker( ILoggerFactory loggerFactory, IOrchestrationFilter? orchestrationFilter = null, IExceptionPropertiesProvider? exceptionPropertiesProvider = null, - DurableTaskWorkerWorkItemFilters? workItemFilters = null) + IOptionsMonitor? workItemFiltersMonitor = null) : base(name, factory) { this.grpcOptions = Check.NotNull(grpcOptions).Get(name); @@ -51,7 +51,7 @@ public GrpcDurableTaskWorker( this.logger = CreateLogger(loggerFactory, this.workerOptions); this.orchestrationFilter = orchestrationFilter; this.ExceptionPropertiesProvider = exceptionPropertiesProvider; - this.workItemFilters = workItemFilters; + this.workItemFilters = workItemFiltersMonitor?.Get(name); } /// diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs index 7404e8f2d..eda058d0c 100644 --- a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -3,6 +3,7 @@ using Microsoft.DurableTask.Entities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.DurableTask.Worker.Tests; @@ -33,10 +34,14 @@ public void UseWorkItemFilters_WithExplicitFilters_RegistersFilters() // Act builder.UseWorkItemFilters(filters); ServiceProvider provider = services.BuildServiceProvider(); - DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - actual.Should().BeSameAs(filters); + actual.Orchestrations.Should().BeEquivalentTo(filters.Orchestrations); + actual.Activities.Should().BeEquivalentTo(filters.Activities); + actual.Entities.Should().BeEquivalentTo(filters.Entities); } [Fact] @@ -54,7 +59,9 @@ public void UseWorkItemFilters_WithoutFilters_AutoGeneratesFromRegistry() // Act builder.UseWorkItemFilters(); ServiceProvider provider = services.BuildServiceProvider(); - DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); @@ -83,7 +90,9 @@ public void UseWorkItemFilters_WithVersioning_IncludesVersionInFilters() // Act builder.UseWorkItemFilters(); ServiceProvider provider = services.BuildServiceProvider(); - DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Contains("1.0")); @@ -104,7 +113,9 @@ public void UseWorkItemFilters_WithEntity_IncludesEntityInFilters() // Act builder.UseWorkItemFilters(); ServiceProvider provider = services.BuildServiceProvider(); - DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert actual.Entities.Should().ContainSingle(e => e.Name == nameof(TestEntity).ToLowerInvariant()); @@ -135,7 +146,9 @@ public void UseWorkItemFilters_EmptyRegistry_CreatesEmptyFilters() // Act builder.UseWorkItemFilters(); ServiceProvider provider = services.BuildServiceProvider(); - DurableTaskWorkerWorkItemFilters actual = provider.GetRequiredService(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert actual.Orchestrations.Should().BeEmpty(); @@ -158,12 +171,57 @@ public void UseWorkItemFilters_NamedBuilders_HaveUniqueFilters() // Act ServiceProvider provider = services.BuildServiceProvider(); - IEnumerable allFilters = provider.GetServices(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + + DurableTaskWorkerWorkItemFilters worker1Filters = filtersMonitor.Get("worker1"); + DurableTaskWorkerWorkItemFilters worker2Filters = filtersMonitor.Get("worker2"); // Assert - allFilters.Should().HaveCount(2); - allFilters.Should().Contain(f => f.Orchestrations.Any(o => o.Name == nameof(TestOrchestrator)) && !f.Activities.Any()); - allFilters.Should().Contain(f => f.Activities.Any(a => a.Name == nameof(TestActivity)) && !f.Orchestrations.Any()); + worker1Filters.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); + worker1Filters.Activities.Should().BeEmpty(); + + worker2Filters.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + worker2Filters.Orchestrations.Should().BeEmpty(); + } + + [Fact] + public void UseWorkItemFilters_NamedBuilders_CanResolveCorrectFiltersByName() + { + // Arrange + // This test verifies that named builders can have their filters resolved independently, + // which is how the actual GrpcDurableTaskWorker needs to resolve filters for each named worker. + ServiceCollection services = new(); + + DefaultDurableTaskWorkerBuilder builder1 = new("worker1", services); + builder1.AddTasks(registry => registry.AddOrchestrator()); + builder1.UseWorkItemFilters(); + + DefaultDurableTaskWorkerBuilder builder2 = new("worker2", services); + builder2.AddTasks(registry => registry.AddActivity()); + builder2.UseWorkItemFilters(); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + + // Use the options pattern to get filters by name - this is how the worker should resolve filters + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + + DurableTaskWorkerWorkItemFilters worker1Filters = filtersMonitor.Get("worker1"); + DurableTaskWorkerWorkItemFilters worker2Filters = filtersMonitor.Get("worker2"); + + // Assert + // Worker1 should have orchestrator but no activity + worker1Filters.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); + worker1Filters.Activities.Should().BeEmpty(); + + // Worker2 should have activity but no orchestrator + worker2Filters.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + worker2Filters.Orchestrations.Should().BeEmpty(); + + // The two filters should be different instances + worker1Filters.Should().NotBeSameAs(worker2Filters); } class TestOrchestrator : TaskOrchestrator From 9445a6dcdc5267246ebbd2c6faac83b79ac7650d Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Wed, 18 Feb 2026 18:11:27 -0800 Subject: [PATCH 04/10] Use Version for registration, add defaults to struct Signed-off-by: Hal Spang --- .../Core/DurableTaskWorkerWorkItemFilters.cs | 27 +++++++++++-------- .../UseWorkItemFiltersTests.cs | 4 +-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index 5ed264870..0dcb689b1 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -39,17 +39,17 @@ internal static DurableTaskWorkerWorkItemFilters FromDurableTaskRegistry(Durable Orchestrations = registry.Orchestrators.Select(orchestration => new OrchestrationFilter { Name = orchestration.Key, - Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.DefaultVersion] : [], + Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.Version] : [], }).ToList(), Activities = registry.Activities.Select(activity => new ActivityFilter { Name = activity.Key, - Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.DefaultVersion] : [], + Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.Version] : [], }).ToList(), Entities = registry.Entities.Select(entity => new EntityFilter { // Entity names are normalized to lowercase in the backend. - Name = entity.Key.ToString().ToLowerInvariant(), + Name = entity.Key.ToString(), }).ToList(), }; } @@ -57,43 +57,48 @@ internal static DurableTaskWorkerWorkItemFilters FromDurableTaskRegistry(Durable /// /// Specifies an orchestration filter. /// - public struct OrchestrationFilter + /// The name of the orchestration. + /// The optional versions of the orchestration. + public readonly struct OrchestrationFilter(string name, IReadOnlyList? versions) { /// /// Gets or initializes the name of the orchestration to filter. /// - public string Name { get; init; } + public string Name { get; init; } = name; /// /// Gets or initializes the versions of the orchestration to filter. /// - public IReadOnlyList Versions { get; init; } + public IReadOnlyList Versions { get; init; } = versions ?? []; } /// /// Specifies an activity filter. /// - public struct ActivityFilter + /// The name of the activity. + /// The optional versions of the activity. + public readonly struct ActivityFilter(string name, IReadOnlyList? versions) { /// /// Gets or initializes the name of the activity to filter. /// - public string Name { get; init; } + public string Name { get; init; } = name; /// /// Gets or initializes the versions of the activity to filter. /// - public IReadOnlyList Versions { get; init; } + public IReadOnlyList Versions { get; init; } = versions ?? []; } /// /// Specifies an entity filter. /// - public struct EntityFilter + /// The name of the entity. + public readonly struct EntityFilter(string name) { /// /// Gets or initializes the name of the entity to filter. /// - public string Name { get; init; } + public string Name { get; init; } = name; } } diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs index eda058d0c..e32bea893 100644 --- a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -83,7 +83,7 @@ public void UseWorkItemFilters_WithVersioning_IncludesVersionInFilters() { options.Versioning = new DurableTaskWorkerOptions.VersioningOptions { - DefaultVersion = "1.0" + Version = "1.0" }; }); @@ -118,7 +118,7 @@ public void UseWorkItemFilters_WithEntity_IncludesEntityInFilters() DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - actual.Entities.Should().ContainSingle(e => e.Name == nameof(TestEntity).ToLowerInvariant()); + actual.Entities.Should().ContainSingle(e => e.Name == nameof(TestEntity)); } [Fact] From 9d25bc073207ee1a53e76bb120101fa6f2970a9e Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Mon, 23 Feb 2026 15:52:23 -0800 Subject: [PATCH 05/10] Make filters on by default Signed-off-by: Hal Spang --- .../DurableTaskWorkerBuilderExtensions.cs | 4 +- .../ServiceCollectionExtensions.cs | 15 +++ .../UseWorkItemFiltersTests.cs | 114 ++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs index d07f53b2b..7ae0c6eda 100644 --- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs +++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs @@ -150,9 +150,9 @@ public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWork Check.NotNull(builder); if (workItemFilters != null) { - // Use the options pattern with the builder's name to support named builders + // Use PostConfigure to ensure user-provided filters override the auto-generated defaults. builder.Services.AddOptions(builder.Name) - .Configure(opts => + .PostConfigure(opts => { opts.Orchestrations = workItemFilters.Orchestrations; opts.Activities = workItemFilters.Activities; diff --git a/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs index e68c550cf..9e8a93c6c 100644 --- a/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -86,6 +86,21 @@ static IServiceCollection ConfigureDurableOptions(IServiceCollection services, s } }); + // Auto-generate work item filters from the registry by default. + // Users can override these by calling UseWorkItemFilters(customFilters) on the builder. + services.AddOptions(name) + .Configure, IOptionsMonitor>( + (opts, registryMonitor, workerOptionsMonitor) => + { + DurableTaskRegistry registry = registryMonitor.Get(name); + DurableTaskWorkerOptions workerOptions = workerOptionsMonitor.Get(name); + DurableTaskWorkerWorkItemFilters generated = + DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, workerOptions); + opts.Orchestrations = generated.Orchestrations; + opts.Activities = generated.Activities; + opts.Entities = generated.Entities; + }); + return services; } diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs index e32bea893..831ff75ee 100644 --- a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -224,6 +224,120 @@ public void UseWorkItemFilters_NamedBuilders_CanResolveCorrectFiltersByName() worker1Filters.Should().NotBeSameAs(worker2Filters); } + [Fact] + public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured() + { + // Arrange + ServiceCollection services = new(); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + }); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + // Assert + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + } + + [Fact] + public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() + { + // Arrange + ServiceCollection services = new(); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddEntity(); + }); + }); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + // Assert + actual.Entities.Should().ContainSingle(e => e.Name == nameof(TestEntity)); + } + + [Fact] + public void WorkItemFilters_ExplicitFiltersOverrideDefaults() + { + // Arrange + ServiceCollection services = new(); + DurableTaskWorkerWorkItemFilters customFilters = new() + { + Orchestrations = [new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "CustomOrch", Versions = ["2.0"] }], + Activities = [], + Entities = [], + }; + + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.UseWorkItemFilters(customFilters); + }); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + // Assert + actual.Orchestrations.Should().ContainSingle(o => o.Name == "CustomOrch" && o.Versions.Contains("2.0")); + actual.Activities.Should().BeEmpty(); + actual.Entities.Should().BeEmpty(); + } + + [Fact] + public void WorkItemFilters_DefaultWithVersioning_WhenNoExplicitFiltersConfigured() + { + // Arrange + ServiceCollection services = new(); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.Configure(options => + { + options.Versioning = new DurableTaskWorkerOptions.VersioningOptions + { + Version = "1.0" + }; + }); + }); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + // Assert + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Contains("1.0")); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity) && a.Versions.Contains("1.0")); + } + class TestOrchestrator : TaskOrchestrator { public override Task RunAsync(TaskOrchestrationContext context, object input) From a2ab20dbf37f8d1176207edae955747d3826c78f Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Mon, 23 Feb 2026 16:01:45 -0800 Subject: [PATCH 06/10] Update protos Signed-off-by: Hal Spang --- src/Grpc/versions.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index b13392fe5..743f3f8bd 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch main at 2026-01-13 00:01:21 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/026329c53fe6363985655857b9ca848ec7238bd2/protos/orchestrator_service.proto +# The following files were downloaded from branch main at 2026-02-24 00:01:28 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/1caadbd7ecfdf5f2309acbeac28a3e36d16aa156/protos/orchestrator_service.proto From 5a662871dfa71e7efb669a2d735ba5288274918a Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Mon, 23 Feb 2026 17:26:08 -0800 Subject: [PATCH 07/10] Better handle null filters + testing Signed-off-by: Hal Spang --- .../DurableTaskWorkerBuilderExtensions.cs | 46 ++-- .../Core/DurableTaskWorkerWorkItemFilters.cs | 4 +- .../UseWorkItemFiltersTests.cs | 231 +++++++----------- ...TaskWorkerWorkItemFiltersExtensionTests.cs | 127 ++++++++++ 4 files changed, 242 insertions(+), 166 deletions(-) diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs index 7ae0c6eda..d42845f60 100644 --- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs +++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs @@ -142,39 +142,33 @@ public static IDurableTaskWorkerBuilder UseOrchestrationFilter(this IDurableTask /// Adds to the specified . /// /// The builder to set the builder target for. - /// The instance of a to use. + /// The instance of a to use. + /// If null, the auto-generated default filters will be cleared. /// The same instance, allowing for method chaining. - /// If this is called without specified filters, the filters will be constructed from the registered orchestrations, activities, and entities. - public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWorkerBuilder builder, DurableTaskWorkerWorkItemFilters? workItemFilters = null) + /// Work item filters are auto-generated from the registry by default. + /// Use this method with explicit filters to override the defaults, or with null to opt out of filtering entirely. + public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWorkerBuilder builder, DurableTaskWorkerWorkItemFilters? workItemFilters) { Check.NotNull(builder); - if (workItemFilters != null) - { - // Use PostConfigure to ensure user-provided filters override the auto-generated defaults. - builder.Services.AddOptions(builder.Name) - .PostConfigure(opts => + + // Use PostConfigure to ensure provided filters override the auto-generated defaults. + // When null is passed, the filters are cleared to opt out of filtering entirely. + builder.Services.AddOptions(builder.Name) + .PostConfigure(opts => + { + if (workItemFilters is null) + { + opts.Orchestrations = []; + opts.Activities = []; + opts.Entities = []; + } + else { opts.Orchestrations = workItemFilters.Orchestrations; opts.Activities = workItemFilters.Activities; opts.Entities = workItemFilters.Entities; - }); - } - else - { - // Auto-generate the filters from registered orchestrations, activities, and entities. - builder.Services.AddOptions(builder.Name) - .Configure, IOptionsMonitor>( - (opts, registryMonitor, workerOptionsMonitor) => - { - DurableTaskRegistry registry = registryMonitor.Get(builder.Name); - DurableTaskWorkerOptions workerOptions = workerOptionsMonitor.Get(builder.Name); - DurableTaskWorkerWorkItemFilters generated = - DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, workerOptions); - opts.Orchestrations = generated.Orchestrations; - opts.Activities = generated.Activities; - opts.Entities = generated.Entities; - }); - } + } + }); return builder; } diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index 0dcb689b1..a524a3d8e 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -6,7 +6,9 @@ namespace Microsoft.DurableTask.Worker; /// /// A class that represents work item filters for a Durable Task Worker. These filters are passed to the backend /// and only work items matching the filters will be processed by the worker. If no filters are provided, -/// the worker will process all work items. +/// the worker will process all work items. By default, these are auto-generated from the registered orchestrations, +/// activities, and entities in the . To opt-out of filters, provide a null +/// value to the property when configuring the gRPC worker. /// public class DurableTaskWorkerWorkItemFilters { diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs index 831ff75ee..63bfffbfe 100644 --- a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -16,7 +16,7 @@ public void UseWorkItemFilters_NullBuilder_Throws() IDurableTaskWorkerBuilder builder = null!; // Act - Action act = () => builder.UseWorkItemFilters(); + Action act = () => builder.UseWorkItemFilters(null); // Assert act.Should().ThrowExactly().WithParameterName("builder"); @@ -28,8 +28,12 @@ public void UseWorkItemFilters_WithExplicitFilters_RegistersFilters() // Arrange ServiceCollection services = new(); DefaultDurableTaskWorkerBuilder builder = new("test", services); - DurableTaskRegistry registry = new(); - DurableTaskWorkerWorkItemFilters filters = DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, null); + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = [new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "MyOrch", Versions = ["1.0"] }], + Activities = [new DurableTaskWorkerWorkItemFilters.ActivityFilter { Name = "MyActivity", Versions = [] }], + Entities = [new DurableTaskWorkerWorkItemFilters.EntityFilter { Name = "myentity" }], + }; // Act builder.UseWorkItemFilters(filters); @@ -45,73 +49,58 @@ public void UseWorkItemFilters_WithExplicitFilters_RegistersFilters() } [Fact] - public void UseWorkItemFilters_WithoutFilters_AutoGeneratesFromRegistry() + public void UseWorkItemFilters_ReturnsBuilder_ForChaining() { // Arrange ServiceCollection services = new(); DefaultDurableTaskWorkerBuilder builder = new("test", services); - builder.AddTasks(registry => - { - registry.AddOrchestrator(); - registry.AddActivity(); - }); // Act - builder.UseWorkItemFilters(); - ServiceProvider provider = services.BuildServiceProvider(); - IOptionsMonitor filtersMonitor = - provider.GetRequiredService>(); - DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + IDurableTaskWorkerBuilder result = builder.UseWorkItemFilters(null); // Assert - actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); - actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + result.Should().BeSameAs(builder); } [Fact] - public void UseWorkItemFilters_WithVersioning_IncludesVersionInFilters() + public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured() { // Arrange ServiceCollection services = new(); - DefaultDurableTaskWorkerBuilder builder = new("test", services); - builder.AddTasks(registry => - { - registry.AddOrchestrator(); - registry.AddActivity(); - }); - builder.Configure(options => + services.AddDurableTaskWorker("test", builder => { - options.Versioning = new DurableTaskWorkerOptions.VersioningOptions + builder.AddTasks(registry => { - Version = "1.0" - }; + registry.AddOrchestrator(); + registry.AddActivity(); + }); }); // Act - builder.UseWorkItemFilters(); ServiceProvider provider = services.BuildServiceProvider(); IOptionsMonitor filtersMonitor = provider.GetRequiredService>(); DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Contains("1.0")); - actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity) && a.Versions.Contains("1.0")); + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); } [Fact] - public void UseWorkItemFilters_WithEntity_IncludesEntityInFilters() + public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() { // Arrange ServiceCollection services = new(); - DefaultDurableTaskWorkerBuilder builder = new("test", services); - builder.AddTasks(registry => + services.AddDurableTaskWorker("test", builder => { - registry.AddEntity(); + builder.AddTasks(registry => + { + registry.AddEntity(); + }); }); // Act - builder.UseWorkItemFilters(); ServiceProvider provider = services.BuildServiceProvider(); IOptionsMonitor filtersMonitor = provider.GetRequiredService>(); @@ -122,113 +111,71 @@ public void UseWorkItemFilters_WithEntity_IncludesEntityInFilters() } [Fact] - public void UseWorkItemFilters_ReturnsBuilder_ForChaining() - { - // Arrange - ServiceCollection services = new(); - DefaultDurableTaskWorkerBuilder builder = new("test", services); - - // Act - IDurableTaskWorkerBuilder result = builder.UseWorkItemFilters(); - - // Assert - result.Should().BeSameAs(builder); - } - - [Fact] - public void UseWorkItemFilters_EmptyRegistry_CreatesEmptyFilters() + public void WorkItemFilters_DefaultWithVersioning_WhenNoExplicitFiltersConfigured() { // Arrange ServiceCollection services = new(); - DefaultDurableTaskWorkerBuilder builder = new("test", services); - builder.AddTasks(_ => { }); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.Configure(options => + { + options.Versioning = new DurableTaskWorkerOptions.VersioningOptions + { + Version = "1.0" + }; + }); + }); // Act - builder.UseWorkItemFilters(); ServiceProvider provider = services.BuildServiceProvider(); IOptionsMonitor filtersMonitor = provider.GetRequiredService>(); DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - actual.Orchestrations.Should().BeEmpty(); - actual.Activities.Should().BeEmpty(); - actual.Entities.Should().BeEmpty(); + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Contains("1.0")); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity) && a.Versions.Contains("1.0")); } [Fact] - public void UseWorkItemFilters_NamedBuilders_HaveUniqueFilters() + public void WorkItemFilters_DefaultEmptyRegistry_ProducesEmptyFilters() { // Arrange ServiceCollection services = new(); - DefaultDurableTaskWorkerBuilder builder1 = new("worker1", services); - builder1.AddTasks(registry => registry.AddOrchestrator()); - builder1.UseWorkItemFilters(); - - DefaultDurableTaskWorkerBuilder builder2 = new("worker2", services); - builder2.AddTasks(registry => registry.AddActivity()); - builder2.UseWorkItemFilters(); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(_ => { }); + }); // Act ServiceProvider provider = services.BuildServiceProvider(); IOptionsMonitor filtersMonitor = provider.GetRequiredService>(); - - DurableTaskWorkerWorkItemFilters worker1Filters = filtersMonitor.Get("worker1"); - DurableTaskWorkerWorkItemFilters worker2Filters = filtersMonitor.Get("worker2"); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - worker1Filters.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); - worker1Filters.Activities.Should().BeEmpty(); - - worker2Filters.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); - worker2Filters.Orchestrations.Should().BeEmpty(); + actual.Orchestrations.Should().BeEmpty(); + actual.Activities.Should().BeEmpty(); + actual.Entities.Should().BeEmpty(); } [Fact] - public void UseWorkItemFilters_NamedBuilders_CanResolveCorrectFiltersByName() + public void WorkItemFilters_ExplicitFiltersOverrideDefaults() { // Arrange - // This test verifies that named builders can have their filters resolved independently, - // which is how the actual GrpcDurableTaskWorker needs to resolve filters for each named worker. ServiceCollection services = new(); + DurableTaskWorkerWorkItemFilters customFilters = new() + { + Orchestrations = [new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "CustomOrch", Versions = ["2.0"] }], + Activities = [], + Entities = [], + }; - DefaultDurableTaskWorkerBuilder builder1 = new("worker1", services); - builder1.AddTasks(registry => registry.AddOrchestrator()); - builder1.UseWorkItemFilters(); - - DefaultDurableTaskWorkerBuilder builder2 = new("worker2", services); - builder2.AddTasks(registry => registry.AddActivity()); - builder2.UseWorkItemFilters(); - - // Act - ServiceProvider provider = services.BuildServiceProvider(); - - // Use the options pattern to get filters by name - this is how the worker should resolve filters - IOptionsMonitor filtersMonitor = - provider.GetRequiredService>(); - - DurableTaskWorkerWorkItemFilters worker1Filters = filtersMonitor.Get("worker1"); - DurableTaskWorkerWorkItemFilters worker2Filters = filtersMonitor.Get("worker2"); - - // Assert - // Worker1 should have orchestrator but no activity - worker1Filters.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); - worker1Filters.Activities.Should().BeEmpty(); - - // Worker2 should have activity but no orchestrator - worker2Filters.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); - worker2Filters.Orchestrations.Should().BeEmpty(); - - // The two filters should be different instances - worker1Filters.Should().NotBeSameAs(worker2Filters); - } - - [Fact] - public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured() - { - // Arrange - ServiceCollection services = new(); services.AddDurableTaskWorker("test", builder => { builder.AddTasks(registry => @@ -236,6 +183,7 @@ public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured( registry.AddOrchestrator(); registry.AddActivity(); }); + builder.UseWorkItemFilters(customFilters); }); // Act @@ -245,12 +193,13 @@ public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured( DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); - actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + actual.Orchestrations.Should().ContainSingle(o => o.Name == "CustomOrch" && o.Versions.Contains("2.0")); + actual.Activities.Should().BeEmpty(); + actual.Entities.Should().BeEmpty(); } [Fact] - public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_NullOverwritesDefaults() { // Arrange ServiceCollection services = new(); @@ -258,8 +207,10 @@ public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() { builder.AddTasks(registry => { - registry.AddEntity(); + registry.AddOrchestrator(); + registry.AddActivity(); }); + builder.UseWorkItemFilters(null); }); // Act @@ -269,17 +220,19 @@ public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - actual.Entities.Should().ContainSingle(e => e.Name == nameof(TestEntity)); + actual.Orchestrations.Should().BeEmpty(); + actual.Activities.Should().BeEmpty(); + actual.Entities.Should().BeEmpty(); } [Fact] - public void WorkItemFilters_ExplicitFiltersOverrideDefaults() + public void WorkItemFilters_EmptyFiltersOverrideDefaults() { // Arrange ServiceCollection services = new(); - DurableTaskWorkerWorkItemFilters customFilters = new() + DurableTaskWorkerWorkItemFilters emptyFilters = new() { - Orchestrations = [new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "CustomOrch", Versions = ["2.0"] }], + Orchestrations = [], Activities = [], Entities = [], }; @@ -291,7 +244,7 @@ public void WorkItemFilters_ExplicitFiltersOverrideDefaults() registry.AddOrchestrator(); registry.AddActivity(); }); - builder.UseWorkItemFilters(customFilters); + builder.UseWorkItemFilters(emptyFilters); }); // Act @@ -301,41 +254,41 @@ public void WorkItemFilters_ExplicitFiltersOverrideDefaults() DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); // Assert - actual.Orchestrations.Should().ContainSingle(o => o.Name == "CustomOrch" && o.Versions.Contains("2.0")); + actual.Orchestrations.Should().BeEmpty(); actual.Activities.Should().BeEmpty(); actual.Entities.Should().BeEmpty(); } [Fact] - public void WorkItemFilters_DefaultWithVersioning_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_NamedBuilders_HaveUniqueDefaultFilters() { // Arrange ServiceCollection services = new(); - services.AddDurableTaskWorker("test", builder => + services.AddDurableTaskWorker("worker1", builder => { - builder.AddTasks(registry => - { - registry.AddOrchestrator(); - registry.AddActivity(); - }); - builder.Configure(options => - { - options.Versioning = new DurableTaskWorkerOptions.VersioningOptions - { - Version = "1.0" - }; - }); + builder.AddTasks(registry => registry.AddOrchestrator()); + }); + services.AddDurableTaskWorker("worker2", builder => + { + builder.AddTasks(registry => registry.AddActivity()); }); // Act ServiceProvider provider = services.BuildServiceProvider(); IOptionsMonitor filtersMonitor = provider.GetRequiredService>(); - DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + DurableTaskWorkerWorkItemFilters worker1Filters = filtersMonitor.Get("worker1"); + DurableTaskWorkerWorkItemFilters worker2Filters = filtersMonitor.Get("worker2"); // Assert - actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Contains("1.0")); - actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity) && a.Versions.Contains("1.0")); + worker1Filters.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); + worker1Filters.Activities.Should().BeEmpty(); + + worker2Filters.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + worker2Filters.Orchestrations.Should().BeEmpty(); + + worker1Filters.Should().NotBeSameAs(worker2Filters); } class TestOrchestrator : TaskOrchestrator @@ -357,4 +310,4 @@ public override Task RunAsync(TaskActivityContext context, object input) class TestEntity : TaskEntity { } -} +} \ No newline at end of file diff --git a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs index fa669df7e..6fd03210a 100644 --- a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs +++ b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs @@ -2,6 +2,10 @@ // Licensed under the MIT License. using Microsoft.DurableTask.Worker.Grpc.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using P = Microsoft.DurableTask.Protobuf; namespace Microsoft.DurableTask.Worker.Grpc.Tests; @@ -260,4 +264,127 @@ public void ToGrpcWorkItemFilters_WithMixedFilters_ConvertsAll() result.Activities[0].Versions.Should().BeEquivalentTo(["1.0", "2.0"]); result.Entities.Should().ContainSingle().Which.Name.Should().Be("myentity"); } + + [Fact] + public void WorkerConstruction_DefaultFilters_FlowToWorker() + { + // Arrange + ServiceCollection services = new(); + services.AddSingleton(new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory()); + + services.AddDurableTaskWorker(builder => + { + builder.UseGrpc(); + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + }); + + // Act + using ServiceProvider provider = services.BuildServiceProvider(); + IHostedService hosted = Assert.Single(provider.GetServices()); + Assert.IsType(hosted); + + DurableTaskWorkerWorkItemFilters? filters = (DurableTaskWorkerWorkItemFilters?)typeof(GrpcDurableTaskWorker) + .GetField("workItemFilters", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetValue(hosted); + + // Assert + filters.Should().NotBeNull(); + filters!.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); + filters.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + } + + [Fact] + public void WorkerConstruction_ExplicitFilters_FlowToWorker() + { + // Arrange + ServiceCollection services = new(); + services.AddSingleton(new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory()); + + DurableTaskWorkerWorkItemFilters customFilters = new() + { + Orchestrations = [new DurableTaskWorkerWorkItemFilters.OrchestrationFilter { Name = "CustomOrch", Versions = ["2.0"] }], + Activities = [], + Entities = [], + }; + + services.AddDurableTaskWorker(builder => + { + builder.UseGrpc(); + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.UseWorkItemFilters(customFilters); + }); + + // Act + using ServiceProvider provider = services.BuildServiceProvider(); + IHostedService hosted = Assert.Single(provider.GetServices()); + Assert.IsType(hosted); + + DurableTaskWorkerWorkItemFilters? filters = (DurableTaskWorkerWorkItemFilters?)typeof(GrpcDurableTaskWorker) + .GetField("workItemFilters", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetValue(hosted); + + // Assert + filters.Should().NotBeNull(); + filters!.Orchestrations.Should().ContainSingle(o => o.Name == "CustomOrch" && o.Versions.Contains("2.0")); + filters.Activities.Should().BeEmpty(); + filters.Entities.Should().BeEmpty(); + } + + [Fact] + public void WorkerConstruction_NullFilters_ClearsDefaultsOnWorker() + { + // Arrange + ServiceCollection services = new(); + services.AddSingleton(new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory()); + + services.AddDurableTaskWorker(builder => + { + builder.UseGrpc(); + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.UseWorkItemFilters(null); + }); + + // Act + using ServiceProvider provider = services.BuildServiceProvider(); + IHostedService hosted = Assert.Single(provider.GetServices()); + Assert.IsType(hosted); + + DurableTaskWorkerWorkItemFilters? filters = (DurableTaskWorkerWorkItemFilters?)typeof(GrpcDurableTaskWorker) + .GetField("workItemFilters", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetValue(hosted); + + // Assert + filters.Should().NotBeNull(); + filters!.Orchestrations.Should().BeEmpty(); + filters.Activities.Should().BeEmpty(); + filters.Entities.Should().BeEmpty(); + } + + class TestOrchestrator : TaskOrchestrator + { + public override Task RunAsync(TaskOrchestrationContext context, object input) + { + throw new NotImplementedException(); + } + } + + class TestActivity : TaskActivity + { + public override Task RunAsync(TaskActivityContext context, object input) + { + throw new NotImplementedException(); + } + } } From 4f45291074c3cb18d88303d75a6f0e42d764bf0c Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Mon, 23 Feb 2026 17:41:40 -0800 Subject: [PATCH 08/10] Fix comment formatting Signed-off-by: Hal Spang --- src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index a524a3d8e..96c9b5177 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -7,8 +7,8 @@ namespace Microsoft.DurableTask.Worker; /// A class that represents work item filters for a Durable Task Worker. These filters are passed to the backend /// and only work items matching the filters will be processed by the worker. If no filters are provided, /// the worker will process all work items. By default, these are auto-generated from the registered orchestrations, -/// activities, and entities in the . To opt-out of filters, provide a null -/// value to the property when configuring the gRPC worker. +/// activities, and entities in the . To opt-out of filters, provide a null +/// value to the property when configuring the gRPC worker. /// public class DurableTaskWorkerWorkItemFilters { From 1c504fc2ffae57418cd08849cfbde4d452110276 Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Tue, 24 Feb 2026 10:43:24 -0800 Subject: [PATCH 09/10] Fix default constructor issue Signed-off-by: Hal Spang --- .../Core/DurableTaskWorkerWorkItemFilters.cs | 26 +++++++++++++- ...TaskWorkerWorkItemFiltersExtensionTests.cs | 35 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index 96c9b5177..08c6415a0 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -8,7 +8,7 @@ namespace Microsoft.DurableTask.Worker; /// and only work items matching the filters will be processed by the worker. If no filters are provided, /// the worker will process all work items. By default, these are auto-generated from the registered orchestrations, /// activities, and entities in the . To opt-out of filters, provide a null -/// value to the property when configuring the gRPC worker. +/// value to the method when configuring the worker. /// public class DurableTaskWorkerWorkItemFilters { @@ -63,6 +63,14 @@ internal static DurableTaskWorkerWorkItemFilters FromDurableTaskRegistry(Durable /// The optional versions of the orchestration. public readonly struct OrchestrationFilter(string name, IReadOnlyList? versions) { + /// + /// Initializes a new instance of the struct with default values. + /// + public OrchestrationFilter() + : this(string.Empty, []) + { + } + /// /// Gets or initializes the name of the orchestration to filter. /// @@ -81,6 +89,14 @@ public readonly struct OrchestrationFilter(string name, IReadOnlyList? v /// The optional versions of the activity. public readonly struct ActivityFilter(string name, IReadOnlyList? versions) { + /// + /// Initializes a new instance of the struct with default values. + /// + public ActivityFilter() + : this(string.Empty, []) + { + } + /// /// Gets or initializes the name of the activity to filter. /// @@ -98,6 +114,14 @@ public readonly struct ActivityFilter(string name, IReadOnlyList? versio /// The name of the entity. public readonly struct EntityFilter(string name) { + /// + /// Initializes a new instance of the struct with default values. + /// + public EntityFilter() + : this(string.Empty) + { + } + /// /// Gets or initializes the name of the entity to filter. /// diff --git a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs index 6fd03210a..92be9f99e 100644 --- a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs +++ b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs @@ -163,6 +163,41 @@ public void ToGrpcWorkItemFilters_WithEntity_ConvertsName() result.Entities[0].Name.Should().Be("testentity"); } + [Fact] + public void ToGrpcWorkItemFilters_WithNullVersions_ConvertsWithoutError() + { + // Arrange + DurableTaskWorkerWorkItemFilters filters = new() + { + Orchestrations = + [ + new DurableTaskWorkerWorkItemFilters.OrchestrationFilter + { + Name = "TestOrchestrator", + }, + ], + Activities = + [ + new DurableTaskWorkerWorkItemFilters.ActivityFilter + { + Name = "TestActivity", + }, + ], + Entities = [], + }; + + // Act + P.WorkItemFilters result = filters.ToGrpcWorkItemFilters(); + + // Assert + result.Orchestrations.Should().ContainSingle(); + result.Orchestrations[0].Name.Should().Be("TestOrchestrator"); + result.Orchestrations[0].Versions.Should().BeEmpty(); + result.Activities.Should().ContainSingle(); + result.Activities[0].Name.Should().Be("TestActivity"); + result.Activities[0].Versions.Should().BeEmpty(); + } + [Fact] public void ToGrpcWorkItemFilters_WithMultipleOrchestrations_ConvertsAll() { From 481eddca3f670904184399235e8d1fa8b14d464b Mon Sep 17 00:00:00 2001 From: Hal Spang Date: Tue, 24 Feb 2026 15:25:47 -0800 Subject: [PATCH 10/10] Update default version handling Signed-off-by: Hal Spang --- .../Core/DurableTaskWorkerWorkItemFilters.cs | 13 +++- .../UseWorkItemFiltersTests.cs | 77 +++++++++++++++++-- ...TaskWorkerWorkItemFiltersExtensionTests.cs | 5 +- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index 08c6415a0..c29ff95ca 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -35,18 +35,25 @@ public class DurableTaskWorkerWorkItemFilters /// A new instance of constructed from the provided registry. internal static DurableTaskWorkerWorkItemFilters FromDurableTaskRegistry(DurableTaskRegistry registry, DurableTaskWorkerOptions? workerOptions) { - // TODO: Support multiple versions per orchestration/activity. For now, grab the worker version from the options. + // TODO: Support multiple versions per orchestration/activity. + // For now, fetch the version based on the versioning match strategy if defined. If undefined, default to null (all versions match). + IReadOnlyList versions = []; + if (workerOptions?.Versioning?.MatchStrategy == DurableTaskWorkerOptions.VersionMatchStrategy.Strict) + { + versions = [workerOptions.Versioning.Version]; + } + return new DurableTaskWorkerWorkItemFilters { Orchestrations = registry.Orchestrators.Select(orchestration => new OrchestrationFilter { Name = orchestration.Key, - Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.Version] : [], + Versions = versions, }).ToList(), Activities = registry.Activities.Select(activity => new ActivityFilter { Name = activity.Key, - Versions = workerOptions?.Versioning != null ? [workerOptions.Versioning.Version] : [], + Versions = versions, }).ToList(), Entities = registry.Entities.Select(entity => new EntityFilter { diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs index 63bfffbfe..9e325fe7b 100644 --- a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -111,7 +111,7 @@ public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() } [Fact] - public void WorkItemFilters_DefaultWithVersioning_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_DefaultNullWithVersioningCurrentOrOlder_WhenNoExplicitFiltersConfigured() { // Arrange ServiceCollection services = new(); @@ -126,7 +126,74 @@ public void WorkItemFilters_DefaultWithVersioning_WhenNoExplicitFiltersConfigure { options.Versioning = new DurableTaskWorkerOptions.VersioningOptions { - Version = "1.0" + Version = "1.0", + MatchStrategy = DurableTaskWorkerOptions.VersionMatchStrategy.CurrentOrOlder, + }; + }); + }); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + // Assert + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Count == 0); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity) && a.Versions.Count == 0); + } + + [Fact] + public void WorkItemFilters_DefaultNullWithVersioningNone_WhenNoExplicitFiltersConfigured() + { + // Arrange + ServiceCollection services = new(); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.Configure(options => + { + options.Versioning = new DurableTaskWorkerOptions.VersioningOptions + { + Version = "1.0", + MatchStrategy = DurableTaskWorkerOptions.VersionMatchStrategy.None, + }; + }); + }); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + // Assert + actual.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator) && o.Versions.Count == 0); + actual.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity) && a.Versions.Count == 0); + } + + [Fact] + public void WorkItemFilters_DefaultVersionWithVersioningStrict_WhenNoExplicitFiltersConfigured() + { + // Arrange + ServiceCollection services = new(); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + }); + builder.Configure(options => + { + options.Versioning = new DurableTaskWorkerOptions.VersioningOptions + { + Version = "1.0", + MatchStrategy = DurableTaskWorkerOptions.VersionMatchStrategy.Strict, }; }); }); @@ -291,7 +358,7 @@ public void WorkItemFilters_NamedBuilders_HaveUniqueDefaultFilters() worker1Filters.Should().NotBeSameAs(worker2Filters); } - class TestOrchestrator : TaskOrchestrator + sealed class TestOrchestrator : TaskOrchestrator { public override Task RunAsync(TaskOrchestrationContext context, object input) { @@ -299,7 +366,7 @@ public override Task RunAsync(TaskOrchestrationContext context, object i } } - class TestActivity : TaskActivity + sealed class TestActivity : TaskActivity { public override Task RunAsync(TaskActivityContext context, object input) { @@ -307,7 +374,7 @@ public override Task RunAsync(TaskActivityContext context, object input) } } - class TestEntity : TaskEntity + sealed class TestEntity : TaskEntity { } } \ No newline at end of file diff --git a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs index 92be9f99e..5921b4dee 100644 --- a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs +++ b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using P = Microsoft.DurableTask.Protobuf; namespace Microsoft.DurableTask.Worker.Grpc.Tests; @@ -407,7 +406,7 @@ public void WorkerConstruction_NullFilters_ClearsDefaultsOnWorker() filters.Entities.Should().BeEmpty(); } - class TestOrchestrator : TaskOrchestrator + sealed class TestOrchestrator : TaskOrchestrator { public override Task RunAsync(TaskOrchestrationContext context, object input) { @@ -415,7 +414,7 @@ public override Task RunAsync(TaskOrchestrationContext context, object i } } - class TestActivity : TaskActivity + sealed class TestActivity : TaskActivity { public override Task RunAsync(TaskActivityContext context, object input) {