diff --git a/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/CustomExceptionPropertyProvider.csproj b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/CustomExceptionPropertyProvider.csproj new file mode 100644 index 00000000..1d2aad47 --- /dev/null +++ b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/CustomExceptionPropertyProvider.csproj @@ -0,0 +1,21 @@ + + + net8.0 + v4 + Exe + enable + enable + CustomExceptionPropertyProvider + + + + + + + + + + + + + diff --git a/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/Functions.cs b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/Functions.cs new file mode 100644 index 00000000..bcdd69da --- /dev/null +++ b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/Functions.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; + +namespace CustomExceptionPropertyProvider; + +// ============================================================================= +// Orchestration + activity +// ============================================================================= + +/// +/// Demonstrates custom FailureDetails properties. An activity throws a +/// ; the registered +/// attaches its structured fields to +/// the failure. The orchestration catches the propagated +/// and returns its FailureDetails, +/// including the custom Properties. +/// +public static class CustomExceptionOrchestration +{ + [Function(nameof(OrchestrationWithCustomException))] + public static async Task OrchestrationWithCustomException( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + try + { +#pragma warning disable DURABLE2001 // Activity has no input + await context.CallActivityAsync(nameof(BusinessActivity)); +#pragma warning restore DURABLE2001 + } + catch (TaskFailedException ex) + { + // FailureDetails.Properties contains the values supplied by the provider. + return ex.FailureDetails; + } + + // Should never reach here — the activity always throws. + return null; + } + + [Function(nameof(BusinessActivity))] + public static void BusinessActivity([ActivityTrigger] TaskActivityContext context) + { + throw new BusinessValidationException( + message: "Business logic validation failed", + stringProperty: "validation-error-123", + intProperty: 100, + longProperty: 999999999L, + dateTimeProperty: new DateTime(2025, 10, 15, 14, 30, 0, DateTimeKind.Utc), + dictionaryProperty: new Dictionary + { + ["error_code"] = "VALIDATION_FAILED", + ["retry_count"] = 3, + ["is_critical"] = true, + }, + listProperty: new List { "error1", "error2", 500, null }, + nullProperty: null); + } + + [Function(nameof(OrchestrationWithCustomException) + "_Start")] + public static async Task Start( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/customException")] HttpRequestData req, + [DurableClient] DurableTaskClient client) + { + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestrationWithCustomException)); + return client.CreateCheckStatusResponse(req, instanceId); + } +} + +// ============================================================================= +// Custom exception + provider +// ============================================================================= + +/// +/// Business exception carrying structured properties that should be surfaced on +/// FailureDetails.Properties. +/// +[Serializable] +public class BusinessValidationException : Exception +{ + public BusinessValidationException( + string message, + string stringProperty, + int intProperty, + long longProperty, + DateTime dateTimeProperty, + IDictionary dictionaryProperty, + IList listProperty, + object? nullProperty) + : base(message) + { + this.StringProperty = stringProperty; + this.IntProperty = intProperty; + this.LongProperty = longProperty; + this.DateTimeProperty = dateTimeProperty; + this.DictionaryProperty = dictionaryProperty; + this.ListProperty = listProperty; + this.NullProperty = nullProperty; + } + + public BusinessValidationException(string message) : base(message) { } + + public string? StringProperty { get; } + public int? IntProperty { get; } + public long? LongProperty { get; } + public DateTime? DateTimeProperty { get; } + public IDictionary? DictionaryProperty { get; } + public IList? ListProperty { get; } + public object? NullProperty { get; } +} + +/// +/// Maps thrown exceptions to the custom properties attached to FailureDetails. +/// Returning null opts out of adding properties for a given exception. +/// +public class BusinessExceptionPropertiesProvider : IExceptionPropertiesProvider +{ + public IDictionary? GetExceptionProperties(Exception exception) + { + return exception switch + { + BusinessValidationException e => new Dictionary + { + ["StringProperty"] = e.StringProperty, + ["IntProperty"] = e.IntProperty, + ["LongProperty"] = e.LongProperty, + ["DateTimeProperty"] = e.DateTimeProperty, + ["DictionaryProperty"] = e.DictionaryProperty, + ["ListProperty"] = e.ListProperty, + ["NullProperty"] = e.NullProperty, + }, + _ => null, + }; + } +} diff --git a/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/Program.cs b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/Program.cs new file mode 100644 index 00000000..07cc96c0 --- /dev/null +++ b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/Program.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CustomExceptionPropertyProvider; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +FunctionsApplicationBuilder builder = FunctionsApplication.CreateBuilder(args); + +// Register the custom exception properties provider. When an activity throws, +// the Durable worker consults this provider to extract structured properties +// from the exception and attaches them to FailureDetails.Properties, which is +// then surfaced to the orchestration that observes the failure. +builder.Services.AddSingleton(); + +builder.Build().Run(); diff --git a/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/README.md b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/README.md new file mode 100644 index 00000000..9d890567 --- /dev/null +++ b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/README.md @@ -0,0 +1,77 @@ +# Custom Exception Property Provider with Durable Functions (.NET) + +.NET | Durable Functions + +## Description + +Demonstrates attaching **custom properties to `FailureDetails`** when an activity throws, using the Durable Task Scheduler (DTS) backend. + +When an activity throws a `BusinessValidationException`, the registered `IExceptionPropertiesProvider` extracts structured fields (string, int, long, date-time, dictionary, list, null) from the exception. The Durable worker propagates those values onto `FailureDetails.Properties`, which the orchestration observes when it catches the failure — making rich, typed error context available to the orchestrator instead of just a message and stack trace. + +## Prerequisites + +1. [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +2. [Docker](https://www.docker.com/products/docker-desktop/) (for running the emulator) +3. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local) + +> **Note:** The custom exception property provider requires these minimum versions (the sample references the latest available): +> - `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` >= 1.9.0 +> - `Microsoft.DurableTask.Worker` >= 1.16.1 (provides `IExceptionPropertiesProvider`) +> - `Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged` >= 1.2.0 (for the Durable Task Scheduler backend) + +## Quick Run + +1. Start the Durable Task Scheduler emulator: + ```bash + docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + ``` + +2. Start the Function app: + ```bash + cd samples/durable-functions/dotnet/CustomExceptionPropertyProvider + func start + ``` + +3. Start the orchestration: + ```bash + curl -X POST http://localhost:7071/api/orchestrators/customException + ``` + +4. Follow the `statusQueryGetUri` from the response to see the completed output. + +5. View in the dashboard: http://localhost:8082 + +## Expected Output + +The orchestration returns the caught `FailureDetails`, whose `Properties` carry the custom values supplied by the provider: + +```json +{ + "errorType": "CustomExceptionPropertyProvider.BusinessValidationException", + "errorMessage": "Business logic validation failed", + "properties": { + "StringProperty": "validation-error-123", + "IntProperty": 100, + "LongProperty": 999999999, + "DateTimeProperty": "2025-10-15T14:30:00Z", + "DictionaryProperty": { + "error_code": "VALIDATION_FAILED", + "retry_count": 3, + "is_critical": true + }, + "ListProperty": ["error1", "error2", 500, null], + "NullProperty": null + } +} +``` + +## How It Works + +- `BusinessExceptionPropertiesProvider` implements `Microsoft.DurableTask.Worker.IExceptionPropertiesProvider` and is registered as a singleton in `Program.cs`. +- `BusinessActivity` throws a `BusinessValidationException` carrying the structured fields. +- `OrchestrationWithCustomException` calls the activity, catches the propagated `TaskFailedException`, and returns `ex.FailureDetails` — including the custom `Properties`. + +## Learn More + +- [Durable Functions error handling](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-error-handling) +- [Durable Task Scheduler Documentation](https://aka.ms/dts-documentation) diff --git a/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/host.json b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/host.json new file mode 100644 index 00000000..50a73621 --- /dev/null +++ b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/host.json @@ -0,0 +1,20 @@ +{ + "version": "2.0", + "logging": { + "logLevel": { + "default": "Information", + "DurableTask.Core": "Warning", + "Microsoft.DurableTask": "Information", + "Host.Triggers.DurableTask": "Information" + } + }, + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + } +} diff --git a/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/test.http b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/test.http new file mode 100644 index 00000000..46a50f84 --- /dev/null +++ b/samples/durable-functions/dotnet/CustomExceptionPropertyProvider/test.http @@ -0,0 +1,22 @@ +# ============================================================================= +# Custom Exception Property Provider — Durable Functions (.NET isolated) Demo +# ============================================================================= +# +# Prerequisites: +# 1. DTS emulator: docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest +# 2. App running: func start --port 7071 +# +# The orchestration calls an activity that throws BusinessValidationException. +# A registered IExceptionPropertiesProvider attaches structured properties to +# FailureDetails.Properties, which the orchestration returns as its output. +# ============================================================================= + +@baseUrl = http://localhost:7071/api + + +### Start the orchestration +# @name customException +POST {{baseUrl}}/orchestrators/customException + +### Check status — the output holds FailureDetails, including custom Properties +GET {{customException.response.body.statusQueryGetUri}} diff --git a/samples/durable-functions/javascript/CustomExceptionPropertyProvider/README.md b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/README.md new file mode 100644 index 00000000..cd7db8d9 --- /dev/null +++ b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/README.md @@ -0,0 +1,79 @@ +# Custom Exception Property Provider — Durable Functions JavaScript + +JavaScript | Durable Functions + +## Description + +Demonstrates attaching **custom properties to `FailureDetails`** when an activity throws, using the Node.js v4 programming model and the Durable Task Scheduler (DTS) backend. + +An activity throws a `BusinessValidationException` carrying structured fields (string, int, long, date-time, dictionary, list, null). A provider registered via `df.app.setExceptionPropertiesProvider(...)` extracts those fields, and the Durable worker propagates them onto `FailureDetails.Properties`. The orchestration catches the propagated `TaskFailedError` and returns its `failureDetails`, so the rich error context is available to the orchestrator instead of just a message and stack trace. + +## Prerequisites + +1. [Node.js 18+](https://nodejs.org/) (use Node 22 LTS; the Functions host does not yet support Node 24) +2. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local) +3. [Docker](https://www.docker.com/products/docker-desktop/) (for the emulator) + +> **Note:** The custom exception property provider requires: +> - The `durable-functions` npm package must include `df.app.setExceptionPropertiesProvider`. +> - An extension bundle that ships the feature. Use the **main** bundle `Microsoft.Azure.Functions.ExtensionBundle` >= **4.37.1** (configured in `host.json`), or the **preview** bundle `Microsoft.Azure.Functions.ExtensionBundle.Preview` >= **4.44.0**. Older bundles will **not** surface `Properties`. +> - The Durable Task Scheduler (azure-managed) SDK >= **1.2.0**. + +## Quick Run + +1. Start the Durable Task Scheduler emulator: + ```bash + docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + ``` + +2. Install dependencies and run: + ```bash + cd samples/durable-functions/javascript/CustomExceptionPropertyProvider + npm install + func start + ``` + +3. Start the orchestration: + ```bash + curl -X POST http://localhost:7071/api/StartCustomException + ``` + +4. Follow the `statusQueryGetUri` from the response to see the completed output. + +5. View in the dashboard: http://localhost:8082 + +## Expected Output + +The orchestration returns the caught `failureDetails`, whose `properties` carry the custom values supplied by the provider: + +```json +{ + "errorType": "BusinessValidationException", + "errorMessage": "Business logic validation failed", + "properties": { + "StringProperty": "validation-error-123", + "IntProperty": 100, + "LongProperty": 999999999, + "DateTimeProperty": "2025-10-15T14:30:00.000Z", + "DictionaryProperty": { + "error_code": "VALIDATION_FAILED", + "retry_count": 3, + "is_critical": true + }, + "ListProperty": ["error1", "error2", 500, null], + "NullProperty": null + } +} +``` + +## How It Works + +- `df.app.setExceptionPropertiesProvider({ getExceptionProperties(error) { ... } })` registers a global provider once at app startup. +- `businessActivity` throws a `BusinessValidationException` carrying the structured fields. +- `orchestrationWithCustomException` calls the activity, catches the propagated `TaskFailedError`, and returns `e.failureDetails` — including the custom `properties`. + +## Learn More + +- [Durable Functions JavaScript API Reference](https://learn.microsoft.com/javascript/api/durable-functions/) +- [Durable Functions error handling](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-error-handling) +- [Durable Task Scheduler Documentation](https://aka.ms/dts-documentation) diff --git a/samples/durable-functions/javascript/CustomExceptionPropertyProvider/host.json b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/host.json new file mode 100644 index 00000000..7c0e97a6 --- /dev/null +++ b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/host.json @@ -0,0 +1,21 @@ +{ + "version": "2.0", + "logging": { + "logLevel": { + "DurableTask.Core": "Warning" + } + }, + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.37.1, 5.0.0)" + } +} diff --git a/samples/durable-functions/javascript/CustomExceptionPropertyProvider/package.json b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/package.json new file mode 100644 index 00000000..8e64792f --- /dev/null +++ b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/package.json @@ -0,0 +1,13 @@ +{ + "name": "durable-functions-custom-exception-property-provider-js", + "version": "1.0.0", + "description": "Durable Functions JavaScript sample: custom exception property provider for FailureDetails with Durable Task Scheduler", + "main": "src/functions/*.js", + "scripts": { + "start": "func start" + }, + "dependencies": { + "@azure/functions": "^4.5.0", + "durable-functions": "^3.5.0" + } +} diff --git a/samples/durable-functions/javascript/CustomExceptionPropertyProvider/src/functions/customExceptionPropertyProvider.js b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/src/functions/customExceptionPropertyProvider.js new file mode 100644 index 00000000..72c3853c --- /dev/null +++ b/samples/durable-functions/javascript/CustomExceptionPropertyProvider/src/functions/customExceptionPropertyProvider.js @@ -0,0 +1,102 @@ +const { app } = require("@azure/functions"); +const df = require("durable-functions"); +const { TaskFailedError } = df; + +// ============================================================================= +// Custom exception carrying structured properties. +// ============================================================================= +class BusinessValidationException extends Error { + constructor( + message, + stringProperty, + intProperty, + longProperty, + dateTimeProperty, + dictionaryProperty, + listProperty, + nullProperty + ) { + super(message); + this.name = "BusinessValidationException"; + // Fix the prototype chain (necessary when extending built-ins). + Object.setPrototypeOf(this, new.target.prototype); + this.stringProperty = stringProperty; + this.intProperty = intProperty; + this.longProperty = longProperty; + this.dateTimeProperty = dateTimeProperty; + this.dictionaryProperty = dictionaryProperty; + this.listProperty = listProperty; + this.nullProperty = nullProperty; + } +} + +// ============================================================================= +// Register a global provider that surfaces custom properties from thrown +// exceptions into FailureDetails.Properties. Return `undefined` to opt out for +// a particular error. +// ============================================================================= +df.app.setExceptionPropertiesProvider({ + getExceptionProperties(error) { + if (error instanceof BusinessValidationException) { + return { + StringProperty: error.stringProperty, + IntProperty: error.intProperty, + LongProperty: error.longProperty, + DateTimeProperty: error.dateTimeProperty, + DictionaryProperty: error.dictionaryProperty, + ListProperty: error.listProperty, + NullProperty: error.nullProperty, + }; + } + return undefined; + }, +}); + +// Activity: throws an exception carrying custom properties. +df.app.activity("businessActivity", { + handler: () => { + throw new BusinessValidationException( + "Business logic validation failed", + "validation-error-123", + 100, + 999999999, + "2025-10-15T14:30:00.000Z", + { + error_code: "VALIDATION_FAILED", + retry_count: 3, + is_critical: true, + }, + ["error1", "error2", 500, null], + null + ); + }, +}); + +// Orchestrator: calls the activity, catches the propagated TaskFailedError, and +// returns its FailureDetails (which includes the custom Properties). +df.app.orchestration("orchestrationWithCustomException", function* (context) { + try { + yield context.df.callActivity("businessActivity"); + } catch (e) { + if (e instanceof TaskFailedError) { + return e.failureDetails; + } + throw e; + } + // Should never reach here — the activity always throws. + return null; +}); + +// HTTP trigger: start the orchestration. +app.http("StartCustomException", { + route: "StartCustomException", + methods: ["POST"], + authLevel: "anonymous", + extraInputs: [df.input.durableClient()], + handler: async (request, context) => { + const client = df.getClient(context); + const instanceId = await client.startNew("orchestrationWithCustomException"); + context.log(`Started orchestration with ID = '${instanceId}'.`); + return client.createCheckStatusResponse(request, instanceId); + }, +});