Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Microsoft.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,39 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportHistory.Tests", "test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedTracingSample", "samples\DistributedTracingSample\DistributedTracingSample.csproj", "{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{21303FBF-2A2B-17C2-D2DF-3E924022E940}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugins", "src\Extensions\Plugins\Plugins.csproj", "{464EF328-1A43-417C-BC9D-C1F808D2C3B8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.Plugins.Tests", "test\Extensions.Plugins.Tests\Extensions.Plugins.Tests.csproj", "{09D76001-410E-4308-9156-01A9E7F5400B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginsSample", "samples\PluginsSample\PluginsSample.csproj", "{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x64.ActiveCfg = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x64.Build.0 = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x86.ActiveCfg = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x86.Build.0 = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|Any CPU.Build.0 = Release|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x64.ActiveCfg = Release|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x64.Build.0 = Release|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x86.ActiveCfg = Release|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x86.Build.0 = Release|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x64.ActiveCfg = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x64.Build.0 = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x86.ActiveCfg = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x86.Build.0 = Debug|Any CPU
Expand Down Expand Up @@ -660,6 +681,42 @@ Global
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}.Release|x64.Build.0 = Release|Any CPU
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}.Release|x86.ActiveCfg = Release|Any CPU
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}.Release|x86.Build.0 = Release|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Debug|x64.ActiveCfg = Debug|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Debug|x64.Build.0 = Debug|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Debug|x86.ActiveCfg = Debug|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Debug|x86.Build.0 = Debug|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Release|Any CPU.Build.0 = Release|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Release|x64.ActiveCfg = Release|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Release|x64.Build.0 = Release|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Release|x86.ActiveCfg = Release|Any CPU
{464EF328-1A43-417C-BC9D-C1F808D2C3B8}.Release|x86.Build.0 = Release|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Debug|x64.ActiveCfg = Debug|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Debug|x64.Build.0 = Debug|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Debug|x86.ActiveCfg = Debug|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Debug|x86.Build.0 = Debug|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Release|Any CPU.Build.0 = Release|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Release|x64.ActiveCfg = Release|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Release|x64.Build.0 = Release|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Release|x86.ActiveCfg = Release|Any CPU
{09D76001-410E-4308-9156-01A9E7F5400B}.Release|x86.Build.0 = Release|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Debug|x64.ActiveCfg = Debug|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Debug|x64.Build.0 = Debug|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Debug|x86.ActiveCfg = Debug|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Debug|x86.Build.0 = Debug|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Release|Any CPU.Build.0 = Release|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Release|x64.ActiveCfg = Release|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Release|x64.Build.0 = Release|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Release|x86.ActiveCfg = Release|Any CPU
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -715,6 +772,10 @@ Global
{354CE69B-78DB-9B29-C67E-0DBB862C7A65} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
{05C9EBA6-7221-D458-47D6-DA457C2F893B} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{21303FBF-2A2B-17C2-D2DF-3E924022E940} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
{464EF328-1A43-417C-BC9D-C1F808D2C3B8} = {21303FBF-2A2B-17C2-D2DF-3E924022E940}
{09D76001-410E-4308-9156-01A9E7F5400B} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{BC7110D2-21D6-4A73-88FD-D4D2A2A670DD} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
Expand Down
22 changes: 22 additions & 0 deletions samples/PluginsSample/PluginsSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>

<ItemGroup>
<!-- Using p2p references so we can show latest changes in samples. -->
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Extensions/Plugins/Plugins.csproj" />
<ProjectReference Include="$(SrcRoot)InProcessTestHost/InProcessTestHost.csproj" />
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
220 changes: 220 additions & 0 deletions samples/PluginsSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// This sample demonstrates the Durable Task Plugin system, which is inspired by Temporal's
// 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
Comment on lines +5 to +10
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.

using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Plugins.BuiltIn;
using Microsoft.DurableTask.Testing;
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

// Create a shared metrics store so we can read metrics after the orchestration completes.
MetricsStore metricsStore = new();

// Use the in-process test host (no external sidecar needed for demonstration).
// In production, replace with .UseGrpc() or .UseDurableTaskScheduler().
await using DurableTaskTestHost testHost = await DurableTaskTestHost.StartAsync(
registry =>
{
// Register orchestration and activities.
registry.AddOrchestratorFunc("GreetingOrchestration", async context =>
{
List<string> greetings = new()
{
await context.CallActivityAsync<string>("SayHello", "Tokyo"),
await context.CallActivityAsync<string>("SayHello", "London"),
await context.CallActivityAsync<string>("SayHello", "Seattle"),
};

return greetings;
});

registry.AddActivityFunc<string, string>("SayHello", (context, city) =>
{
return $"Hello, {city}!";
});
});

DurableTaskClient client = testHost.Client;

// Schedule a new orchestration instance.
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("GreetingOrchestration");
Console.WriteLine($"Started orchestration: {instanceId}");

// Wait for the orchestration to complete.
OrchestrationMetadata result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);
Console.WriteLine($"Orchestration completed with status: {result.RuntimeStatus}");
Console.WriteLine($"Output: {result.SerializedOutput}");

