Skip to content

Commit 19a8d8d

Browse files
committed
Add ExceptionPropertiesSample demonstrating IExceptionPropertiesProvider usage
1 parent f3a7fca commit 19a8d8d

File tree

6 files changed

+371
-0
lines changed

6 files changed

+371
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.DurableTask.Worker;
5+
6+
namespace ExceptionPropertiesSample;
7+
8+
/// <summary>
9+
/// Custom exception properties provider that extracts additional properties from exceptions
10+
/// to include in TaskFailureDetails for better diagnostics and error handling.
11+
/// </summary>
12+
public class CustomExceptionPropertiesProvider : IExceptionPropertiesProvider
13+
{
14+
/// <summary>
15+
/// Extracts custom properties from exceptions to enrich failure details.
16+
/// </summary>
17+
/// <param name="exception">The exception to extract properties from.</param>
18+
/// <returns>
19+
/// A dictionary of custom properties to include in the FailureDetails,
20+
/// or null if no properties should be added for this exception type.
21+
/// </returns>
22+
public IDictionary<string, object?>? GetExceptionProperties(Exception exception)
23+
{
24+
return exception switch
25+
{
26+
BusinessValidationException businessEx => new Dictionary<string, object?>
27+
{
28+
["ErrorCode"] = businessEx.ErrorCode,
29+
["StatusCode"] = businessEx.StatusCode,
30+
["Metadata"] = businessEx.Metadata,
31+
},
32+
ArgumentOutOfRangeException argEx => new Dictionary<string, object?>
33+
{
34+
["ParameterName"] = argEx.ParamName ?? string.Empty,
35+
["ActualValue"] = argEx.ActualValue?.ToString() ?? string.Empty,
36+
},
37+
ArgumentNullException argNullEx => new Dictionary<string, object?>
38+
{
39+
["ParameterName"] = argNullEx.ParamName ?? string.Empty,
40+
},
41+
_ => null // No custom properties for other exception types
42+
};
43+
}
44+
}
45+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace ExceptionPropertiesSample;
5+
6+
/// <summary>
7+
/// Custom business exception that includes additional properties for better error diagnostics.
8+
/// </summary>
9+
public class BusinessValidationException : Exception
10+
{
11+
public BusinessValidationException(
12+
string message,
13+
string? errorCode = null,
14+
int? statusCode = null,
15+
Dictionary<string, object?>? metadata = null)
16+
: base(message)
17+
{
18+
this.ErrorCode = errorCode;
19+
this.StatusCode = statusCode;
20+
this.Metadata = metadata ?? new Dictionary<string, object?>();
21+
}
22+
23+
/// <summary>
24+
/// Gets the error code associated with this validation failure.
25+
/// </summary>
26+
public string? ErrorCode { get; }
27+
28+
/// <summary>
29+
/// Gets the HTTP status code that should be returned for this error.
30+
/// </summary>
31+
public int? StatusCode { get; }
32+
33+
/// <summary>
34+
/// Gets additional metadata about the validation failure.
35+
/// </summary>
36+
public Dictionary<string, object?> Metadata { get; }
37+
}
38+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Hosting" />
11+
12+
<!-- Real projects would use package references -->
13+
<!--
14+
<PackageReference Include="Microsoft.DurableTask.Client.Grpc" Version="1.5.0" />
15+
<PackageReference Include="Microsoft.DurableTask.Worker.Grpc" Version="1.5.0" />
16+
-->
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<!-- Using p2p references so we can show latest changes in samples. -->
21+
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
22+
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
23+
</ItemGroup>
24+
25+
</Project>
26+
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// This sample demonstrates how to use IExceptionPropertiesProvider to enrich
5+
// TaskFailureDetails with custom exception properties for better diagnostics.
6+
7+
using ExceptionPropertiesSample;
8+
using Microsoft.DurableTask.Client;
9+
using Microsoft.DurableTask.Worker;
10+
using Microsoft.Extensions.Hosting;
11+
12+
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
13+
14+
// Register the durable task client
15+
builder.Services.AddDurableTaskClient().UseGrpc();
16+
17+
// Register the durable task worker with custom exception properties provider
18+
builder.Services.AddDurableTaskWorker()
19+
.AddTasks(tasks =>
20+
{
21+
tasks.AddOrchestrator<ValidationOrchestration>();
22+
tasks.AddActivity<ValidateInputActivity>();
23+
})
24+
.UseGrpc();
25+
26+
// Register the custom exception properties provider
27+
// This will automatically extract custom properties from exceptions and include them in TaskFailureDetails
28+
builder.Services.AddSingleton<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();
29+
30+
IHost host = builder.Build();
31+
32+
// Start the worker
33+
await host.StartAsync();
34+
35+
// Get the client to start orchestrations
36+
DurableTaskClient client = host.Services.GetRequiredService<DurableTaskClient>();
37+
38+
Console.WriteLine("Exception Properties Sample");
39+
Console.WriteLine("===========================");
40+
Console.WriteLine();
41+
42+
// Test case 1: Valid input (should succeed)
43+
Console.WriteLine("Test 1: Valid input");
44+
string instanceId1 = await client.ScheduleNewOrchestrationInstanceAsync(
45+
"ValidationOrchestration",
46+
input: "Hello World");
47+
48+
OrchestrationMetadata result1 = await client.WaitForInstanceCompletionAsync(
49+
instanceId1,
50+
getInputsAndOutputs: true);
51+
52+
if (result1.RuntimeStatus == OrchestrationRuntimeStatus.Completed)
53+
{
54+
Console.WriteLine($"✓ Orchestration completed successfully");
55+
Console.WriteLine($" Output: {result1.ReadOutputAs<string>()}");
56+
}
57+
Console.WriteLine();
58+
59+
// Test case 2: Empty input (should fail with custom properties)
60+
Console.WriteLine("Test 2: Empty input (should fail)");
61+
string instanceId2 = await client.ScheduleNewOrchestrationInstanceAsync(
62+
"ValidationOrchestration",
63+
input: string.Empty);
64+
65+
OrchestrationMetadata result2 = await client.WaitForInstanceCompletionAsync(
66+
instanceId2,
67+
getInputsAndOutputs: true);
68+
69+
if (result2.RuntimeStatus == OrchestrationRuntimeStatus.Failed && result2.FailureDetails != null)
70+
{
71+
Console.WriteLine($"✗ Orchestration failed as expected");
72+
Console.WriteLine($" Error Type: {result2.FailureDetails.ErrorType}");
73+
Console.WriteLine($" Error Message: {result2.FailureDetails.ErrorMessage}");
74+
75+
// Display custom properties that were extracted by IExceptionPropertiesProvider
76+
if (result2.FailureDetails.Properties != null && result2.FailureDetails.Properties.Count > 0)
77+
{
78+
Console.WriteLine($" Custom Properties:");
79+
foreach (var property in result2.FailureDetails.Properties)
80+
{
81+
Console.WriteLine($" - {property.Key}: {property.Value}");
82+
}
83+
}
84+
}
85+
Console.WriteLine();
86+
87+
// Test case 3: Short input (should fail with different custom properties)
88+
Console.WriteLine("Test 3: Short input (should fail)");
89+
string instanceId3 = await client.ScheduleNewOrchestrationInstanceAsync(
90+
"ValidationOrchestration",
91+
input: "Hi");
92+
93+
OrchestrationMetadata result3 = await client.WaitForInstanceCompletionAsync(
94+
instanceId3,
95+
getInputsAndOutputs: true);
96+
97+
if (result3.RuntimeStatus == OrchestrationRuntimeStatus.Failed && result3.FailureDetails != null)
98+
{
99+
Console.WriteLine($"✗ Orchestration failed as expected");
100+
Console.WriteLine($" Error Type: {result3.FailureDetails.ErrorType}");
101+
Console.WriteLine($" Error Message: {result3.FailureDetails.ErrorMessage}");
102+
103+
// Display custom properties
104+
if (result3.FailureDetails.Properties != null && result3.FailureDetails.Properties.Count > 0)
105+
{
106+
Console.WriteLine($" Custom Properties:");
107+
foreach (var property in result3.FailureDetails.Properties)
108+
{
109+
Console.WriteLine($" - {property.Key}: {property.Value}");
110+
}
111+
}
112+
}
113+
Console.WriteLine();
114+
115+
Console.WriteLine("Sample completed. Press any key to exit...");
116+
Console.ReadKey();
117+
118+
await host.StopAsync();
119+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Exception Properties Sample
2+
3+
This sample demonstrates how to use `IExceptionPropertiesProvider` to enrich `TaskFailureDetails` with custom exception properties for better diagnostics and error handling.
4+
5+
## Overview
6+
7+
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.
8+
9+
## Key Concepts
10+
11+
1. **Custom Exception with Properties**: Create exceptions that carry additional context (error codes, metadata, etc.)
12+
2. **IExceptionPropertiesProvider**: Implement this interface to extract custom properties from exceptions
13+
3. **Automatic Property Extraction**: The framework automatically uses your provider when converting exceptions to `TaskFailureDetails`
14+
4. **Retrieving Failure Details**: Use the durable client to retrieve orchestration status and access the enriched failure details
15+
16+
## What This Sample Does
17+
18+
1. Defines a `BusinessValidationException` with custom properties (ErrorCode, StatusCode, Metadata)
19+
2. Implements `CustomExceptionPropertiesProvider` that extracts these properties from exceptions
20+
3. Creates a validation orchestration and activity that throws the custom exception
21+
4. Demonstrates how to retrieve and display failure details with custom properties using the durable client
22+
23+
## Running the Sample
24+
25+
```bash
26+
dotnet run --project ExceptionPropertiesSample
27+
```
28+
29+
## Expected Output
30+
31+
The sample runs three test cases:
32+
1. **Valid input**: Orchestration completes successfully
33+
2. **Empty input**: Orchestration fails with custom properties (ErrorCode, StatusCode, Metadata)
34+
3. **Short input**: Orchestration fails with different custom properties
35+
36+
For failed orchestrations, you'll see the custom properties extracted by the `IExceptionPropertiesProvider` displayed in the console.
37+
38+
## Code Structure
39+
40+
- `CustomExceptions.cs`: Defines the `BusinessValidationException` with custom properties
41+
- `CustomExceptionPropertiesProvider.cs`: Implements `IExceptionPropertiesProvider` to extract properties
42+
- `Tasks.cs`: Contains the orchestration and activity that throw custom exceptions
43+
- `Program.cs`: Sets up the worker, registers the provider, and demonstrates retrieving failure details
44+
45+
## Key Code Snippet
46+
47+
```csharp
48+
// Register the custom exception properties provider
49+
builder.Services.AddSingleton<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();
50+
51+
// Retrieve failure details with custom properties
52+
OrchestrationMetadata result = await client.WaitForInstanceCompletionAsync(
53+
instanceId,
54+
getInputsAndOutputs: true); // Important: must be true to get failure details
55+
56+
if (result.FailureDetails?.Properties != null)
57+
{
58+
foreach (var property in result.FailureDetails.Properties)
59+
{
60+
Console.WriteLine($"{property.Key}: {property.Value}");
61+
}
62+
}
63+
```
64+
65+
## Notes
66+
67+
- The `getInputsAndOutputs` parameter must be `true` when calling `GetInstanceAsync` or `WaitForInstanceCompletionAsync` to retrieve failure details
68+
- Custom properties are only included if the orchestration is in a `Failed` state
69+
- The `IExceptionPropertiesProvider` is called automatically by the framework when exceptions are caught
70+
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.DurableTask;
5+
6+
namespace ExceptionPropertiesSample;
7+
8+
/// <summary>
9+
/// Orchestration that demonstrates custom exception properties in failure details.
10+
/// </summary>
11+
[DurableTask("ValidationOrchestration")]
12+
public class ValidationOrchestration : TaskOrchestrator<string, string>
13+
{
14+
public override async Task<string> RunAsync(TaskOrchestrationContext context, string input)
15+
{
16+
// Call an activity that may throw a custom exception with properties
17+
try
18+
{
19+
string result = await context.CallActivityAsync<string>("ValidateInput", input);
20+
return result;
21+
}
22+
catch (TaskFailedException ex)
23+
{
24+
// The failure details will include custom properties from IExceptionPropertiesProvider
25+
// These properties are automatically extracted and included in the TaskFailureDetails
26+
throw;
27+
}
28+
}
29+
}
30+
31+
/// <summary>
32+
/// Activity that validates input and throws a custom exception with properties on failure.
33+
/// </summary>
34+
[DurableTask("ValidateInput")]
35+
public class ValidateInputActivity : TaskActivity<string, string>
36+
{
37+
public override Task<string> RunAsync(TaskActivityContext context, string input)
38+
{
39+
// Simulate validation logic
40+
if (string.IsNullOrWhiteSpace(input))
41+
{
42+
throw new BusinessValidationException(
43+
message: "Input validation failed: input cannot be empty",
44+
errorCode: "VALIDATION_001",
45+
statusCode: 400,
46+
metadata: new Dictionary<string, object?>
47+
{
48+
["Field"] = "input",
49+
["ValidationRule"] = "Required",
50+
["Timestamp"] = DateTime.UtcNow,
51+
});
52+
}
53+
54+
if (input.Length < 3)
55+
{
56+
throw new BusinessValidationException(
57+
message: $"Input validation failed: input must be at least 3 characters (received {input.Length})",
58+
errorCode: "VALIDATION_002",
59+
statusCode: 400,
60+
metadata: new Dictionary<string, object?>
61+
{
62+
["Field"] = "input",
63+
["ValidationRule"] = "MinLength",
64+
["MinLength"] = 3,
65+
["ActualLength"] = input.Length,
66+
});
67+
}
68+
69+
// Validation passed
70+
return Task.FromResult($"Validation successful for input: {input}");
71+
}
72+
}
73+

0 commit comments

Comments
 (0)