Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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; }
}

26 changes: 26 additions & 0 deletions samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<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)Worker/Grpc/Worker.Grpc.csproj" />
</ItemGroup>

</Project>

Comment thread
YunchuWang marked this conversation as resolved.
119 changes: 119 additions & 0 deletions samples/ExceptionPropertiesSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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.DurableTask.Client;
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Register the durable task client
builder.Services.AddDurableTaskClient().UseGrpc();

// Register the durable task worker with custom exception properties provider
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();

// 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)
Comment thread
YunchuWang marked this conversation as resolved.
{
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)
Comment thread
YunchuWang marked this conversation as resolved.
{
Console.WriteLine($" - {property.Key}: {property.Value}");
}
}
}
Console.WriteLine();

Console.WriteLine("Sample completed. Press any key to exit...");
Console.ReadKey();

await host.StopAsync();

70 changes: 70 additions & 0 deletions samples/ExceptionPropertiesSample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 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

```bash
Comment thread
YunchuWang marked this conversation as resolved.
dotnet run --project ExceptionPropertiesSample
Comment thread
YunchuWang marked this conversation as resolved.
Outdated
```

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

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

using Microsoft.DurableTask;

namespace ExceptionPropertiesSample;

/// <summary>
/// Orchestration that demonstrates custom exception properties in failure details.
/// </summary>
[DurableTask("ValidationOrchestration")]
public class ValidationOrchestration : TaskOrchestrator<string, string>
{
public override async Task<string> RunAsync(TaskOrchestrationContext context, string input)
{
// Call an activity that may throw a custom exception with properties
try
{
string result = await context.CallActivityAsync<string>("ValidateInput", input);
return result;
}
catch (TaskFailedException ex)
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
{
// The failure details will include custom properties from IExceptionPropertiesProvider
// These properties are automatically extracted and included in the TaskFailureDetails
throw;
}
Comment thread
YunchuWang marked this conversation as resolved.
Outdated
}
}

/// <summary>
/// Activity that validates input and throws a custom exception with properties on failure.
/// </summary>
[DurableTask("ValidateInput")]
public class ValidateInputActivity : TaskActivity<string, string>
{
public override Task<string> 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<string, object?>
{
["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<string, object?>
{
["Field"] = "input",
["ValidationRule"] = "MinLength",
["MinLength"] = 3,
["ActualLength"] = input.Length,
});
}

// Validation passed
return Task.FromResult($"Validation successful for input: {input}");
}
}

Loading