diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 0ca1cad17..837a0ba0c 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -93,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduledTasks.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LargePayloadConsoleApp", "samples\LargePayloadConsoleApp\LargePayloadConsoleApp.csproj", "{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExceptionPropertiesSample", "samples\ExceptionPropertiesSample\ExceptionPropertiesSample.csproj", "{7C3ECBCE-BEFB-4982-842E-B654BB6B6285}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureBlobPayloads", "src\Extensions\AzureBlobPayloads\AzureBlobPayloads.csproj", "{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost", "src\InProcessTestHost\InProcessTestHost.csproj", "{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}" @@ -261,6 +263,10 @@ Global {6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Release|Any CPU.Build.0 = Release|Any CPU + {7C3ECBCE-BEFB-4982-842E-B654BB6B6285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C3ECBCE-BEFB-4982-842E-B654BB6B6285}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C3ECBCE-BEFB-4982-842E-B654BB6B6285}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C3ECBCE-BEFB-4982-842E-B654BB6B6285}.Release|Any CPU.Build.0 = Release|Any CPU {FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -313,6 +319,7 @@ Global {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} + {7C3ECBCE-BEFB-4982-842E-B654BB6B6285} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} {FE1DA748-D6DB-E168-BC42-6DBBCEAF229C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/samples/ExceptionPropertiesSample/CustomExceptionPropertiesProvider.cs b/samples/ExceptionPropertiesSample/CustomExceptionPropertiesProvider.cs new file mode 100644 index 000000000..7273d1332 --- /dev/null +++ b/samples/ExceptionPropertiesSample/CustomExceptionPropertiesProvider.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Worker; + +namespace ExceptionPropertiesSample; + +/// +/// Custom exception properties provider that extracts additional properties from exceptions +/// to include in TaskFailureDetails for better diagnostics and error handling. +/// +public class CustomExceptionPropertiesProvider : IExceptionPropertiesProvider +{ + /// + /// Extracts custom properties from exceptions to enrich failure details. + /// + /// The exception to extract properties from. + /// + /// A dictionary of custom properties to include in the FailureDetails, + /// or null if no properties should be added for this exception type. + /// + public IDictionary? GetExceptionProperties(Exception exception) + { + return exception switch + { + BusinessValidationException businessEx => new Dictionary + { + ["ErrorCode"] = businessEx.ErrorCode, + ["StatusCode"] = businessEx.StatusCode, + ["Metadata"] = businessEx.Metadata, + }, + ArgumentOutOfRangeException argEx => new Dictionary + { + ["ParameterName"] = argEx.ParamName ?? string.Empty, + ["ActualValue"] = argEx.ActualValue?.ToString() ?? string.Empty, + }, + ArgumentNullException argNullEx => new Dictionary + { + ["ParameterName"] = argNullEx.ParamName ?? string.Empty, + }, + _ => null // No custom properties for other exception types + }; + } +} + diff --git a/samples/ExceptionPropertiesSample/CustomExceptions.cs b/samples/ExceptionPropertiesSample/CustomExceptions.cs new file mode 100644 index 000000000..19e91f0a6 --- /dev/null +++ b/samples/ExceptionPropertiesSample/CustomExceptions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace ExceptionPropertiesSample; + +/// +/// Custom business exception that includes additional properties for better error diagnostics. +/// +public class BusinessValidationException : Exception +{ + public BusinessValidationException( + string message, + string? errorCode = null, + int? statusCode = null, + Dictionary? metadata = null) + : base(message) + { + this.ErrorCode = errorCode; + this.StatusCode = statusCode; + this.Metadata = metadata ?? new Dictionary(); + } + + /// + /// Gets the error code associated with this validation failure. + /// + public string? ErrorCode { get; } + + /// + /// Gets the HTTP status code that should be returned for this error. + /// + public int? StatusCode { get; } + + /// + /// Gets additional metadata about the validation failure. + /// + public Dictionary Metadata { get; } +} + diff --git a/samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj b/samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj new file mode 100644 index 000000000..d24b2f473 --- /dev/null +++ b/samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj @@ -0,0 +1,28 @@ + + + + Exe + net6.0;net8.0;net10.0 + enable + + + + + + + + + + + + + + + + + + + diff --git a/samples/ExceptionPropertiesSample/Program.cs b/samples/ExceptionPropertiesSample/Program.cs new file mode 100644 index 000000000..6d805ff8b --- /dev/null +++ b/samples/ExceptionPropertiesSample/Program.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// This sample demonstrates how to use IExceptionPropertiesProvider to enrich +// TaskFailureDetails with custom exception properties for better diagnostics. + +using ExceptionPropertiesSample; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.AzureManaged; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.AzureManaged; +using Microsoft.Extensions.Hosting; + +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + +string? schedulerConnectionString = builder.Configuration.GetValue("DURABLE_TASK_SCHEDULER_CONNECTION_STRING"); +bool useScheduler = !string.IsNullOrWhiteSpace(schedulerConnectionString); + +// Register the durable task client +if (useScheduler) +{ + builder.Services.AddDurableTaskClient(clientBuilder => clientBuilder.UseDurableTaskScheduler(schedulerConnectionString!)); +} +else +{ + builder.Services.AddDurableTaskClient().UseGrpc(); +} + +// Register the durable task worker with custom exception properties provider +if (useScheduler) +{ + builder.Services.AddDurableTaskWorker(workerBuilder => + { + workerBuilder.AddTasks(tasks => + { + tasks.AddOrchestrator(); + tasks.AddActivity(); + }); + + workerBuilder.UseDurableTaskScheduler(schedulerConnectionString!); + }); +} +else +{ + builder.Services.AddDurableTaskWorker() + .AddTasks(tasks => + { + tasks.AddOrchestrator(); + tasks.AddActivity(); + }) + .UseGrpc(); +} + +// Register the custom exception properties provider +// This will automatically extract custom properties from exceptions and include them in TaskFailureDetails +builder.Services.AddSingleton(); + +IHost host = builder.Build(); + +// Start the worker +await host.StartAsync(); + +// Get the client to start orchestrations +DurableTaskClient client = host.Services.GetRequiredService(); + +Console.WriteLine("Exception Properties Sample"); +Console.WriteLine("==========================="); +Console.WriteLine(); + +Console.WriteLine(useScheduler + ? "Configured to use Durable Task Scheduler (DTS)." + : "Configured to use local gRPC. (Set DURABLE_TASK_SCHEDULER_CONNECTION_STRING to use DTS.)"); +Console.WriteLine(); + +// Test case 1: Valid input (should succeed) +Console.WriteLine("Test 1: Valid input"); +string instanceId1 = await client.ScheduleNewOrchestrationInstanceAsync( + "ValidationOrchestration", + input: "Hello World"); + +OrchestrationMetadata result1 = await client.WaitForInstanceCompletionAsync( + instanceId1, + getInputsAndOutputs: true); + +if (result1.RuntimeStatus == OrchestrationRuntimeStatus.Completed) +{ + Console.WriteLine($"✓ Orchestration completed successfully"); + Console.WriteLine($" Output: {result1.ReadOutputAs()}"); +} +Console.WriteLine(); + +// Test case 2: Empty input (should fail with custom properties) +Console.WriteLine("Test 2: Empty input (should fail)"); +string instanceId2 = await client.ScheduleNewOrchestrationInstanceAsync( + "ValidationOrchestration", + input: string.Empty); + +OrchestrationMetadata result2 = await client.WaitForInstanceCompletionAsync( + instanceId2, + getInputsAndOutputs: true); + +if (result2.RuntimeStatus == OrchestrationRuntimeStatus.Failed && result2.FailureDetails != null) +{ + Console.WriteLine($"✗ Orchestration failed as expected"); + Console.WriteLine($" Error Type: {result2.FailureDetails.ErrorType}"); + Console.WriteLine($" Error Message: {result2.FailureDetails.ErrorMessage}"); + + // Display custom properties that were extracted by IExceptionPropertiesProvider + if (result2.FailureDetails.Properties != null && result2.FailureDetails.Properties.Count > 0) + { + Console.WriteLine($" Custom Properties:"); + foreach (var property in result2.FailureDetails.Properties) + { + Console.WriteLine($" - {property.Key}: {property.Value}"); + } + } +} +Console.WriteLine(); + +// Test case 3: Short input (should fail with different custom properties) +Console.WriteLine("Test 3: Short input (should fail)"); +string instanceId3 = await client.ScheduleNewOrchestrationInstanceAsync( + "ValidationOrchestration", + input: "Hi"); + +OrchestrationMetadata result3 = await client.WaitForInstanceCompletionAsync( + instanceId3, + getInputsAndOutputs: true); + +if (result3.RuntimeStatus == OrchestrationRuntimeStatus.Failed && result3.FailureDetails != null) +{ + Console.WriteLine($"✗ Orchestration failed as expected"); + Console.WriteLine($" Error Type: {result3.FailureDetails.ErrorType}"); + Console.WriteLine($" Error Message: {result3.FailureDetails.ErrorMessage}"); + + // Display custom properties + if (result3.FailureDetails.Properties != null && result3.FailureDetails.Properties.Count > 0) + { + Console.WriteLine($" Custom Properties:"); + foreach (var property in result3.FailureDetails.Properties) + { + Console.WriteLine($" - {property.Key}: {property.Value}"); + } + } +} +Console.WriteLine(); + +Console.WriteLine("Sample completed. Press any key to exit..."); +Console.ReadKey(); + +await host.StopAsync(); + diff --git a/samples/ExceptionPropertiesSample/README.md b/samples/ExceptionPropertiesSample/README.md new file mode 100644 index 000000000..a9ff13db9 --- /dev/null +++ b/samples/ExceptionPropertiesSample/README.md @@ -0,0 +1,85 @@ +# Exception Properties Sample + +This sample demonstrates how to use `IExceptionPropertiesProvider` to enrich `TaskFailureDetails` with custom exception properties for better diagnostics and error handling. + +## Overview + +When orchestrations or activities throw exceptions, the Durable Task framework captures failure details. By implementing `IExceptionPropertiesProvider`, you can extract custom properties from exceptions and include them in the `TaskFailureDetails`, making it easier to diagnose issues and handle errors programmatically. + +## Key Concepts + +1. **Custom Exception with Properties**: Create exceptions that carry additional context (error codes, metadata, etc.) +2. **IExceptionPropertiesProvider**: Implement this interface to extract custom properties from exceptions +3. **Automatic Property Extraction**: The framework automatically uses your provider when converting exceptions to `TaskFailureDetails` +4. **Retrieving Failure Details**: Use the durable client to retrieve orchestration status and access the enriched failure details + +## What This Sample Does + +1. Defines a `BusinessValidationException` with custom properties (ErrorCode, StatusCode, Metadata) +2. Implements `CustomExceptionPropertiesProvider` that extracts these properties from exceptions +3. Creates a validation orchestration and activity that throws the custom exception +4. Demonstrates how to retrieve and display failure details with custom properties using the durable client + +## Running the Sample + +This sample can run against either: + +1. **Durable Task Scheduler (DTS)** (recommended): set the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` environment variable. +2. **Local gRPC endpoint**: if the env var is not set, the sample uses the default local gRPC configuration. + +### DTS + +Set `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` and run the sample. + +```cmd +set DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://...;TaskHub=...;Authentication=...; +dotnet run --project samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj +``` + +```bash +export DURABLE_TASK_SCHEDULER_CONNECTION_STRING="Endpoint=https://...;TaskHub=...;Authentication=...;" +dotnet run --project samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj +``` + +## Expected Output + +The sample runs three test cases: +1. **Valid input**: Orchestration completes successfully +2. **Empty input**: Orchestration fails with custom properties (ErrorCode, StatusCode, Metadata) +3. **Short input**: Orchestration fails with different custom properties + +For failed orchestrations, you'll see the custom properties extracted by the `IExceptionPropertiesProvider` displayed in the console. + +## Code Structure + +- `CustomExceptions.cs`: Defines the `BusinessValidationException` with custom properties +- `CustomExceptionPropertiesProvider.cs`: Implements `IExceptionPropertiesProvider` to extract properties +- `Tasks.cs`: Contains the orchestration and activity that throw custom exceptions +- `Program.cs`: Sets up the worker, registers the provider, and demonstrates retrieving failure details + +## Key Code Snippet + +```csharp +// Register the custom exception properties provider +builder.Services.AddSingleton(); + +// Retrieve failure details with custom properties +OrchestrationMetadata result = await client.WaitForInstanceCompletionAsync( + instanceId, + getInputsAndOutputs: true); // Important: must be true to get failure details + +if (result.FailureDetails?.Properties != null) +{ + foreach (var property in result.FailureDetails.Properties) + { + Console.WriteLine($"{property.Key}: {property.Value}"); + } +} +``` + +## Notes + +- The `getInputsAndOutputs` parameter must be `true` when calling `GetInstanceAsync` or `WaitForInstanceCompletionAsync` to retrieve failure details +- Custom properties are only included if the orchestration is in a `Failed` state +- The `IExceptionPropertiesProvider` is called automatically by the framework when exceptions are caught + diff --git a/samples/ExceptionPropertiesSample/Tasks.cs b/samples/ExceptionPropertiesSample/Tasks.cs new file mode 100644 index 000000000..50c7d289d --- /dev/null +++ b/samples/ExceptionPropertiesSample/Tasks.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; + +namespace ExceptionPropertiesSample; + +/// +/// Orchestration that demonstrates custom exception properties in failure details. +/// +[DurableTask("ValidationOrchestration")] +public class ValidationOrchestration : TaskOrchestrator +{ + public override async Task RunAsync(TaskOrchestrationContext context, string input) + { + // Call an activity that may throw a custom exception with properties + string result = await context.CallActivityAsync("ValidateInput", input); + return result; + } +} + +/// +/// Activity that validates input and throws a custom exception with properties on failure. +/// +[DurableTask("ValidateInput")] +public class ValidateInputActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string input) + { + // Simulate validation logic + if (string.IsNullOrWhiteSpace(input)) + { + throw new BusinessValidationException( + message: "Input validation failed: input cannot be empty", + errorCode: "VALIDATION_001", + statusCode: 400, + metadata: new Dictionary + { + ["Field"] = "input", + ["ValidationRule"] = "Required", + ["Timestamp"] = DateTime.UtcNow, + }); + } + + if (input.Length < 3) + { + throw new BusinessValidationException( + message: $"Input validation failed: input must be at least 3 characters (received {input.Length})", + errorCode: "VALIDATION_002", + statusCode: 400, + metadata: new Dictionary + { + ["Field"] = "input", + ["ValidationRule"] = "MinLength", + ["MinLength"] = 3, + ["ActualLength"] = input.Length, + }); + } + + // Validation passed + return Task.FromResult($"Validation successful for input: {input}"); + } +} +