Conversation
Introduces a new Microsoft.DurableTask.Extensions.Plugins package that provides a composable plugin/interceptor system inspired by Temporal's plugin architecture. Plugin System Core: - IDurableTaskPlugin interface with named plugins containing interceptors - IOrchestrationInterceptor for orchestration lifecycle events (start/complete/fail) - IActivityInterceptor for activity lifecycle events (start/complete/fail) - SimplePlugin builder pattern (mirrors Temporal's SimplePlugin) - PluginPipeline for orchestrating interceptor execution - PluginOrchestrationWrapper and PluginActivityWrapper for transparent integration - DI extension methods: UsePlugin(), UsePlugins() on IDurableTaskWorkerBuilder 5 Built-in Plugins: 1. LoggingPlugin - Structured ILogger events for all orchestration/activity lifecycle 2. MetricsPlugin - Thread-safe execution counts, durations, success/failure tracking 3. AuthorizationPlugin - IAuthorizationHandler-based authorization before execution 4. ValidationPlugin - IInputValidator-based input validation before execution 5. RateLimitingPlugin - Token-bucket rate limiting for activity dispatches Also includes: - 24 unit tests covering all plugins and the pipeline - PluginsSample demonstrating all 5 plugins with in-process test host - Convenience extension methods: UseLoggingPlugin(), UseMetricsPlugin(), etc.
Plugins now serve two purposes, matching Temporal's design: 1. Reusable activities/orchestrations - plugins can ship pre-built tasks that auto-register when UsePlugin() is called. Users import a plugin and its activities become available to call from any orchestration. 2. Cross-cutting interceptors - the existing interceptor support for logging, metrics, auth, validation, rate limiting (unchanged). Changes: - IDurableTaskPlugin: added RegisterTasks(DurableTaskRegistry) method - SimplePlugin: added AddTasks(Action<DurableTaskRegistry>) builder method - UsePlugin(): now auto-calls RegisterTasks to register plugin tasks - 5 built-in plugins: implement RegisterTasks as no-op (cross-cutting only) - Added 5 new SimplePluginTests for task registration - Updated sample to demonstrate the importable plugin pattern - Updated README with dual-purpose documentation All 29 tests pass.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new Durable Task plugin/interceptor system (plus several built-in plugins) under src/Extensions/Plugins, along with a sample and a new test project to validate plugin behavior.
Changes:
- Added core plugin abstractions (
IDurableTaskPlugin, interceptor interfaces/contexts), aPluginPipeline, and aSimplePluginbuilder. - Added built-in plugins: logging, metrics, authorization, validation, and rate limiting (+ DI extension methods).
- Added a new
PluginsSampleand a newExtensions.Plugins.Teststest project; updated the main solution to include the new projects.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Extensions.Plugins.Tests/AuthorizationPluginTests.cs | Unit tests for AuthorizationPlugin. |
| test/Extensions.Plugins.Tests/Extensions.Plugins.Tests.csproj | New test project for plugins. |
| test/Extensions.Plugins.Tests/LoggingPluginTests.cs | Unit tests for LoggingPlugin. |
| test/Extensions.Plugins.Tests/MetricsPluginTests.cs | Unit tests for MetricsPlugin + MetricsStore. |
| test/Extensions.Plugins.Tests/PluginPipelineTests.cs | Tests for interceptor invocation ordering in PluginPipeline. |
| test/Extensions.Plugins.Tests/RateLimitingPluginTests.cs | Unit tests for token-bucket rate limiting behavior. |
| test/Extensions.Plugins.Tests/ValidationPluginTests.cs | Unit tests for ValidationPlugin. |
| src/Extensions/Plugins/SimplePlugin.cs | Adds a builder-pattern “simple plugin” aggregator. |
| src/Extensions/Plugins/Plugins.csproj | New Extensions project for plugins + built-ins. |
| src/Extensions/Plugins/PluginPipeline.cs | Pipeline executor that runs interceptors in registration order. |
| src/Extensions/Plugins/PluginOrchestrationWrapper.cs | Wrapper intended to run orchestration interceptors around orchestrators. |
| src/Extensions/Plugins/PluginActivityWrapper.cs | Wrapper intended to run activity interceptors around activities. |
| src/Extensions/Plugins/OrchestrationInterceptorContext.cs | Context passed to orchestration interceptors. |
| src/Extensions/Plugins/IOrchestrationInterceptor.cs | Orchestration interceptor interface contract. |
| src/Extensions/Plugins/IDurableTaskPlugin.cs | Core plugin interface (name + interceptors). |
| src/Extensions/Plugins/IActivityInterceptor.cs | Activity interceptor interface contract. |
| src/Extensions/Plugins/DependencyInjection/DurableTaskWorkerBuilderExtensions.Plugins.cs | Adds UsePlugin(s) DI registration helpers. |
| src/Extensions/Plugins/DependencyInjection/DurableTaskBuiltInPluginExtensions.cs | Convenience DI helpers for built-in plugins. |
| src/Extensions/Plugins/BuiltIn/ValidationResult.cs | Validation result type for ValidationPlugin. |
| src/Extensions/Plugins/BuiltIn/ValidationPlugin.cs | Built-in input validation plugin. |
| src/Extensions/Plugins/BuiltIn/RateLimitingPlugin.cs | Built-in per-activity token bucket rate limiter. |
| src/Extensions/Plugins/BuiltIn/MetricsPlugin.cs | Built-in metrics plugin + MetricsStore + TaskMetrics. |
| src/Extensions/Plugins/BuiltIn/LoggingPlugin.cs | Built-in structured logging plugin. |
| src/Extensions/Plugins/BuiltIn/IInputValidator.cs | Validator contract for ValidationPlugin. |
| src/Extensions/Plugins/BuiltIn/IAuthorizationHandler.cs | Authorization handler contract for AuthorizationPlugin. |
| src/Extensions/Plugins/BuiltIn/AuthorizationTargetType.cs | Enum describing authorization target type. |
| src/Extensions/Plugins/BuiltIn/AuthorizationPlugin.cs | Built-in authorization plugin. |
| src/Extensions/Plugins/BuiltIn/AuthorizationContext.cs | Context passed to authorization handler. |
| src/Extensions/Plugins/ActivityInterceptorContext.cs | Context passed to activity interceptors. |
| samples/PluginsSample/README.md | Documentation for the plugins sample. |
| samples/PluginsSample/Program.cs | Sample code demonstrating the plugin APIs. |
| samples/PluginsSample/PluginsSample.csproj | New sample project referencing the new plugins project. |
| Microsoft.DurableTask.sln | Adds the new Plugins project, test project, and sample to the solution (+ new platform configs). |
You can also share your feedback on Copilot code review. Take the survey.
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="$(SrcRoot)Extensions/Plugins/Plugins.csproj" /> | ||
| </ItemGroup> |
There was a problem hiding this comment.
This test project is missing <IsTestProject>true</IsTestProject> (and typically <IsPackable>false</IsPackable>). In this repo, test/Directory.Build.targets only adds xUnit/Moq/FluentAssertions/Microsoft.NET.Test.Sdk (and TestHelpers) when IsTestProject is true, so this project will not restore the required test dependencies and tests likely won’t run/discover.
| Mock<IOrchestrationInterceptor> interceptor1 = new(); | ||
| Mock<IOrchestrationInterceptor> interceptor2 = new(); | ||
|
|
||
| SimplePlugin plugin = SimplePlugin.NewBuilder("test") | ||
| .AddOrchestrationInterceptor(interceptor1.Object) | ||
| .AddOrchestrationInterceptor(interceptor2.Object) |
There was a problem hiding this comment.
The Moq mocks here don’t set up OnOrchestrationStartingAsync to return a non-null Task. With Moq’s default behavior, Task-returning methods can return null, which will cause PluginPipeline to throw when it does await interceptor.OnOrchestrationStartingAsync(...). Add setups returning Task.CompletedTask (or use MockBehavior.Strict + setups).
| Mock<IActivityInterceptor> interceptor1 = new(); | ||
| Mock<IActivityInterceptor> interceptor2 = new(); | ||
|
|
||
| SimplePlugin plugin = SimplePlugin.NewBuilder("test") | ||
| .AddActivityInterceptor(interceptor1.Object) | ||
| .AddActivityInterceptor(interceptor2.Object) | ||
| .Build(); |
There was a problem hiding this comment.
The Moq mocks here don’t set up OnActivityStartingAsync to return a non-null Task. If Moq returns null for this Task-returning method, PluginPipeline will throw when awaiting it. Add setups returning Task.CompletedTask (or use MockBehavior.Strict + setups).
| Mock<IOrchestrationInterceptor> interceptor = new(); | ||
|
|
||
| SimplePlugin plugin = SimplePlugin.NewBuilder("test") | ||
| .AddOrchestrationInterceptor(interceptor.Object) | ||
| .Build(); | ||
|
|
There was a problem hiding this comment.
interceptor is a Moq mock with no setup for OnOrchestrationCompletedAsync, so it may return null for the Task and cause PluginPipeline.ExecuteOrchestrationCompletedAsync to throw when awaiting it. Set up the mock to return Task.CompletedTask before executing the pipeline.
| Mock<IOrchestrationInterceptor> interceptor = new(); | ||
| Exception exception = new InvalidOperationException("test error"); | ||
|
|
||
| SimplePlugin plugin = SimplePlugin.NewBuilder("test") | ||
| .AddOrchestrationInterceptor(interceptor.Object) | ||
| .Build(); | ||
|
|
There was a problem hiding this comment.
interceptor is a Moq mock with no setup for OnOrchestrationFailedAsync, so it may return null for the Task and cause PluginPipeline.ExecuteOrchestrationFailedAsync to throw when awaiting it. Set up the mock to return Task.CompletedTask before executing the pipeline.
| ## What Plugins Can Do | ||
|
|
||
| Temporal-style plugins serve **two purposes**: | ||
|
|
||
| ### 1. Reusable Activities and Orchestrations | ||
|
|
||
| Plugins can ship pre-built activities and orchestrations that users get automatically | ||
| when they register the plugin. This is the "import and use" pattern: | ||
|
|
||
| ```csharp | ||
| // A plugin author creates a package with reusable activities | ||
| var stringUtilsPlugin = SimplePlugin.NewBuilder("MyOrg.StringUtils") | ||
| .AddTasks(registry => | ||
| { | ||
| registry.AddActivityFunc<string, string>("StringUtils.ToUpper", |
There was a problem hiding this comment.
The README says the sample “registers all 5 built-in plugins on a Durable Task worker” and lists a DTS sidecar as a prerequisite, but Program.cs uses DurableTaskTestHost and doesn’t actually configure a worker builder with .UseLoggingPlugin()/.UseMetricsPlugin()/.UsePlugin(...) or connect to a sidecar. Either update the README to match the in-process demo, or update the sample code to actually register plugins on a worker and run against the emulator as documented.
| // plugin/interceptor pattern. It shows how to use the 5 built-in plugins: | ||
| // 1. LoggingPlugin - Structured logging for orchestration and activity lifecycle events | ||
| // 2. MetricsPlugin - Execution counts, durations, and success/failure tracking | ||
| // 3. AuthorizationPlugin - Input-based authorization checks before execution | ||
| // 4. ValidationPlugin - Input validation before task execution | ||
| // 5. RateLimitingPlugin - Token-bucket rate limiting for activity dispatches |
There was a problem hiding this comment.
Despite the header comment saying this “shows how to use the 5 built-in plugins”, the sample never registers plugins on a worker (e.g., via services.AddDurableTaskWorker().UseLoggingPlugin()...) and instead manually invokes interceptor methods. If the intent is to demonstrate the end-to-end worker plugin pipeline, this should be updated to actually configure a worker with the plugin extensions and run an orchestration/activity through it.
| // plugin/interceptor pattern. It shows how to use the 5 built-in plugins: | |
| // 1. LoggingPlugin - Structured logging for orchestration and activity lifecycle events | |
| // 2. MetricsPlugin - Execution counts, durations, and success/failure tracking | |
| // 3. AuthorizationPlugin - Input-based authorization checks before execution | |
| // 4. ValidationPlugin - Input validation before task execution | |
| // 5. RateLimitingPlugin - Token-bucket rate limiting for activity dispatches | |
| // plugin/interceptor pattern. It introduces the 5 built-in plugins and shows how to inspect | |
| // and interact with their interceptor pipelines programmatically: | |
| // 1. LoggingPlugin - Structured logging for orchestration and activity lifecycle events | |
| // 2. MetricsPlugin - Execution counts, durations, and success/failure tracking | |
| // 3. AuthorizationPlugin - Input-based authorization checks before execution | |
| // 4. ValidationPlugin - Input validation before task execution | |
| // 5. RateLimitingPlugin - Token-bucket rate limiting for activity dispatches | |
| // In a real worker, these plugins are registered via AddDurableTaskWorker().UseLoggingPlugin(), | |
| // UseMetricsPlugin(), UseAuthorizationPlugin(), UseValidationPlugin(), and UseRateLimitingPlugin(). |
| public async Task ExecuteOrchestrationStartingAsync(OrchestrationInterceptorContext context) | ||
| { | ||
| foreach (IDurableTaskPlugin plugin in this.plugins) | ||
| { | ||
| foreach (IOrchestrationInterceptor interceptor in plugin.OrchestrationInterceptors) |
There was a problem hiding this comment.
These public pipeline entrypoints don’t validate arguments (e.g., context can be null). Most public APIs in this repo use Check.NotNull(...) at boundaries, so consider adding null checks to fail fast with a clearer exception.
| this.orchestrationInterceptors = new List<IOrchestrationInterceptor> | ||
| { | ||
| new ValidationOrchestrationInterceptor(validators), | ||
| }; | ||
| this.activityInterceptors = new List<IActivityInterceptor> | ||
| { | ||
| new ValidationActivityInterceptor(validators), |
There was a problem hiding this comment.
validators is only checked for null, but the array may contain null elements and is stored without copying. A null element will cause a NullReferenceException during interception, and external mutation of the passed-in array could change plugin behavior after construction. Consider validating that all elements are non-null and copying the array defensively in the constructor.
| this.orchestrationInterceptors = new List<IOrchestrationInterceptor> | |
| { | |
| new ValidationOrchestrationInterceptor(validators), | |
| }; | |
| this.activityInterceptors = new List<IActivityInterceptor> | |
| { | |
| new ValidationActivityInterceptor(validators), | |
| IInputValidator[] validatorsCopy = new IInputValidator[validators.Length]; | |
| for (int i = 0; i < validators.Length; i++) | |
| { | |
| IInputValidator validator = validators[i] | |
| ?? throw new ArgumentException("validators cannot contain null elements.", nameof(validators)); | |
| validatorsCopy[i] = validator; | |
| } | |
| this.orchestrationInterceptors = new List<IOrchestrationInterceptor> | |
| { | |
| new ValidationOrchestrationInterceptor(validatorsCopy), | |
| }; | |
| this.activityInterceptors = new List<IActivityInterceptor> | |
| { | |
| new ValidationActivityInterceptor(validatorsCopy), |
| readonly RateLimitingOptions options; | ||
| readonly ConcurrentDictionary<string, TokenBucket> buckets = new(); | ||
|
|
||
| public RateLimitingActivityInterceptor(RateLimitingOptions options) => this.options = options; | ||
|
|
||
| public Task OnActivityStartingAsync(ActivityInterceptorContext context) | ||
| { | ||
| string key = context.Name; | ||
| TokenBucket bucket = this.buckets.GetOrAdd(key, _ => new TokenBucket( | ||
| this.options.MaxTokens, | ||
| this.options.RefillRate, | ||
| this.options.RefillInterval)); | ||
|
|
||
| if (!bucket.TryConsume()) | ||
| { | ||
| throw new RateLimitExceededException( | ||
| $"Rate limit exceeded for activity '{context.Name}'. " + | ||
| $"Max {this.options.MaxTokens} executions per {this.options.RefillInterval}."); |
There was a problem hiding this comment.
RateLimitingActivityInterceptor stores a reference to the mutable RateLimitingOptions. If the options instance is mutated after plugin construction, new token buckets may be created with different settings than existing ones. Consider copying the option values into immutable fields (or cloning the options) when constructing the interceptor/plugin.
| readonly RateLimitingOptions options; | |
| readonly ConcurrentDictionary<string, TokenBucket> buckets = new(); | |
| public RateLimitingActivityInterceptor(RateLimitingOptions options) => this.options = options; | |
| public Task OnActivityStartingAsync(ActivityInterceptorContext context) | |
| { | |
| string key = context.Name; | |
| TokenBucket bucket = this.buckets.GetOrAdd(key, _ => new TokenBucket( | |
| this.options.MaxTokens, | |
| this.options.RefillRate, | |
| this.options.RefillInterval)); | |
| if (!bucket.TryConsume()) | |
| { | |
| throw new RateLimitExceededException( | |
| $"Rate limit exceeded for activity '{context.Name}'. " + | |
| $"Max {this.options.MaxTokens} executions per {this.options.RefillInterval}."); | |
| readonly int maxTokens; | |
| readonly int refillRate; | |
| readonly System.TimeSpan refillInterval; | |
| readonly ConcurrentDictionary<string, TokenBucket> buckets = new(); | |
| public RateLimitingActivityInterceptor(RateLimitingOptions options) | |
| { | |
| this.maxTokens = options.MaxTokens; | |
| this.refillRate = options.RefillRate; | |
| this.refillInterval = options.RefillInterval; | |
| } | |
| public Task OnActivityStartingAsync(ActivityInterceptorContext context) | |
| { | |
| string key = context.Name; | |
| TokenBucket bucket = this.buckets.GetOrAdd( | |
| key, | |
| _ => new TokenBucket( | |
| this.maxTokens, | |
| this.refillRate, | |
| this.refillInterval)); | |
| if (!bucket.TryConsume()) | |
| { | |
| throw new RateLimitExceededException( | |
| $"Rate limit exceeded for activity '{context.Name}'. " + | |
| $"Max {this.maxTokens} executions per {this.refillInterval}."); |
… real DTS Key changes: - PluginOrchestrationWrapper and PluginActivityWrapper are now wired into the actual execution pipeline via PluginRegistryPostConfigure. When UsePlugin() is called, a PostConfigure<DurableTaskRegistry> wraps every registered orchestrator/activity factory so the plugin interceptors run transparently on real executions. - Added InternalsVisibleTo for Plugins in Abstractions.csproj to access registry internals (Orchestrators, Activities dictionaries). - Removed SharedSection Core from Plugins.csproj to avoid polyfill type clashes on net6+/net8+/net10+ (IVT exposes netstandard2.0 polyfills). Added minimal local Check class instead. - Added HasOrchestrationInterceptors and HasActivityInterceptors properties to PluginPipeline for conditional wrapping. E2E Tests (test/Extensions.Plugins.E2ETests/): - 9 tests against real DTS scheduler (wbtwus1, westus3) - All pass in ~1.4 minutes - Tests: MetricsPlugin, LoggingPlugin, AuthorizationPlugin (allow + deny), ValidationPlugin (accept + reject), PluginRegisteredActivity, MultiplePlugins, PluginWithTasksAndInterceptors - Requires DTS_CONNECTION_STRING env var (not for CI, local only)
E2E Test Report — Plugin System against Real DTS SchedulerTest Environment
Test Results: ✅ All 9 E2E Tests PassedTest Breakdown
Console OutputHow to Run Locally$env:DTS_CONNECTION_STRING = "Endpoint=https://your-scheduler.region.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub"
dotnet test test/Extensions.Plugins.E2ETests/Extensions.Plugins.E2ETests.csprojTests skip gracefully when |
The E2E tests require DTS_CONNECTION_STRING (real DTS scheduler) and cannot run in CI. Removed the E2E test project from the solution file so CI's 'dotnet test \' no longer includes it. E2E tests can still be run locally: \ = '...' dotnet test test/Extensions.Plugins.E2ETests/Extensions.Plugins.E2ETests.csproj Tests now return early with output message when connection string is not set. Verified locally: - dotnet build Microsoft.DurableTask.sln --configuration release - dotnet test Microsoft.DurableTask.sln --configuration release (exit 0) - dotnet pack Microsoft.DurableTask.sln --configuration release
| this.output.WriteLine($"Scheduled: {instanceId}"); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| this.output.WriteLine($"Scheduled: {instanceId}"); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| this.output.WriteLine($"Scheduled: {instanceId}"); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| string instanceId = await this.fixture.Client.ScheduleNewOrchestrationInstanceAsync(orchName); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| string instanceId = await this.fixture.Client.ScheduleNewOrchestrationInstanceAsync(orchName); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| string instanceId = await this.fixture.Client.ScheduleNewOrchestrationInstanceAsync(orchName); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| string instanceId = await this.fixture.Client.ScheduleNewOrchestrationInstanceAsync(orchName); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| string instanceId = await this.fixture.Client.ScheduleNewOrchestrationInstanceAsync(orchName); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
| string instanceId = await this.fixture.Client.ScheduleNewOrchestrationInstanceAsync(orchName); | ||
|
|
||
| OrchestrationMetadata result = await this.fixture.Client.WaitForInstanceCompletionAsync( | ||
| instanceId, getInputsAndOutputs: true, new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); |
Summary
Introduces a new
Microsoft.DurableTask.Extensions.Pluginspackage that provides a composable plugin/interceptor system inspired by Temporal's plugin architecture. This enables adding cross-cutting concerns like logging, metrics, authorization, validation, and rate limiting to Durable Task workers without modifying core orchestration or activity logic.Design
The plugin system follows Temporal's design principles:
IDurableTaskPlugin— Named plugin interface containing orchestration and activity interceptorsSimplePlugin— Builder pattern for creating plugins (mirrors Temporal'sSimplePlugin)PluginPipeline— Orchestrates interceptor execution in registration orderPluginOrchestrationWrapperandPluginActivityWrappertransparently decorateITaskOrchestrator/ITaskActivityto invoke interceptorsInterceptor Interfaces
IOrchestrationInterceptor— Hooks into orchestration start, completion, and failure (replay-safe: only fires on non-replay executions)IActivityInterceptor— Hooks into activity start, completion, and failureDI Integration
Extension methods on
IDurableTaskWorkerBuilder:5 Built-in Plugins
ILoggerevents for all orchestration/activity lifecycle eventsMetricsStoreIAuthorizationHandlerchecks before orchestration/activity executionIInputValidatorchecks on inputs before executionFiles Changed
New Package:
src/Extensions/Plugins/IDurableTaskPlugin,IOrchestrationInterceptor,IActivityInterceptorOrchestrationInterceptorContext,ActivityInterceptorContextPluginPipeline,SimplePluginPluginOrchestrationWrapper,PluginActivityWrapperDurableTaskWorkerBuilderExtensions.Plugins.cs,DurableTaskBuiltInPluginExtensions.csBuiltIn/New Tests:
test/Extensions.Plugins.Tests/New Sample:
samples/PluginsSample/Testing