Skip to content

Add plugin system with 5 built-in plugins#664

Draft
YunchuWang wants to merge 5 commits intomainfrom
feature/temporal-inspired-plugins
Draft

Add plugin system with 5 built-in plugins#664
YunchuWang wants to merge 5 commits intomainfrom
feature/temporal-inspired-plugins

Conversation

@YunchuWang
Copy link
Member

Summary

Introduces a new Microsoft.DurableTask.Extensions.Plugins package 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 interceptors
  • SimplePlugin — Builder pattern for creating plugins (mirrors Temporal's SimplePlugin)
  • PluginPipeline — Orchestrates interceptor execution in registration order
  • Wrapper classesPluginOrchestrationWrapper and PluginActivityWrapper transparently decorate ITaskOrchestrator/ITaskActivity to invoke interceptors

Interceptor Interfaces

  • IOrchestrationInterceptor — Hooks into orchestration start, completion, and failure (replay-safe: only fires on non-replay executions)
  • IActivityInterceptor — Hooks into activity start, completion, and failure

DI Integration

Extension methods on IDurableTaskWorkerBuilder:

builder.Services.AddDurableTaskWorker()
    .UsePlugin(myPlugin)           // Add any IDurableTaskPlugin
    .UseLoggingPlugin()            // Built-in convenience methods
    .UseMetricsPlugin(store)
    .UseAuthorizationPlugin(handler)
    .UseValidationPlugin(validators)
    .UseRateLimitingPlugin(opt => { opt.MaxTokens = 100; })
    .UseGrpc();

5 Built-in Plugins

Plugin Description
LoggingPlugin Emits structured ILogger events for all orchestration/activity lifecycle events
MetricsPlugin Thread-safe tracking of execution counts, durations, success/failure rates via MetricsStore
AuthorizationPlugin Runs IAuthorizationHandler checks before orchestration/activity execution
ValidationPlugin Runs IInputValidator checks on inputs before execution
RateLimitingPlugin Token-bucket rate limiting per activity name with configurable burst/refill

Files Changed

New Package: src/Extensions/Plugins/

  • Core interfaces: IDurableTaskPlugin, IOrchestrationInterceptor, IActivityInterceptor
  • Context classes: OrchestrationInterceptorContext, ActivityInterceptorContext
  • Pipeline: PluginPipeline, SimplePlugin
  • Wrappers: PluginOrchestrationWrapper, PluginActivityWrapper
  • DI: DurableTaskWorkerBuilderExtensions.Plugins.cs, DurableTaskBuiltInPluginExtensions.cs
  • 5 Built-in plugins under BuiltIn/

New Tests: test/Extensions.Plugins.Tests/

  • 24 unit tests covering all plugins and the pipeline

New Sample: samples/PluginsSample/

  • Demonstrates all 5 plugins with in-process test host
  • Includes README with usage documentation

Testing

  • All 24 unit tests pass
  • Sample runs successfully with in-process test host, verifying E2E orchestration + plugin demonstrations
  • Build succeeds for all target frameworks (netstandard2.0, net6.0, net8.0, net10.0)

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.
Copilot AI review requested due to automatic review settings March 12, 2026 19:21
@YunchuWang YunchuWang changed the title Add Temporal-inspired plugin system with 5 built-in plugins Add plugin system with 5 built-in plugins Mar 12, 2026
@YunchuWang YunchuWang marked this pull request as draft March 12, 2026 19:26
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.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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), a PluginPipeline, and a SimplePlugin builder.
  • Added built-in plugins: logging, metrics, authorization, validation, and rate limiting (+ DI extension methods).
  • Added a new PluginsSample and a new Extensions.Plugins.Tests test 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.

Comment on lines +3 to +9
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(SrcRoot)Extensions/Plugins/Plugins.csproj" />
</ItemGroup>
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +23
Mock<IOrchestrationInterceptor> interceptor1 = new();
Mock<IOrchestrationInterceptor> interceptor2 = new();

SimplePlugin plugin = SimplePlugin.NewBuilder("test")
.AddOrchestrationInterceptor(interceptor1.Object)
.AddOrchestrationInterceptor(interceptor2.Object)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +47
Mock<IActivityInterceptor> interceptor1 = new();
Mock<IActivityInterceptor> interceptor2 = new();

SimplePlugin plugin = SimplePlugin.NewBuilder("test")
.AddActivityInterceptor(interceptor1.Object)
.AddActivityInterceptor(interceptor2.Object)
.Build();
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +69
Mock<IOrchestrationInterceptor> interceptor = new();