// --- Demonstrate Plugin APIs ---
Console.WriteLine("\n=== Plugin Demonstrations ===");

// Demo 1: LoggingPlugin
Console.WriteLine("\n--- 1. LoggingPlugin ---");
Console.WriteLine("The LoggingPlugin emits structured ILogger events for lifecycle events.");
Console.WriteLine("It would be registered on a worker builder like this:");
Console.WriteLine(" builder.Services.AddDurableTaskWorker().UseLoggingPlugin().UseGrpc();");

ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));
LoggingPlugin loggingPlugin = new(loggerFactory);
Console.WriteLine($"Plugin name: {loggingPlugin.Name}");
Console.WriteLine($"Orchestration interceptors: {loggingPlugin.OrchestrationInterceptors.Count}");
Console.WriteLine($"Activity interceptors: {loggingPlugin.ActivityInterceptors.Count}");

// Demo 2: MetricsPlugin
Console.WriteLine("\n--- 2. MetricsPlugin ---");
MetricsPlugin metricsPlugin = new(metricsStore);

// Simulate lifecycle events
var orchCtx = new Microsoft.DurableTask.Plugins.OrchestrationInterceptorContext("Demo", "test-1", false, null);
await metricsPlugin.OrchestrationInterceptors[0].OnOrchestrationStartingAsync(orchCtx);
await metricsPlugin.OrchestrationInterceptors[0].OnOrchestrationCompletedAsync(orchCtx, "done");

var actCtx = new Microsoft.DurableTask.Plugins.ActivityInterceptorContext("DemoActivity", "test-1", "input");
await metricsPlugin.ActivityInterceptors[0].OnActivityStartingAsync(actCtx);
await metricsPlugin.ActivityInterceptors[0].OnActivityCompletedAsync(actCtx, "result");
await metricsPlugin.ActivityInterceptors[0].OnActivityStartingAsync(actCtx);
await metricsPlugin.ActivityInterceptors[0].OnActivityFailedAsync(actCtx, new Exception("test failure"));

Console.WriteLine("Orchestration metrics:");
foreach (var (name, metrics) in metricsStore.GetAllOrchestrationMetrics())
{
Console.WriteLine($" '{name}': Started={metrics.Started}, Completed={metrics.Completed}, Failed={metrics.Failed}");
}

Console.WriteLine("Activity metrics:");
foreach (var (name, metrics) in metricsStore.GetAllActivityMetrics())
{
Console.WriteLine($" '{name}': Started={metrics.Started}, Completed={metrics.Completed}, Failed={metrics.Failed}");
}

// Demo 3: AuthorizationPlugin
Console.WriteLine("\n--- 3. AuthorizationPlugin ---");
AuthorizationPlugin authPlugin = new(new AllowAllAuthorizationHandler());
var authOrcCtx = new Microsoft.DurableTask.Plugins.OrchestrationInterceptorContext("SecureOrch", "secure-1", false, null);
await authPlugin.OrchestrationInterceptors[0].OnOrchestrationStartingAsync(authOrcCtx);

// Demo 4: ValidationPlugin
Console.WriteLine("\n--- 4. ValidationPlugin ---");
ValidationPlugin validationPlugin = new(new CityNameValidator());
var validCtx = new Microsoft.DurableTask.Plugins.ActivityInterceptorContext("SayHello", "val-1", "Tokyo");
await validationPlugin.ActivityInterceptors[0].OnActivityStartingAsync(validCtx);
Console.WriteLine(" Validation passed for input 'Tokyo'");

