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}");
+ }
+}
+