Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Microsoft.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.DurableTask.Worker;

namespace ExceptionPropertiesSample;

/// <summary>
/// Custom exception properties provider that extracts additional properties from exceptions
/// to include in TaskFailureDetails for better diagnostics and error handling.
/// </summary>
public class CustomExceptionPropertiesProvider : IExceptionPropertiesProvider
{
/// <summary>
/// Extracts custom properties from exceptions to enrich failure details.
/// </summary>
/// <param name="exception">The exception to extract properties from.</param>
/// <returns>
/// A dictionary of custom properties to include in the FailureDetails,
/// or null if no properties should be added for this exception type.
/// </returns>
public IDictionary<string, object?>? GetExceptionProperties(Exception exception)
{
return exception switch
{
BusinessValidationException businessEx => new Dictionary<string, object?>
{
["ErrorCode"] = businessEx.ErrorCode,
["StatusCode"] = businessEx.StatusCode,
["Metadata"] = businessEx.Metadata,
},
ArgumentOutOfRangeException argEx => new Dictionary<string, object?>
{
["ParameterName"] = argEx.ParamName ?? string.Empty,
["ActualValue"] = argEx.ActualValue?.ToString() ?? string.Empty,
},
ArgumentNullException argNullEx => new Dictionary<string, object?>
{
["ParameterName"] = argNullEx.ParamName ?? string.Empty,
},
_ => null // No custom properties for other exception types
};
}
}

38 changes: 38 additions & 0 deletions samples/ExceptionPropertiesSample/CustomExceptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace ExceptionPropertiesSample;

/// <summary>
/// Custom business exception that includes additional properties for better error diagnostics.
/// </summary>
public class BusinessValidationException : Exception
{
public BusinessValidationException(
string message,
string? errorCode = null,
int? statusCode = null,
Dictionary<string, object?>? metadata = null)
: base(message)
{
this.ErrorCode = errorCode;
this.StatusCode = statusCode;
this.Metadata = metadata ?? new Dictionary<string, object?>();
}

/// <summary>
/// Gets the error code associated with this validation failure.
/// </summary>
public string? ErrorCode { get; }

/// <summary>
/// Gets the HTTP status code that should be returned for this error.
/// </summary>
public int? StatusCode { get; }

/// <summary>
/// Gets additional metadata about the validation failure.
/// </summary>
public Dictionary<string, object?> Metadata { get; }
}

28 changes: 28 additions & 0 deletions samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

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

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

<!-- Real projects would use package references -->
<!--
<PackageReference Include="Microsoft.DurableTask.Client.Grpc" Version="1.5.0" />
<PackageReference Include="Microsoft.DurableTask.Worker.Grpc" Version="1.5.0" />
-->
</ItemGroup>

<ItemGroup>
<!-- Using p2p references so we can show latest changes in samples. -->
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Client/AzureManaged/Client.AzureManaged.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" />
</ItemGroup>

</Project>

154 changes: 154 additions & 0 deletions samples/ExceptionPropertiesSample/Program.cs
Original file line number Diff line number Diff line change
@@ -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<string>("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<ValidationOrchestration>();
tasks.AddActivity<ValidateInputActivity>();
});

workerBuilder.UseDurableTaskScheduler(schedulerConnectionString!);
});
}
else
{
builder.Services.AddDurableTaskWorker()
.AddTasks(tasks =>
{
tasks.AddOrchestrator<ValidationOrchestration>();
tasks.AddActivity<ValidateInputActivity>();
})
.UseGrpc();
}

// Register the custom exception properties provider
// This will automatically extract custom properties from exceptions and include them in TaskFailureDetails
builder.Services.AddSingleton<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();

IHost host = builder.Build();

// Start the worker
await host.StartAsync();

// Get the client to start orchestrations
DurableTaskClient client = host.Services.GetRequiredService<DurableTaskClient>();

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

85 changes: 85 additions & 0 deletions samples/ExceptionPropertiesSample/README.md
Original file line number Diff line number Diff line change
@@ -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<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();

// 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

Loading
Loading