try
{
var invalidCtx = new Microsoft.DurableTask.Plugins.ActivityInterceptorContext("SayHello", "val-1", "");
await validationPlugin.ActivityInterceptors[0].OnActivityStartingAsync(invalidCtx);
}
catch (ArgumentException ex)
{
Console.WriteLine($" Validation correctly rejected empty input: {ex.Message}");
}

// Demo 5: RateLimitingPlugin
Console.WriteLine("\n--- 5. RateLimitingPlugin ---");
RateLimitingPlugin rateLimitPlugin = new(new RateLimitingOptions
{
MaxTokens = 3,
RefillRate = 0,
RefillInterval = TimeSpan.FromHours(1),
});

var rlCtx = new Microsoft.DurableTask.Plugins.ActivityInterceptorContext("LimitedAction", "rl-1", "data");
int allowed = 0;
int denied = 0;
for (int i = 0; i < 5; i++)
{
try
{
await rateLimitPlugin.ActivityInterceptors[0].OnActivityStartingAsync(rlCtx);
allowed++;
}
catch (RateLimitExceededException)
{
denied++;
}
}

Console.WriteLine($" Rate limit (max 3): Allowed={allowed}, Denied={denied}");

// Demo: SimplePlugin builder with built-in activities
Console.WriteLine("\n--- SimplePlugin Builder (with built-in activities) ---");
Console.WriteLine("Plugins can provide reusable activities that auto-register when added to a worker.");
Console.WriteLine("Example: A 'StringUtils' plugin that ships pre-built string activities.");

// This is how a plugin author would package reusable activities.
// Users just call .UsePlugin(stringUtilsPlugin) and the activities become available.
var stringUtilsPlugin = Microsoft.DurableTask.Plugins.SimplePlugin.NewBuilder("MyOrg.StringUtils")
.AddTasks(registry =>
{
registry.AddActivityFunc<string, string>("StringUtils.ToUpper", (ctx, input) => input.ToUpperInvariant());
registry.AddActivityFunc<string, string>("StringUtils.Reverse", (ctx, input) =>
new string(input.Reverse().ToArray()));
registry.AddActivityFunc<string, int>("StringUtils.WordCount", (ctx, input) =>
input.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length);
})
.AddOrchestrationInterceptor(loggingPlugin.OrchestrationInterceptors[0])
.Build();

// Verify the plugin registers its tasks into a registry
DurableTaskRegistry testRegistry = new();
stringUtilsPlugin.RegisterTasks(testRegistry);
Console.WriteLine($" Plugin '{stringUtilsPlugin.Name}' registered activities into the worker.");
Console.WriteLine($" Orchestration interceptors: {stringUtilsPlugin.OrchestrationInterceptors.Count}");
Console.WriteLine("");
Console.WriteLine(" Usage in production:");
Console.WriteLine(" builder.Services.AddDurableTaskWorker()");
Console.WriteLine(" .UsePlugin(stringUtilsPlugin) // auto-registers StringUtils.* activities");
Console.WriteLine(" .UseGrpc();");
Console.WriteLine("");
Console.WriteLine(" Then in an orchestration:");
Console.WriteLine(" string upper = await context.CallActivityAsync<string>(\"StringUtils.ToUpper\", \"hello\");");

Console.WriteLine("\n=== All plugin demonstrations completed successfully! ===");

// --- Helper classes for the sample ---

/// <summary>
/// A simple authorization handler that allows all tasks to execute.
/// In a real application, this would check user claims, roles, or other policies.
/// </summary>
sealed class AllowAllAuthorizationHandler : IAuthorizationHandler
{
public Task<bool> AuthorizeAsync(AuthorizationContext context)
{
Console.WriteLine($" [Auth] Authorized {context.TargetType} '{context.Name}' for instance '{context.InstanceId}'");
return Task.FromResult(true);
}
}

/// <summary>
/// A validator that ensures city names passed to the SayHello activity are non-empty strings.
/// </summary>
sealed class CityNameValidator : IInputValidator
{
public Task<ValidationResult> ValidateAsync(TaskName taskName, object? input)
{
// Only validate the SayHello activity.
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."));
}
}
Comment on lines +209 to +215

return Task.FromResult(ValidationResult.Success);
}
}

Loading
Loading