{"kind":"AgentDefinition","metadata":{"namespace":"community","name":"azure-durable-functions-csharp","version":"0.1.0"},"spec":{"agents_md":"---\ndescription: 'Guidelines and best practices for building Azure Durable Functions in C# using the isolated worker model'\napplyTo: '**/*.cs, **/host.json, **/local.settings.json, **/*.csproj'\n---\n\n# Azure Durable Functions C# Development\n\n## General Instructions\n\n- Always use the **isolated worker model** with the `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` NuGet package for new Durable Functions projects.\n- Use `Microsoft.DurableTask` namespaces for orchestrator and activity context types (`TaskOrchestrationContext`, `TaskActivityContext`).\n- Separate orchestrators, activities, entities, and client starter functions into distinct classes or files for clarity.\n- Never mix orchestration logic with activity logic — orchestrators coordinate; activities do work.\n- Always use `context.CreateReplaySafeLogger(nameof(OrchestratorName))` inside orchestrator functions for logging; never use injected `ILogger\u003cT\u003e` directly in orchestrators as it logs on every replay.\n- Use `async Task` or `async Task\u003cT\u003e` for all orchestrator and activity methods — never `async void`.\n- Treat orchestrator code as **deterministic and replay-safe**: no `DateTime.Now`, `Guid.NewGuid()`, `Random`, direct HTTP calls, or non-deterministic I/O inside orchestrators.\n- Use `context.CurrentUtcDateTime` instead of `DateTime.UtcNow` inside orchestrators.\n\n## Project Structure\n\n- Register Durable Functions support in `Program.cs` via `builder.Services.AddDurableTaskClient()` and `builder.ConfigureFunctionsWorkerDefaults(x =\u003e x.UseDurableTask())`.\n- Organize orchestrators, activities, and entities into feature-based folders (e.g., `/Orchestrations/OrderProcessing/`), not by function type.\n- Name orchestrators with the suffix `Orchestrator` (e.g., `ProcessOrderOrchestrator`), activities with the suffix `Activity` (e.g., `ChargePaymentActivity`), and entities with the suffix `Entity` (e.g., `CartEntity`).\n- Use constants or static readonly strings for activity/orchestrator/entity names passed to `CallActivityAsync`, `CallSubOrchestratorAsync`, and `GetEntityStateAsync` to prevent typos.\n\n## Configuration Files\n\n### local.settings.json\n- Always include `AzureWebJobsStorage` connection string for local development — Durable Functions requires storage to maintain orchestration state.\n- Use `\"UseDevelopmentStorage=true\"` or Azurite connection string for local testing — never use a production storage account from local dev.\n- Set `FUNCTIONS_WORKER_RUNTIME` to `\"dotnet-isolated\"` in local.settings.json.\n- For Netherite or MSSQL storage providers, include provider-specific connection strings (e.g., `EventHubsConnection` for Netherite).\n- Never commit `local.settings.json` to source control — add it to `.gitignore`; use `local.settings.json.example` with placeholder values instead.\n- Store sensitive values (storage keys, Event Hub connection strings) using Azure Key Vault locally via `@Microsoft.KeyVault(...)` references if needed.\n\n### host.json\n- Configure Durable Functions-specific settings under `\"extensions\": { \"durableTask\": { ... } }` — do not rely on defaults for production.\n- Set `\"hubName\"` to a meaningful, environment-specific value (e.g., `\"MyAppProd\"`, `\"MyAppDev\"`) to isolate Task Hubs across environments sharing the same storage account.\n- Tune `\"maxConcurrentActivityFunctions\"` and `\"maxConcurrentOrchestratorFunctions\"` based on expected throughput and hosting plan — defaults are conservative.\n- Enable extended sessions (`\"extendedSessionsEnabled\": true`) for long-running orchestrations on Premium/Dedicated plans to reduce replay overhead.\n- Configure the storage provider: use `\"storageProvider\": { \"type\": \"netherite\" }` or `\"mssql\"` for high-scale scenarios instead of default Azure Storage.\n- Set `\"maxQueuePollingInterval\"` appropriately — lower values increase responsiveness but increase storage transaction costs on Consumption plan.\n- Configure Application Insights sampling rate under `\"logging\": { \"applicationInsights\": { \"samplingSettings\": { ... } } }` to control telemetry volume.\n\n## Orchestration Patterns\n\n### Function Chaining\n- Use sequential `await context.CallActivityAsync\u003cT\u003e(nameof(ActivityName), input)` calls for step-by-step workflows where each step depends on the result of the previous.\n- Pass only serializable, lightweight data as inputs/outputs between activities — avoid passing entire domain objects with circular references.\n\n### Fan-Out / Fan-In\n- Use `Task.WhenAll(tasks)` after fanning out with multiple `context.CallActivityAsync` calls to aggregate parallel results.\n- Cap the degree of parallelism when fanning out over large collections — use batching (e.g., partitioning input lists) to avoid overwhelming downstream services or hitting Durable Functions storage limits.\n- Prefer `List\u003cTask\u003cT\u003e\u003e` over dynamic task arrays; capture all tasks before awaiting to avoid replay issues.\n\n### Async HTTP API (Human Interaction / Long-Running)\n- Use `client.ScheduleNewOrchestrationInstanceAsync` from an HTTP trigger starter function; return `await client.CreateCheckStatusResponseAsync(req, instanceId)` to provide polling URLs to callers.\n- Use `context.WaitForExternalEvent\u003cT\u003e(\"EventName\", timeout)` combined with `context.CreateTimer(deadline, CancellationToken)` to implement approval/callback patterns with timeouts.\n- Always handle the timeout race: use `Task.WhenAny(externalEventTask, timerTask)` and cancel the timer if the event arrives first.\n\n### Monitoring / Polling Pattern\n- Use a `while` loop with `context.CreateTimer(context.CurrentUtcDateTime.Add(interval), CancellationToken.None)` for polling workflows instead of separate timer-triggered functions.\n- Ensure the monitoring loop has a clear exit condition to avoid infinite loops that never terminate.\n- For recurring eternal workflows, use `context.ContinueAsNew(input)` to restart the orchestration with fresh state and prevent unbounded history growth.\n\n### Eternal Orchestrations\n- Use `context.ContinueAsNew(newInput)` at the end of the orchestrator body to restart with clean state for long-lived recurring workflows.\n- Drain any pending external events before calling `ContinueAsNew` when using `isKeepRunning` patterns.\n- Combine `ContinueAsNew` with `context.CreateTimer` to implement periodic tasks (e.g., daily report generation, cache refresh).\n\n### Sub-Orchestrations\n- Use `context.CallSubOrchestratorAsync\u003cT\u003e(nameof(SubOrchestrator), instanceId, input)` to decompose complex workflows into reusable child orchestrations.\n- Provide an explicit `instanceId` for sub-orchestrations when idempotency or correlation is required.\n- Limit sub-orchestration nesting depth to avoid history size issues; flatten workflows where possible.\n\n### Entity Functions (Stateful Entities)\n- Define entities using class-based syntax implementing `TaskEntity\u003cTState\u003e` for typed, encapsulated state management.\n- Access entity state only via entity operations (`entity.State`); never read or write entity storage directly.\n- Use `context.Entities.CallEntityAsync\u003cT\u003e` from activities or `context.Entities.SignalEntityAsync` from orchestrators for fire-and-forget entity operations.\n- Prefer `SignalEntityAsync` over `CallEntityAsync` from orchestrators when the return value is not needed, to avoid unnecessary blocking.\n- Use entities for scenarios requiring distributed counters, distributed locks, aggregators, or per-user/per-session state.\n- Keep entity state small and serializable; avoid storing large blobs or collections that grow unboundedly in entity state.\n\n## Activity Functions\n\n- Keep activity functions focused on a single unit of work — they are the only place to perform I/O (database reads/writes, HTTP calls, queue sends).\n- Inject services (e.g., `IRepository`, `IHttpClientFactory`) via constructor DI into the class containing activity functions; do not use `[FromServices]` inside the activity method.\n- Make activities **idempotent** where possible — orchestrators may call the same activity multiple times on retry.\n- Use `TaskActivityContext` parameter type for activity context; log using the injected `ILogger\u003cT\u003e` (not a replay-safe logger — activities are not replayed).\n- Return only serializable types from activities; avoid returning domain entities with navigation properties.\n\n## Error Handling and Compensation\n\n- Wrap `context.CallActivityAsync` calls in try/catch blocks within the orchestrator to handle `TaskFailedException` for graceful error handling and compensation.\n- Implement compensating transactions (saga pattern) in the catch block by calling undo activities when a step fails mid-workflow.\n- Use `RetryPolicy` (via `new TaskOptions(new RetryPolicy(maxRetries, firstRetryInterval))`) on activity calls for automatic retries with backoff on transient failures.\n- Distinguish between transient errors (retry) and business errors (fail-fast and compensate) — do not retry validation or authorization failures.\n- Always terminate stuck orchestrations via the Durable Functions management API or client if they enter an error state that cannot self-resolve.\n\n## Timers\n\n- Use `context.CreateTimer(fireAt, CancellationToken)` for durable delays inside orchestrators — never use `Task.Delay` or `Thread.Sleep`.\n- Always cancel timers that are no longer needed (e.g., when an external event arrives before the timer fires) by passing and cancelling a `CancellationTokenSource`.\n- Avoid very short timer intervals (under 1 minute) in production on the Consumption plan; they may cause excessive storage polling costs.\n\n## Instance Management\n\n- Use meaningful, deterministic `instanceId` values (e.g., `$\"order-{orderId}\"`) instead of GUIDs when the orchestration needs to be correlated to a business entity.\n- Check for existing instances using `client.GetInstanceMetadataAsync(instanceId)` before scheduling new ones to prevent duplicate orchestrations (singleton pattern).\n- Use `client.TerminateInstanceAsync`, `client.SuspendInstanceAsync`, and `client.ResumeInstanceAsync` for lifecycle management in management APIs or administrative functions.\n- Purge completed/failed orchestration history periodically using `client.PurgeInstanceAsync` or bulk purge to control Task Hub storage growth.\n\n## Observability\n\n- Use `context.CreateReplaySafeLogger(nameof(Orchestrator))` for all logging inside orchestrators to prevent duplicate log entries during replay.\n- Log the `instanceId` in every log statement from orchestrators and starters for end-to-end traceability.\n- Use Application Insights with the Durable Functions integration to track orchestration lifecycle events, activity durations, and failures.\n- Monitor orchestration health via the Durable Functions HTTP management API endpoints (`/runtime/webhooks/durabletask/instances`) or the Durable Functions Monitor VS Code extension.\n- Set `durableTask.maxConcurrentOrchestratorFunctions` and `durableTask.maxConcurrentActivityFunctions` in `host.json` to control concurrency and prevent resource exhaustion.\n\n## Storage and Task Hub Configuration\n\n- Configure the Task Hub name in `host.json` under `\"extensions\": { \"durableTask\": { \"hubName\": \"MyTaskHub\" } }` to isolate environments (dev/staging/prod) sharing the same storage account.\n- Use separate storage accounts or Task Hub names per environment to avoid cross-environment interference.\n- For high-throughput scenarios, use the **Netherite** or **MSSQL** storage provider instead of the default Azure Storage provider to improve performance and reduce costs.\n- Avoid storing large payloads (\u003e64KB) directly as orchestration inputs/outputs; store large data in Blob Storage and pass the reference (URL/ID) instead.\n\n## Testing Durable Functions\n\n- Use the `Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Tests` NuGet package (if available) or manually mock `TaskOrchestrationContext` for unit testing orchestrators.\n- Test activity functions in isolation as regular methods — inject mocks for their dependencies (repositories, HTTP clients) and assert on return values.\n- Test orchestrator logic by mocking `context.CallActivityAsync`, `context.CreateTimer`, and `context.WaitForExternalEvent` using a test harness or manual mocks.\n- Avoid testing the Durable Functions runtime itself (event sourcing, replay) — focus tests on the business logic inside orchestrators and activities.\n- Use integration tests with Azurite or an isolated Azure Storage account to test end-to-end workflows, including starter → orchestrator → activity → completion.\n- Use deterministic instance IDs in tests (e.g., `$\"test-{Guid.NewGuid()}\"`) to enable querying and verifying orchestration state via `client.GetInstanceMetadataAsync`.\n- Test timeout scenarios by mocking `context.CreateTimer` to fire immediately and verifying the orchestrator handles the timeout branch.\n- Test compensation/error handling by forcing activity failures (throw exceptions in mocked activities) and asserting the orchestrator calls compensating activities.\n- Use `client.WaitForInstanceCompletionAsync` in integration tests instead of polling — it blocks until the orchestration completes or times out.\n- For entity tests, use `context.Entities.SignalEntityAsync` in test orchestrators and verify entity state via `client.ReadEntityStateAsync` after the orchestration completes.\n\n## Existing Code Review Guidance\n\n- If `DateTime.UtcNow` or `DateTime.Now` is used inside an orchestrator, flag it and replace with `context.CurrentUtcDateTime`.\n- If `Guid.NewGuid()` or `Random` is used inside an orchestrator, flag it as non-deterministic and move it to an activity.\n- If direct HTTP calls (`HttpClient.GetAsync`, etc.) are made inside an orchestrator, flag them immediately and move the call into an activity function.\n- If `Task.Delay` or `Thread.Sleep` is used inside an orchestrator, replace with `context.CreateTimer`.\n- If orchestration history is growing unboundedly without `ContinueAsNew` on long-running loops, suggest adding `ContinueAsNew` to reset history.\n- If entity state is storing large collections or blob data, suggest externalizing large data to Blob Storage and storing only references in entity state.\n- If activity functions are not idempotent and the workflow has no retry/compensation logic, flag this as a reliability risk.\n","description":"Guidelines and best practices for building Azure Durable Functions in C# using the isolated worker model","import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/blob/541b7819d8c3545c6df122491af4fa1eae415779/instructions/azure-durable-functions-csharp.instructions.md"},"manifest":{}},"content_hash":[185,240,9,207,111,103,89,78,255,179,240,110,185,58,230,191,143,55,60,123,194,180,32,56,212,190,80,131,22,196,48,133],"trust_level":"unsigned","yanked":false}