SimplePlugin plugin = SimplePlugin.NewBuilder("test")
.AddOrchestrationInterceptor(interceptor.Object)
.Build();

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +90
Mock<IOrchestrationInterceptor> interceptor = new();
Exception exception = new InvalidOperationException("test error");

SimplePlugin plugin = SimplePlugin.NewBuilder("test")
.AddOrchestrationInterceptor(interceptor.Object)
.Build();

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +20
## 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",
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +10
// 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
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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().

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +37
public async Task ExecuteOrchestrationStartingAsync(OrchestrationInterceptorContext context)
{
foreach (IDurableTaskPlugin plugin in this.plugins)
{
foreach (IOrchestrationInterceptor interceptor in plugin.OrchestrationInterceptors)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +34
this.orchestrationInterceptors = new List<IOrchestrationInterceptor>
{
new ValidationOrchestrationInterceptor(validators),
};
this.activityInterceptors = new List<IActivityInterceptor>
{
new ValidationActivityInterceptor(validators),
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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),

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +71
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}.");
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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}.");

Copilot uses AI. Check for mistakes.
Comment on lines +209 to +215
if (taskName.Name == "SayHello")
{
if (input is not string city || string.IsNullOrWhiteSpace(city))
{
return Task.FromResult(ValidationResult.Failure("City name must be a non-empty string."));
}
}
… 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)
@YunchuWang
Copy link
Member Author

E2E Test Report — Plugin System against Real DTS Scheduler

Test Environment

  • DTS Scheduler: wbtwus1 (West US 3)
  • Task Hub: th1
  • Authentication: DefaultAzureCredential
  • Runtime: .NET 10.0

Test Results: ✅ All 9 E2E Tests Passed

Test Run Successful.
Total tests: 9
     Passed: 9
 Total time: 1.3842 Minutes

Test Breakdown

# Test What It Validates Result
1 MetricsPlugin_TracksExecutionCounts_E2E PluginOrchestrationWrapper + PluginActivityWrapper run during real DTS execution. MetricsStore records started=1, completed=1 for both. ✅ 9s
2 LoggingPlugin_DoesNotBreakExecution_E2E LoggingPlugin interceptors fire without breaking flow. Output: "Hi world" ✅ 9s
3 AuthorizationPlugin_BlocksUnauthorized_E2E DenyAll handler causes UnauthorizedAccessException, orch fails with Authorization denied ✅ 8s
4 AuthorizationPlugin_AllowsAuthorized_E2E AllowAll handler lets execution proceed. Output: "ok:allowed" ✅ 8s
5 ValidationPlugin_RejectsInvalidInput_E2E Empty string rejected by validator, activity fails with Input must be non-empty ✅ 9s
6 ValidationPlugin_AcceptsValidInput_E2E Valid input passes validation. Output: "processed:valid-data" ✅ 8s
7 PluginRegisteredActivity_IsCallable_E2E Activity registered via SimplePlugin.AddTasks() is callable from orch. Output: "plugin:from-orch" ✅ 8s
8 MultiplePlugins_AllInterceptorsFire_E2E 3 plugins (Logging+Metrics+Auth) all fire correctly together ✅ 8s
9 PluginWithTasksAndInterceptors_WorksE2E Plugin with both built-in activity + metrics interceptors — both work ✅ 9s

Console Output

Passed MetricsPlugin_TracksExecutionCounts_E2E [9 s]
  Orch started=1, completed=1 | Activity started=1, completed=1

Passed LoggingPlugin_DoesNotBreakExecution_E2E [9 s]
  Completed with output: "Hi world"

Passed AuthorizationPlugin_BlocksUnauthorized_E2E [8 s]
  Failed as expected: Authorization denied for orchestration 'AuthBlockOrch_...'

Passed AuthorizationPlugin_AllowsAuthorized_E2E [8 s]
  Authorized OK: "ok:allowed"

Passed ValidationPlugin_RejectsInvalidInput_E2E [9 s]
  Validation rejected: Input must be non-empty.

Passed ValidationPlugin_AcceptsValidInput_E2E [8 s]
  Valid OK: "processed:valid-data"

Passed PluginRegisteredActivity_IsCallable_E2E [8 s]
  Plugin activity result: "plugin:from-orch"

Passed MultiplePlugins_AllInterceptorsFire_E2E [8 s]
  Multiple plugins OK. Orch started=1

Passed PluginWithTasksAndInterceptors_WorksE2E [9 s]
  Full plugin OK. Activity started=1

How 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.csproj

Tests skip gracefully when DTS_CONNECTION_STRING is not set — safe for CI.

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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants