<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Martin Oehlert</title>
    <description>The latest articles on DEV Community by Martin Oehlert (@martin_oehlert).</description>
    <link>https://dev.to/martin_oehlert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1661015%2Fd0bdf508-0244-49d8-8655-aea054d71b86.png</url>
      <title>DEV Community: Martin Oehlert</title>
      <link>https://dev.to/martin_oehlert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/martin_oehlert"/>
    <language>en</language>
    <item>
      <title>Production Realities: When Azure Functions Stops Being Serverless</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 10 Apr 2026 05:38:43 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g</link>
      <guid>https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bonus: Production Realities: When Serverless Stops Being Serverless (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Enterprise Reality Check
&lt;/h2&gt;

&lt;p&gt;At what point does an Azure Functions deployment stop being serverless and start being managed compute with a monthly bill? The shift happens not in one decision but in a sequence of reasonable ones: a VNet requirement from the security team, then private endpoints for the storage account, then an API gateway because the function is public-facing.&lt;/p&gt;

&lt;p&gt;Your function works on Consumption. Zero cost at idle, automatic scaling, no infrastructure to think about. Then the security review lands. VNet integration is mandatory. Consumption doesn't support VNets, so you move to Flex Consumption. Private endpoints? Flex handles those too. You're still paying close to nothing at idle.&lt;/p&gt;

&lt;p&gt;Then the surrounding infrastructure arrives. API Management adds $147 to $700. WAF protection adds $333. Each requirement passes its own cost-benefit test. None of them would make you question the architecture on their own. But the total floor lands somewhere between $530 and $1,080 per month, and the function plan itself is the smallest line item on the invoice.&lt;/p&gt;

&lt;p&gt;The serverless pitch from Part 1 of this series was real. It just applies to a narrower set of workloads than most teams expect when they start. Once you're past that boundary, the question isn't whether to pay more. It's whether the Functions abstraction is still worth paying for, or whether Container Apps or App Service would give you the same outcome with less friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  VNet: The Requirement That Changes Everything
&lt;/h2&gt;

&lt;p&gt;Most enterprise environments mandate &lt;strong&gt;VNet integration&lt;/strong&gt; for anything touching internal databases, key vaults behind private endpoints, or services that shouldn't be exposed to the public internet. The Consumption plan doesn't support VNet. That single requirement forces you into a different hosting plan, and each plan carries different pricing and operational constraints.&lt;/p&gt;

&lt;p&gt;These are your options (East US pricing, April 2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;VNet&lt;/th&gt;
&lt;th&gt;Scale to Zero&lt;/th&gt;
&lt;th&gt;Min Idle Cost/mo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumption&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex Consumption (on-demand)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex Consumption (1 always-ready, 2 GB)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;~$21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium EP1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;~$146&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium EP2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;~$291&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated S1/P1v3&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container Apps (1 replica, 0.25 vCPU)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;~$10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The jump from Consumption to Premium EP1 is $146/month for a single function app sitting idle. That's the cost of VNet access before your code processes a single request. Premium EP2 doubles it. These aren't theoretical numbers: they're the minimum monthly charges while your function waits for traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex Consumption&lt;/strong&gt; went GA in November 2024, and Microsoft now positions it as the recommended path for apps that need dynamic scaling with VNet support. In on-demand mode, Flex preserves the scale-to-zero model that made Consumption attractive. It also skips the Azure Files dependency (the shared file system Premium uses for deployment artifacts and runtime state). If your security team mandates private networking for storage, that saves roughly $30/month on private endpoint costs you'd otherwise pay on Premium. Under the hood, Flex uses shared gateways (up to 27 shared gateway IPs) instead of dedicated VNet-injected workers. That's how it keeps costs lower.&lt;/p&gt;

&lt;p&gt;If you need guaranteed warm instances to avoid cold starts, Flex's always-ready configuration starts at about $21/month for one instance with 2 GB memory. That's still a fraction of Premium EP1.&lt;/p&gt;

&lt;p&gt;But Flex has real constraints. Before you commit to it, compare what you're giving up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Flex Consumption&lt;/th&gt;
&lt;th&gt;Premium&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Linux only&lt;/td&gt;
&lt;td&gt;Windows + Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apps per plan&lt;/td&gt;
&lt;td&gt;One&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment slots&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Up to 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-process .NET&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App init timeout&lt;/td&gt;
&lt;td&gt;Fixed 30s&lt;/td&gt;
&lt;td&gt;No limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFS file shares&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional availability&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Broad&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;one-app-per-plan&lt;/strong&gt; limitation is easy to overlook. You can't consolidate multiple function apps onto a single Flex plan the way you would with Premium. For teams running five or ten function apps, Premium's ability to share a single plan across all of them can actually cost less per app than running each on its own Flex instance.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;30-second app init timeout&lt;/strong&gt; is fixed. Not configurable. If your function app loads large dependency injection containers and connects to multiple databases at startup, 30 seconds may not be enough. Premium has no startup timeout limit, so heavy initialization is never a problem there.&lt;/p&gt;

&lt;p&gt;If your codebase uses &lt;strong&gt;in-process .NET&lt;/strong&gt; (the older hosting model where your function runs inside the Functions host process), Flex doesn't support it. You'd need to migrate to the isolated worker model first, which is its own project.&lt;/p&gt;

&lt;p&gt;If you need Windows, deployment slots, or in-process .NET: Premium is your only option. If you're on Linux with the isolated worker model and can live with one app per plan, Flex Consumption gives you VNet support without abandoning scale-to-zero.&lt;/p&gt;

&lt;p&gt;One more thing worth knowing: Linux Consumption is on a deprecation path. No new features after September 2025, with retirement scheduled for September 2028. Microsoft is pushing new workloads toward Flex Consumption, and the deprecation timeline makes that push harder to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan Escalation Path
&lt;/h2&gt;

&lt;p&gt;You start on Consumption. Your function triggers on an HTTP request, processes a message, writes to Cosmos DB. It costs nothing when idle. The serverless model, doing what it's supposed to do.&lt;/p&gt;

&lt;p&gt;Then the requirements start arriving, one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  VNet integration
&lt;/h3&gt;

&lt;p&gt;Your security team requires all compute to run inside a virtual network. Consumption doesn't support VNet integration, so you move to &lt;strong&gt;Flex Consumption&lt;/strong&gt; (on-demand only). This still scales to zero. You're still paying nothing at idle. No problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: $0/mo idle&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Private endpoints
&lt;/h3&gt;

&lt;p&gt;Next review: inbound traffic to your function app must go through a private endpoint, and the backing storage accounts need private endpoints too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex Consumption supports inbound private endpoints.&lt;/strong&gt; You don't need to leave Flex for this. Your function app keeps scale-to-zero, and the private endpoint adds ~$7/month.&lt;/p&gt;

&lt;p&gt;Flex also needs private endpoints for its backing storage accounts: Blob, Queue, and Table. Three endpoints, not four, because Flex has no Azure Files dependency. That's &lt;strong&gt;~$22/mo&lt;/strong&gt; for storage endpoints.&lt;/p&gt;

&lt;p&gt;One deployment gotcha worth knowing: combining VNet integration with inbound private endpoints on Flex can cause deployment timeouts at the Kudu RemoveWorkersStep. The current workaround is temporarily removing the private endpoint during deployments, then re-adding it. Not ideal for automated pipelines, and worth factoring into your CI/CD design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$29/mo (Flex on-demand)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The fork: when Premium becomes unavoidable
&lt;/h3&gt;

&lt;p&gt;Most teams can stay on Flex through the VNet and private endpoint requirements. But Flex has constraints that force some teams onto &lt;strong&gt;Premium EP1&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows hosting&lt;/strong&gt; required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment slots&lt;/strong&gt; for blue-green deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-process .NET&lt;/strong&gt; (not yet migrated to isolated worker)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple function apps&lt;/strong&gt; sharing a single plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App init exceeding 30 seconds&lt;/strong&gt; (Flex's hard timeout)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these apply, EP1 gives you 1 vCPU and 3.5 GB of memory. The math: 1 vCPU at $116.80 plus 3.5 GB at $8.322 per GB = &lt;strong&gt;~$146/mo&lt;/strong&gt;. It runs 24/7 whether your function executes or not. Storage private endpoints on Premium cost &lt;strong&gt;~$30/mo&lt;/strong&gt; (four endpoints, including Azure Files).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total if forced to Premium: ~$176/mo&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold starts
&lt;/h3&gt;

&lt;p&gt;On Flex, cold starts are still possible when scaling from zero. If your workload needs guaranteed warm instances, Flex's always-ready configuration starts at ~$21/month for one instance with 2 GB memory. On Premium, cold starts are a non-issue: EP1 keeps at least one instance warm by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$50/mo (Flex + always-ready) or ~$176/mo (Premium)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  API Management
&lt;/h3&gt;

&lt;p&gt;Your API needs rate limiting and a developer portal. You add &lt;strong&gt;Azure API Management&lt;/strong&gt;. The pricing depends on what your organization needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;APIM Basic (classic)&lt;/strong&gt;: ~$147/mo, no VNet integration, 99.95% SLA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIM Standard v2&lt;/strong&gt;: ~$700/mo, partial VNet support (backend only), 99.95% SLA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIM Premium (classic)&lt;/strong&gt;: ~$2,795/mo, full VNet integration, 99.99% SLA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams start with Basic and accept the VNet gap. Some compliance requirements force Standard v2 or higher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$197/mo (Flex + Basic) or ~$323/mo (Premium + Basic)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  WAF protection
&lt;/h3&gt;

&lt;p&gt;Compliance also wants a Web Application Firewall in front of your API. You deploy &lt;strong&gt;Application Gateway WAF_v2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The breakdown: $0.443/hour for 730 hours ($323), plus at least one capacity unit ($10.50), plus a public IP ($3.65). That's &lt;strong&gt;~$333-335/mo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Application Gateway v1 retires April 28, 2026, so WAF_v2 is the only option going forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$530/mo (Flex floor) or ~$656/mo (Premium floor)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The full picture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Consumption ($0 idle)
  + VNet requirement
  → Flex on-demand: still $0 idle

  + private endpoints (inbound + storage)
  → Flex: ~$29/mo (1 inbound PE + 3 storage PEs)
  → Premium (if forced by constraints): ~$176/mo (EP1 + 4 storage PEs)

  + cold start elimination
  → Flex always-ready: ~$21/mo
  → Premium: included (always-on)

  + API Management
  → APIM Basic: ~$147/mo
  → OR APIM Standard v2: ~$700/mo

  + WAF protection
  → Application Gateway WAF_v2: ~$333/mo

  Flex path:
  = ~$530/mo floor (Flex + always-ready + PEs + APIM Basic + WAF)
  = ~$1,083/mo ceiling (Flex + PEs + APIM Standard v2 + WAF)

  Premium path (forced by constraints):
  = ~$656/mo floor (EP1 + PEs + APIM Basic + WAF)
  = ~$1,209/mo ceiling (EP1 + PEs + APIM Standard v2 + WAF)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of these requirements is reasonable on its own. Your security team isn't wrong to ask for VNet integration. Private endpoints are a real protection. APIM and WAF exist because APIs need them.&lt;/p&gt;

&lt;p&gt;The function plan itself is the smallest factor. On Flex, your compute cost at idle is $0 to $50/month. On Premium, it's $146 to $176. Either way, APIM and WAF add $480 to $1,033 on top. Those two services dominate the bill regardless of which Functions plan you choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and Deploy Friction
&lt;/h2&gt;

&lt;p&gt;The cost story from the plan escalation section is the monthly bill. The deployment story is the engineering time you spend before your code even runs.&lt;/p&gt;

&lt;p&gt;You cannot convert a function app from one plan to another in place. Moving from Consumption to Flex Consumption, or from Premium to Flex, means creating a new function app, redeploying your code, and deleting the old one. There is no &lt;code&gt;az functionapp update --sku FC1&lt;/code&gt;. Microsoft's own migration guide recommends running both apps in parallel during a transition period, then cutting over. For production workloads, that's a blue-green deployment you didn't plan for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The app settings cleanup
&lt;/h3&gt;

&lt;p&gt;Flex Consumption deprecates roughly 20 app settings and site properties that other plans rely on. If you copy your existing configuration to a new Flex app without cleaning it up, the deployment fails or the app behaves unpredictably.&lt;/p&gt;

&lt;p&gt;These settings must be removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Deployment (handled by functionAppConfig.deployment.storage on Flex)
WEBSITE_RUN_FROM_PACKAGE

# Azure Files (Flex has no Azure Files dependency)
WEBSITE_CONTENTAZUREFILECONNECTIONSTRING
WEBSITE_CONTENTSHARE
WEBSITE_SKIP_CONTENTSHARE_VALIDATION

# Networking (inherited from the integrated VNet on Flex)
WEBSITE_CONTENTOVERVNET
WEBSITE_VNET_ROUTE_ALL
WEBSITE_DNS_SERVER

# Runtime (managed via functionAppConfig.runtime on Flex)
FUNCTIONS_EXTENSION_VERSION
FUNCTIONS_WORKER_RUNTIME

# Scaling (renamed in functionAppConfig.scaleAndConcurrency)
WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most dangerous one is &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt;. On Consumption and Premium, this setting controls how your code gets deployed. On Flex, it must not exist. Flex uses &lt;code&gt;functionAppConfig.deployment.storage&lt;/code&gt; to point at a blob container instead of Azure Files. If &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt; is still present, the deployment silently uses the wrong mechanism.&lt;/p&gt;

&lt;p&gt;Site properties change too. &lt;code&gt;alwaysOn&lt;/code&gt; must be &lt;code&gt;false&lt;/code&gt; on Flex (it's invalid), but &lt;code&gt;true&lt;/code&gt; on Premium and Dedicated. &lt;code&gt;functionsRuntimeScaleMonitoringEnabled&lt;/code&gt; is unnecessary on Flex because scale monitoring is built in, but forgetting to remove it won't break anything. ARM template properties like &lt;code&gt;linuxFxVersion&lt;/code&gt;, &lt;code&gt;containerSize&lt;/code&gt;, and &lt;code&gt;isReserved&lt;/code&gt; are all replaced by the &lt;code&gt;functionAppConfig&lt;/code&gt; section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Infrastructure as Code breaks across plans
&lt;/h3&gt;

&lt;p&gt;Your Terraform and Bicep templates don't just need new property values. They need different resources entirely.&lt;/p&gt;

&lt;p&gt;In Terraform, &lt;code&gt;azurerm_linux_function_app&lt;/code&gt; does not work with the Flex Consumption SKU. Attempting to provision it with an &lt;code&gt;FC1&lt;/code&gt; service plan fails. You need &lt;code&gt;azurerm_function_app_flex_consumption&lt;/code&gt;, a separate resource introduced in AzureRM provider v4.21.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Consumption / Premium: this resource&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_linux_function_app"&lt;/span&gt; &lt;span class="s2"&gt;"func"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;service_plan_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_service_plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Flex Consumption: different resource, different schema&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_function_app_flex_consumption"&lt;/span&gt; &lt;span class="s2"&gt;"func"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;service_plan_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_service_plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;site_config&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nx"&gt;storage_container_type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blobContainer"&lt;/span&gt;
  &lt;span class="nx"&gt;storage_container_endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${azurerm_storage_account.sa.primary_blob_endpoint}${azurerm_storage_container.deploy.name}"&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Flex resource requires a blob storage container for deployments (no Azure Files), supports &lt;code&gt;maximum_instance_count&lt;/code&gt; and &lt;code&gt;instance_memory_mb&lt;/code&gt; properties that don't exist on the standard resource, and has its own quirks. As of early 2026, you still need to set &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; to an empty string as a workaround when using managed identity authentication, then use &lt;code&gt;AzureWebJobsStorage__accountName&lt;/code&gt; for the actual connection.&lt;/p&gt;

&lt;p&gt;In Bicep, the SKU values map to different tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;SKU Name&lt;/th&gt;
&lt;th&gt;SKU Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumption&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Y1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Dynamic&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex Consumption&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FC1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FlexConsumption&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EP1&lt;/code&gt;/&lt;code&gt;EP2&lt;/code&gt;/&lt;code&gt;EP3&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ElasticPremium&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;FC1&lt;/code&gt; plan also requires &lt;code&gt;reserved: true&lt;/code&gt; and a &lt;code&gt;functionAppConfig&lt;/code&gt; section that replaces most of the properties you'd normally set as app settings. That's a structural rewrite of your deployment template, not a property change.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD pipeline adjustments
&lt;/h3&gt;

&lt;p&gt;GitHub Actions requires the &lt;code&gt;sku&lt;/code&gt; parameter in &lt;code&gt;azure/functions-action&lt;/code&gt; when deploying to Flex Consumption with a publish profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure/functions-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.FUNCTION_APP_NAME }}&lt;/span&gt;
    &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PACKAGE_PATH }}&lt;/span&gt;
    &lt;span class="na"&gt;publish-profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PUBLISH_PROFILE }}&lt;/span&gt;
    &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flexconsumption'&lt;/span&gt;
    &lt;span class="na"&gt;remote-build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;sku: 'flexconsumption'&lt;/code&gt;, the action deploys using the standard Consumption mechanism, which fails silently or produces a broken deployment. With OIDC or service principal authentication, the action can auto-detect the SKU, but publish profile deployments need it explicitly.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;scm-do-build-during-deployment&lt;/code&gt; and &lt;code&gt;enable-oryx-build&lt;/code&gt; flags that you might have in your existing workflow are also wrong for Flex. Flex always performs an Oryx build during remote deployment. Setting those flags manually can interfere with the process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Private endpoints break GitHub-hosted runners
&lt;/h3&gt;

&lt;p&gt;If your function app runs on Premium with private endpoints enabled, the SCM/Kudu site is not publicly reachable. GitHub-hosted runners cannot connect to it. Your deployment fails with &lt;code&gt;Failed to fetch Kudu App Settings (CODE: 404)&lt;/code&gt; or a 401, and the error message gives you almost no indication that networking is the problem.&lt;/p&gt;

&lt;p&gt;Your options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted runner inside the VNet&lt;/strong&gt;: works, but now you're maintaining a VM ($50-100/month) to deploy a "serverless" function&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub-hosted runners with Azure private networking&lt;/strong&gt;: GitHub can inject a runner NIC directly into your VNet subnet, giving hosted runners private access without self-hosted infrastructure. Requires a GitHub Team or Enterprise Cloud plan and larger runners (2-64 vCPU, per-minute billing). Supported in 25 Azure regions as of early 2026, but notably not West Europe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy via ARM using a service principal&lt;/strong&gt;: bypasses SCM entirely, pushes configuration through the Azure Resource Manager API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage to blob storage&lt;/strong&gt;: upload your package to a storage account the function app can reach, then trigger deployment from there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Premium, you also need &lt;code&gt;WEBSITE_SKIP_CONTENTSHARE_VALIDATION=1&lt;/code&gt; in your ARM or Bicep templates when the backing storage account has a firewall or private endpoints. Without it, the ARM deployment fails during content share validation because the deployment engine can't reach the storage account through the private endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  The compound effect
&lt;/h3&gt;

&lt;p&gt;Any one of these issues is a half-day fix. The compound effect is what costs real engineering time: you change plans, which changes your Terraform resources, which changes your app settings, which changes your GitHub Actions workflow, which breaks because of private endpoint networking. Each layer has its own failure mode, its own error messages, and its own documentation scattered across different Microsoft Learn pages.&lt;/p&gt;

&lt;p&gt;The real cost of plan migration shows up in the sprint consumed by infrastructure work, not in the Azure bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Serverless Stops Making Sense
&lt;/h2&gt;

&lt;p&gt;At some point, the friction outweighs the abstraction. If you're paying $530/month or more, fighting plan migrations in Terraform, and managing deployment workarounds because private endpoints interfere with your CI pipeline, you should be asking: is the Functions hosting model still earning its keep?&lt;/p&gt;

&lt;p&gt;Signs it's time to look elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your total infrastructure cost exceeds what the "serverless" label saves you in operational effort&lt;/li&gt;
&lt;li&gt;You're spending more time working around platform constraints than building features&lt;/li&gt;
&lt;li&gt;Your build and deploy pipeline is already as complex as it would be with containers&lt;/li&gt;
&lt;li&gt;Your team needs operational control (sidecars, traffic splitting, custom health probes) that Functions doesn't expose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two alternatives worth evaluating are Azure Container Apps and App Service. They solve different problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Container Apps: the container-native path
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Azure Container Apps&lt;/strong&gt; (ACA) can host the Functions runtime directly. The v2 model, which Microsoft recommends for all new deployments, creates a single &lt;code&gt;Microsoft.App&lt;/code&gt; resource with &lt;code&gt;kind=functionapp&lt;/code&gt;. No hidden proxy resources, no dual-resource management. Your function app is a container app with Functions triggers and bindings wired in.&lt;/p&gt;

&lt;p&gt;The resource definition looks like a normal container app deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az containerapp create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-func-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment&lt;/span&gt; my-aca-env &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; myregistry.azurecr.io/my-func:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kind&lt;/span&gt; functionapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-replicas&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-replicas&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;KEDA&lt;/strong&gt; (Kubernetes-based Event Driven Autoscaling) handles scaling. The Functions runtime automatically configures KEDA scale rules based on your triggers. You don't write KEDA definitions yourself; the platform infers them from your bindings. HTTP, Service Bus, Event Hubs, Queue Storage, and other triggers all map to KEDA scalers behind the scenes, and your app can scale to zero when idle.&lt;/p&gt;

&lt;p&gt;What you gain over Premium Functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: a single replica at 0.25 vCPU / 512 MB idles at roughly ~$10/month on the Consumption workload profile. Compare that to Premium EP1 at ~$146/month. With scale-to-zero, idle cost drops to $0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidecar containers&lt;/strong&gt;: run log forwarders, auth proxies, or Dapr sidecars alongside your function app in the same pod&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dapr integration&lt;/strong&gt;: pub/sub, state management, and service invocation without managing the infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic splitting via revisions&lt;/strong&gt;: route a percentage of traffic to a new version before promoting it, something Functions deployment slots can't do with the same granularity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU support&lt;/strong&gt;: if you're running inference workloads alongside event-driven functions, ACA supports GPU-backed workload profiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you give up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Containerization is mandatory&lt;/strong&gt;. There is no code-only deployment path. You build a Docker image, push it to a registry, and deploy from there. If your team has no container experience, this is a real adoption cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No built-in continuous deployment&lt;/strong&gt; from the Functions tooling. You wire up GitHub Actions or Azure Pipelines yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inbound Private Endpoints through the Functions networking layer are not available.&lt;/strong&gt; The Functions networking features table on Microsoft's docs shows a blank cell for "Inbound Private Endpoints" under the Container Apps column. ACA itself supports private endpoints at the environment level (workload profiles environments only), but the Functions-specific private endpoint feature does not carry over. If your compliance requirements specifically mandate Functions inbound private endpoints, Flex Consumption or Premium are your options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The environment-level private endpoint on ACA carries additional charges through the &lt;strong&gt;Dedicated Plan Management&lt;/strong&gt; fee. Budget roughly $67-70/month for this capability, which applies at the environment level regardless of how many apps you run inside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  App Service: the predictable option
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;App Service&lt;/strong&gt; doesn't get much attention in the serverless conversation, but it's worth considering if your workload has predictable traffic and you've already left the scale-to-zero model behind. On Premium EP1, you're paying ~$146/month for a single function app that never scales to zero anyway. An App Service P1v3 plan gives you 2 vCPUs and 8 GB of memory. It supports multiple apps on the same plan, full deployment slots (up to 5 on Standard, 20 on Premium), and no cold starts. The pricing is comparable, and you get operational features that Functions on Premium doesn't match.&lt;/p&gt;

&lt;p&gt;App Service won't give you KEDA-based event scaling or scale-to-zero. It's a fixed-compute model. But if you're already paying for always-on compute through Premium Functions, the question is whether the Functions event-driven abstractions justify the constraints that come with them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Picking the right exit
&lt;/h3&gt;

&lt;p&gt;The choice depends on what pushed you away from Functions in the first place:&lt;/p&gt;

&lt;p&gt;If your main frustration is &lt;strong&gt;cost&lt;/strong&gt;, Container Apps on the Consumption workload profile gives you scale-to-zero with VNet support at a fraction of Premium pricing. You keep the Functions programming model, triggers, and bindings.&lt;/p&gt;

&lt;p&gt;If your frustration is &lt;strong&gt;operational control&lt;/strong&gt;, Container Apps gives you sidecars, revisions, and a container runtime you can customize. The trade-off is containerization overhead and the requirement to build and manage container images.&lt;/p&gt;

&lt;p&gt;Teams frustrated by &lt;strong&gt;complexity for a steady workload&lt;/strong&gt; often find that App Service is the right fit. It strips away the serverless machinery and gives you a predictable compute environment with mature deployment tooling.&lt;/p&gt;

&lt;p&gt;None of these are a universal upgrade. Each one trades a Functions limitation for a different set of constraints. The point is to make that trade consciously, not to discover it after migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making an Honest Choice
&lt;/h2&gt;

&lt;p&gt;Three plans, three different products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumption&lt;/strong&gt; is genuine serverless. Your code runs, you pay for execution time, it scales to zero when idle. If your workload is public-facing, doesn't need VNet access, and won't face a security review that mandates private networking, Consumption is the right plan. It does exactly what the marketing says. The catch: Linux Consumption enters restricted feature mode in September 2025, with full retirement in September 2028. New workloads shouldn't start here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex Consumption&lt;/strong&gt; is serverless with enterprise networking. It went GA in November 2024 and it's the plan Microsoft recommends for new dynamic-scale workloads in 2026. You get VNet integration, inbound private endpoints, scale-to-zero, and no Azure Files dependency. The constraints are real (Linux only, one app per plan, no deployment slots, 30-second init timeout), but for teams that can work within them, Flex keeps the serverless economics intact while passing a security review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Premium&lt;/strong&gt; is managed compute with event-driven scaling. It is not serverless. You're paying for always-on instances whether traffic arrives or not. Premium exists because some requirements (Windows, deployment slots, in-process .NET, multiple apps per plan) have no other home. If you're on Premium, own that decision. Budget for it as compute, not as serverless with extra features.&lt;/p&gt;

&lt;p&gt;The distinction matters most at the moment you least want to think about it: during the security review, when someone asks why your function app can't reach a private endpoint. Know which plan you're actually buying before that conversation starts. Migrating between plans means deleting and recreating the function app, updating Terraform resources, and rewriting deployment pipelines. It's not a configuration change. It's a project.&lt;/p&gt;

&lt;p&gt;Has a security review pushed you from Consumption to Premium, or did you start on Premium from day one?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bonus: Production Realities: When Serverless Stops Being Serverless (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Azure Functions Observability: From Blind Spots to Production Clarity</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 03 Apr 2026 06:25:08 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4</link>
      <guid>https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 9: Azure Functions Observability: From Blind Spots to Production Clarity (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;Your function works locally, passes all tests, and deploys without errors. But how do you know it's healthy at 2am when a queue-triggered function silently drops messages? With a traditional web app on a VM, you'd SSH in, check logs, inspect process health. Serverless strips all of that away.&lt;/p&gt;

&lt;p&gt;The observability gap in serverless is real, and it's structural. Your function runs inside an ephemeral container that spins up on demand, processes an event, and disappears. There's no persistent server to monitor, no process to attach a debugger to, no &lt;code&gt;/var/log&lt;/code&gt; to tail. When your function app scales to zero between invocations, even continuous metric collection breaks down: there is literally nothing running to emit telemetry.&lt;/p&gt;

&lt;p&gt;And when it scales from zero to fifty concurrent instances under load, correlating a single failed request across that distributed execution becomes a different problem entirely. Traditional APM tools assume long-lived processes with stable identities. Serverless functions violate every one of those assumptions.&lt;/p&gt;

&lt;p&gt;Application Insights fills that gap. When connected to your function app (via a connection string, not the deprecated instrumentation key), it automatically captures request telemetry for every function execution, tracks dependencies like HTTP calls and database queries, collects host-level performance counters, and aggregates invocation metrics you can query from the portal or through code.&lt;/p&gt;

&lt;p&gt;On top of that, it gives you structured log queries with KQL (Kusto Query Language), an application map that visualizes how your function calls downstream services, distributed tracing that follows a single request across multiple functions and dependencies, and alerting rules that can page you before your users notice something is wrong.&lt;/p&gt;

&lt;p&gt;The examples below use the classic Application Insights SDK with the isolated worker model, which is what most production .NET function apps run today. The companion repository at &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; has working examples of both the classic SDK (&lt;code&gt;HttpTriggerDemo&lt;/code&gt;) and OpenTelemetry (&lt;code&gt;EventHubDemo&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Application Insights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating the Resource
&lt;/h3&gt;

&lt;p&gt;You can create an Application Insights resource through the Azure Portal (search "Application Insights" and click Create) or provision it with Bicep alongside your function app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: 'appi-orders-prod'
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalyticsWorkspace.id
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resource gives you a &lt;strong&gt;connection string&lt;/strong&gt;, which looks like &lt;code&gt;InstrumentationKey=&amp;lt;guid&amp;gt;;IngestionEndpoint=https://region.in.applicationinsights.azure.com/&lt;/code&gt;. Use this, not the instrumentation key alone. Microsoft deprecated standalone instrumentation key ingestion in March 2025, and connection strings are required for sovereign clouds, regional endpoints, and Entra ID-authenticated ingestion. Store the value in your function app's &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt; application setting, and Azure picks it up automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  NuGet Packages
&lt;/h3&gt;

&lt;p&gt;The isolated worker model needs two packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.ApplicationInsights.WorkerService &lt;span class="nt"&gt;--version&lt;/span&gt; 2.22.0
dotnet add package Microsoft.Azure.Functions.Worker.ApplicationInsights &lt;span class="nt"&gt;--version&lt;/span&gt; 2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pin &lt;code&gt;Microsoft.ApplicationInsights.WorkerService&lt;/code&gt; to 2.22.0.&lt;/strong&gt; Version 3.0.0 migrated to OpenTelemetry internally and broke the &lt;code&gt;ITelemetryInitializer&lt;/code&gt; interface that &lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt; depends on. The result is a &lt;code&gt;TypeLoadException&lt;/code&gt; at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.TypeLoadException: Could not load type
'Microsoft.ApplicationInsights.Extensibility.ITelemetryInitializer'
from assembly 'Microsoft.ApplicationInsights, Version=3.0.0.1'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This affects every .NET version (not just .NET 10). Until the Functions worker package ships a compatible update, stay on 2.22.0. Add a comment in your &lt;code&gt;.csproj&lt;/code&gt; so the next person who runs &lt;code&gt;dotnet outdated&lt;/code&gt; doesn't blindly upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Do NOT upgrade to 3.x: breaks Functions worker. See github.com/Azure/azure-functions-dotnet-worker/issues/3322 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.ApplicationInsights.WorkerService"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.22.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.ApplicationInsights"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Check https://github.com/Azure/azure-functions-dotnet-worker/issues/3322 before upgrading either package --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second package, &lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt;, is what connects your dependency telemetry (HTTP calls, SQL queries, queue operations) back to the parent function invocation. Without it, correlation breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Program.cs Configuration
&lt;/h3&gt;

&lt;p&gt;Two method calls handle the setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AddApplicationInsightsTelemetryWorkerService()&lt;/code&gt; registers the Application Insights SDK for worker-style apps (background services, Functions). &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; hooks into the Functions runtime's activity pipeline so that incoming triggers and outbound calls produce the right request and dependency telemetry.&lt;/p&gt;

&lt;p&gt;See this in context in the &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/HttpTriggerDemo/Program.cs" rel="noopener noreferrer"&gt;HttpTriggerDemo Program.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One catch: the SDK registers a default logging filter that suppresses everything below &lt;code&gt;Warning&lt;/code&gt;. If you leave it in place, your &lt;code&gt;ILogger.LogInformation()&lt;/code&gt; calls never reach Application Insights. Remove it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggerFilterOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;LoggerFilterRule&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderName&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt;
            &lt;span class="s"&gt;"Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, manage log levels through &lt;code&gt;appsettings.json&lt;/code&gt; (loaded automatically by &lt;code&gt;FunctionsApplication.CreateBuilder&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Microsoft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Gets Auto-Collected vs. What You Add Manually
&lt;/h3&gt;

&lt;p&gt;Once that's in place, the SDK and the Functions runtime collect telemetry automatically, with no extra code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08iqaljtc0amwpqfevt9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08iqaljtc0amwpqfevt9.png" alt="Auto-collected vs manual telemetry" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha: Two Log Pipelines, Two Configurations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The isolated worker model runs your code in a separate process from the Functions host. This means &lt;code&gt;host.json&lt;/code&gt; controls logging for the &lt;strong&gt;host process&lt;/strong&gt; (trigger dispatch, scaling decisions, extension lifecycle), while your &lt;code&gt;Program.cs&lt;/code&gt; or &lt;code&gt;appsettings.json&lt;/code&gt; controls logging for the &lt;strong&gt;worker process&lt;/strong&gt; (your function code, your dependencies, your &lt;code&gt;ILogger&lt;/code&gt; calls). If you set &lt;code&gt;"Default": "Information"&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt; but never configure your worker, your application logs still default to &lt;code&gt;Warning&lt;/code&gt; only. You have to configure both sides, and they use different files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Logging Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Structured Logging with ILogger
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Structured logging&lt;/strong&gt; writes log entries as key-value pairs instead of flat strings. In Application Insights, those keys become columns in the &lt;code&gt;customDimensions&lt;/code&gt; property of the &lt;code&gt;traces&lt;/code&gt; table, which means you can filter and aggregate by them in KQL without parsing text.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ILogger&lt;/code&gt; API supports this through &lt;strong&gt;message templates&lt;/strong&gt;: named placeholders wrapped in curly braces, filled by positional arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;OrderMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Order received: {OrderId} from customer {CustomerId}, total {OrderTotal}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stopwatch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Order {OrderId} processed successfully in {ElapsedMs}ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to watch here. First, use &lt;strong&gt;PascalCase&lt;/strong&gt; for placeholder names (&lt;code&gt;{OrderId}&lt;/code&gt;, not &lt;code&gt;{orderId}&lt;/code&gt; or &lt;code&gt;{order_id}&lt;/code&gt;). Application Insights stores these as &lt;code&gt;customDimensions&lt;/code&gt; keys, and PascalCase matches the convention for the rest of the telemetry schema. Second, never use string interpolation (&lt;code&gt;$"Order {orderId}"&lt;/code&gt;) in log calls. Interpolated strings defeat structured logging entirely: the provider receives a pre-formatted string with no queryable fields, and the arguments are evaluated even when the log level is disabled.&lt;/p&gt;

&lt;p&gt;In the Application Insights &lt;code&gt;traces&lt;/code&gt; table, a query like this pulls all logs for a specific order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;traces
| where customDimensions.OrderId == "ORD-20260330-1847"
| project timestamp, message, customDimensions.CustomerId, customDimensions.ElapsedMs
| order by timestamp asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  High-Performance Logging with Source Generators
&lt;/h3&gt;

&lt;p&gt;For functions processing thousands of messages per second (high-throughput queue or Event Hub triggers), the standard &lt;code&gt;LogInformation&lt;/code&gt; extension methods have measurable overhead: they box value-type arguments, allocate a &lt;code&gt;params object[]&lt;/code&gt; on every call, and parse the message template at runtime.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;[LoggerMessage]&lt;/code&gt; source generator&lt;/strong&gt; eliminates all three costs by generating strongly typed methods at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderLogs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Order {OrderId} received from {CustomerId}, total {OrderTotal}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OrderReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;orderTotal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Order {OrderId} retry attempt {RetryCount}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OrderRetrying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call these directly: &lt;code&gt;OrderLogs.OrderReceived(logger, message.OrderId, message.CustomerId, message.Total)&lt;/code&gt;. The generated code includes a log-level check before evaluating any arguments, so you pay zero cost when Information-level logging is disabled in production. Use this pattern on hot paths; for functions running a few times per minute, the standard extension methods are fine. The companion repo has source generator examples in both &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/HttpTriggerDemo/Logging/OrderLogs.cs" rel="noopener noreferrer"&gt;HttpTriggerDemo/Logging/OrderLogs.cs&lt;/a&gt; and &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/EventHubDemo/Logging/SensorLogs.cs" rel="noopener noreferrer"&gt;EventHubDemo/Logging/SensorLogs.cs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlation with BeginScope
&lt;/h3&gt;

&lt;p&gt;Individual log lines tell you what happened; &lt;strong&gt;BeginScope&lt;/strong&gt; ties related entries into a single operation. When you wrap your function body in a scope, every log entry inside it automatically inherits the scope's properties as &lt;code&gt;customDimensions&lt;/code&gt; in Application Insights.&lt;/p&gt;

&lt;p&gt;The critical detail: you must pass a &lt;code&gt;Dictionary&amp;lt;string, object&amp;gt;&lt;/code&gt; to &lt;code&gt;BeginScope&lt;/code&gt; for the keys to appear as individual &lt;code&gt;customDimensions&lt;/code&gt; columns. A plain string or a message template with arguments produces a single formatted string in the scope, which is much harder to query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;OrderMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OrderId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CustomerId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TenantId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Validating order"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ValidateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Charging payment"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ChargePaymentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order complete"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every log line inside that &lt;code&gt;using&lt;/code&gt; block now carries &lt;code&gt;OrderId&lt;/code&gt;, &lt;code&gt;CustomerId&lt;/code&gt;, and &lt;code&gt;TenantId&lt;/code&gt; in its &lt;code&gt;customDimensions&lt;/code&gt;, even the ones from &lt;code&gt;ValidateAsync&lt;/code&gt; and &lt;code&gt;ChargePaymentAsync&lt;/code&gt; (assuming they use the same &lt;code&gt;ILogger&lt;/code&gt; instance). This is how you trace a complete business operation across multiple internal methods without threading correlation IDs through every method signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Levels for Production
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;host.json&lt;/code&gt; &lt;strong&gt;logLevel&lt;/strong&gt; section controls which categories reach Application Insights. Two categories are easy to misconfigure, and getting them wrong silently breaks your monitoring dashboards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"logLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Aggregator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trace"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;Host.Results&lt;/code&gt;&lt;/strong&gt; feeds the &lt;code&gt;requests&lt;/code&gt; table. If you raise this above &lt;code&gt;Information&lt;/code&gt;, successful function executions stop appearing in the Application Insights Performance and Failures blades, and the Function Monitor tab in the portal goes blank. You lose your primary visibility into whether functions are running at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Host.Aggregator&lt;/code&gt;&lt;/strong&gt; feeds the &lt;code&gt;customMetrics&lt;/code&gt; table with aggregated counts and durations. Set it to &lt;code&gt;Trace&lt;/code&gt; so the runtime writes every batch. If you raise this to &lt;code&gt;Warning&lt;/code&gt; or higher, the function overview dashboard in the portal loses its success rate and duration charts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;default: Warning&lt;/code&gt; baseline keeps noise low for framework categories (&lt;code&gt;Microsoft.*&lt;/code&gt;, &lt;code&gt;Worker&lt;/code&gt;, &lt;code&gt;System.*&lt;/code&gt;) while &lt;code&gt;Function: Information&lt;/code&gt; ensures your own function logs reach Application Insights.&lt;/p&gt;

&lt;p&gt;When you need to change log levels without redeploying, override any &lt;code&gt;host.json&lt;/code&gt; value through app settings. The pattern replaces dots with double underscores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;AzureFunctionsJobHost__logging__logLevel__Function.ProcessOrder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes effect on the next function host restart (which happens automatically when you update an app setting) and lets you temporarily increase verbosity for a single function without touching &lt;code&gt;host.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sampling Configuration
&lt;/h3&gt;

&lt;p&gt;Application Insights enables &lt;strong&gt;adaptive sampling&lt;/strong&gt; by default, targeting 20 telemetry items per second per host. At low volume, you won't notice. At scale, sampling can silently discard traces, dependencies, and custom events before they reach your workspace.&lt;/p&gt;

&lt;p&gt;The recommended production configuration excludes &lt;code&gt;Request&lt;/code&gt; and &lt;code&gt;Exception&lt;/code&gt; from sampling, so you never lose function execution records or error details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"applicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"samplingSettings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"maxTelemetryItemsPerSecond"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"excludedTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Request;Exception"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check whether sampling is actively dropping data, run this KQL query. Any row where &lt;code&gt;TelemetrySavedPercentage&lt;/code&gt; is below 100 means that telemetry type is being sampled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;union traces, dependencies, requests
| where timestamp &amp;gt; ago(1d)
| summarize
    TelemetrySavedPercentage = round(100.0 / avg(itemCount), 1),
    TelemetryDroppedPercentage = round(100.0 - 100.0 / avg(itemCount), 1)
    by bin(timestamp, 1h), itemType
| where TelemetrySavedPercentage &amp;lt; 100
| order by timestamp desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;itemCount&lt;/code&gt; field on each telemetry item tells you how many similar items it represents. An &lt;code&gt;itemCount&lt;/code&gt; of 5 means Application Insights kept one item and estimated it represents five. If your &lt;code&gt;traces&lt;/code&gt; show 30% dropped, either raise &lt;code&gt;maxTelemetryItemsPerSecond&lt;/code&gt; or add &lt;code&gt;Trace&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; for the categories that matter most to your debugging workflow. Watch your ingestion costs, though: excluding too many types from sampling at high volume can push you past your daily data cap quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Traces and Metrics
&lt;/h2&gt;

&lt;p&gt;Once telemetry is flowing into Application Insights, you need to know where to look and what to ask. The portal gives you three entry points: Transaction Search for hunting specific executions, Log queries (KQL) for anything that requires aggregation or correlation, and the Application Map for a visual snapshot of your function app's dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction Search
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Transaction Search&lt;/strong&gt; is the fastest way to find what happened to a specific function execution. Open it from the left nav in your Application Insights resource, or use the shortcut from the Investigate section of the overview blade.&lt;/p&gt;

&lt;p&gt;The filters that matter most for Azure Functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation name&lt;/strong&gt;: the function name as registered in the runtime (e.g., &lt;code&gt;ProcessOrderFunction&lt;/code&gt;). Filter here when you want all executions of a specific function in a time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result code&lt;/strong&gt;: for HTTP triggers, this is the HTTP status code (&lt;code&gt;200&lt;/code&gt;, &lt;code&gt;500&lt;/code&gt;, etc.). For non-HTTP triggers (queue, timer, blob), &lt;code&gt;0&lt;/code&gt; means success and &lt;code&gt;1&lt;/code&gt; means failure. Combine with operation name to pull only failed runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time range&lt;/strong&gt;: narrow this first, before adding other filters. Application Insights searches can time out on broad time ranges at high volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click any result to open the &lt;strong&gt;End-to-end transaction view&lt;/strong&gt;. This is where distributed tracing pays off: you'll see the full execution timeline as a Gantt chart, with your function's request at the top and every outbound dependency (HTTP calls to payment APIs, SQL queries, Service Bus operations) shown as child spans with their durations. If a queue-triggered &lt;code&gt;ProcessOrderFunction&lt;/code&gt; call failed at 2am, this view tells you whether the failure was in your code, in a downstream HTTP call, or in a database query.&lt;/p&gt;

&lt;p&gt;One limitation: Transaction Search shows individual telemetry items, not aggregated data. If you want to know "how many orders failed between 1am and 3am and which customer IDs were affected", you need KQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  KQL Essentials for Azure Functions
&lt;/h3&gt;

&lt;p&gt;All four tables you'll use most (&lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;traces&lt;/code&gt;, &lt;code&gt;exceptions&lt;/code&gt;) share an &lt;code&gt;operation_Id&lt;/code&gt; column. That ID is the distributed trace ID that ties every log line, dependency call, and exception back to the single function invocation that produced them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding slow executions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;requests&lt;/code&gt; table records every function invocation. &lt;code&gt;duration&lt;/code&gt; is in milliseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(24h)
| where name == "ProcessOrderFunction"
| where duration &amp;gt; 5000
| project timestamp, id, duration, resultCode, operation_Id
| order by duration desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the slowest &lt;code&gt;ProcessOrderFunction&lt;/code&gt; executions in the last 24 hours. Swap &lt;code&gt;&amp;gt; 5000&lt;/code&gt; for whatever your SLA threshold is. The &lt;code&gt;operation_Id&lt;/code&gt; in each row is your entry point into the full trace for that execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracing a single request end-to-end&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You have an &lt;code&gt;operation_Id&lt;/code&gt; from a failed execution (from Transaction Search, from an alert, or from a support ticket). This query reconstructs everything that happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let traceId = "abc123def456";
union requests, dependencies, traces, exceptions
| where operation_Id == traceId
| project timestamp, itemType, name, message, duration, success, resultCode, customDimensions
| order by timestamp asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;union&lt;/code&gt; across all four tables is deliberate. A single function execution produces rows in multiple tables: a &lt;code&gt;requests&lt;/code&gt; row for the invocation itself, &lt;code&gt;dependencies&lt;/code&gt; rows for every outbound call, &lt;code&gt;traces&lt;/code&gt; rows for your &lt;code&gt;ILogger&lt;/code&gt; calls, and an &lt;code&gt;exceptions&lt;/code&gt; row if something threw. The &lt;code&gt;itemType&lt;/code&gt; column tells you which table each row came from.&lt;/p&gt;

&lt;p&gt;If you set up &lt;code&gt;BeginScope&lt;/code&gt; with &lt;code&gt;OrderId&lt;/code&gt; and &lt;code&gt;CustomerId&lt;/code&gt; as described in the logging section, those values appear in &lt;code&gt;customDimensions&lt;/code&gt; on every trace row. You can also work backwards from a business ID when you don't have an &lt;code&gt;operation_Id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;traces
| where timestamp &amp;gt; ago(24h)
| where customDimensions.OrderId == "ORD-20260330-1847"
| project operation_Id
| distinct operation_Id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take that &lt;code&gt;operation_Id&lt;/code&gt; and feed it into the union query above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counting failures by function name over time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(7d)
| where success == false
| summarize FailureCount = count() by bin(timestamp, 1h), name
| order by timestamp desc, FailureCount desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This surfaces which functions fail most, and whether failures cluster at specific times (a sign of a dependency being unhealthy during a maintenance window, or a batch job hitting a resource limit).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding dependency bottlenecks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your function may be fast; a downstream service may not be.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies
| where timestamp &amp;gt; ago(24h)
| where cloud_RoleName == "your-function-app-name"
| summarize
    CallCount = count(),
    P50 = percentile(duration, 50),
    P95 = percentile(duration, 95),
    P99 = percentile(duration, 99),
    FailureRate = round(100.0 * countif(success == false) / count(), 1)
    by target, type
| order by P95 desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;"your-function-app-name"&lt;/code&gt; with the value in your function app's Application Insights configuration (it defaults to the function app name). The &lt;code&gt;target&lt;/code&gt; column shows the external endpoint or database, and &lt;code&gt;type&lt;/code&gt; shows the dependency kind (&lt;code&gt;HTTP&lt;/code&gt;, &lt;code&gt;SQL&lt;/code&gt;, &lt;code&gt;Azure Service Bus&lt;/code&gt;, etc.). A high &lt;code&gt;P95&lt;/code&gt; with a low &lt;code&gt;FailureRate&lt;/code&gt; means the dependency is slow but not failing outright: the kind of problem that shows up as user-visible latency before it shows up as errors.&lt;/p&gt;

&lt;p&gt;One gotcha with KQL in the portal: queries run against a Log Analytics workspace, and there's a default query scope. If you open KQL from the Application Insights blade, you're automatically scoped to that resource's workspace. If you open it from a general Log Analytics workspace, you need to add &lt;code&gt;| where cloud_RoleName == "your-function-app-name"&lt;/code&gt; to every query, or you'll get results mixed across all resources in the workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Map
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Application Map&lt;/strong&gt; (left nav, under Investigate) renders your function app as a node and every dependency it calls as connected nodes. Each connection shows call volume, average duration, and failure rate. Nodes turn yellow when failure rates exceed roughly 20-30% and red above 50% (the thresholds aren't configurable).&lt;/p&gt;

&lt;p&gt;For a &lt;code&gt;ProcessOrderFunction&lt;/code&gt; that calls a payment API and writes to SQL, you'd see three nodes: your function app in the centre, the payment API to one side, and the SQL database to the other. The lines between them show call counts and P95 latency. If the payment API node is yellow, that's your first place to look during an incident.&lt;/p&gt;

&lt;p&gt;The map is useful for a quick health check and for onboarding new team members, but it has limits. It aggregates across all functions in the app, so if you have ten functions and one is hammering a slow dependency, the map shows the aggregate. It also doesn't distinguish between functions calling the same dependency: if both &lt;code&gt;ProcessOrderFunction&lt;/code&gt; and &lt;code&gt;RefundOrderFunction&lt;/code&gt; call the same SQL database, the database shows one aggregated node. For function-level dependency analysis, go back to the KQL query above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Alert rules&lt;/strong&gt; in Application Insights let you define a condition and trigger an action group (email, Teams webhook, PagerDuty, etc.) when the condition is met. You configure them under the Alerts section of your Application Insights resource.&lt;/p&gt;

&lt;p&gt;To create a failure rate alert for &lt;code&gt;ProcessOrderFunction&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;strong&gt;Create &amp;gt; Alert rule&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the signal to &lt;strong&gt;Custom log search&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use this KQL as the condition:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where name == "ProcessOrderFunction"
| where timestamp &amp;gt; ago(5m)
| summarize
    Total = count(),
    Failed = countif(success == false)
| extend FailureRate = round(100.0 * Failed / Total, 1)
| where FailureRate &amp;gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set evaluation frequency to every 1 minute, and the lookback window to 5 minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure the threshold: alert when the query returns any rows (meaning &lt;code&gt;FailureRate &amp;gt; 5&lt;/code&gt; for that window).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Action groups&lt;/strong&gt; are the notification mechanism. One action group can send email, post to a Teams incoming webhook, and call an Azure Automation runbook simultaneously. Define your on-call action group once, then reuse it across all alert rules.&lt;/p&gt;

&lt;p&gt;A few practical notes on alert tuning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with a 5-minute window and a 5% threshold, then tighten after you've seen a few weeks of baseline data. Alerting on 1-minute windows at 1% failure rate on a low-volume function produces a lot of noise for transient errors.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;requests&lt;/code&gt; table has a 1-2 minute ingestion delay under normal conditions and up to 5 minutes during ingestion spikes. A 5-minute lookback window accounts for this. A 1-minute window can miss failures entirely if ingestion is delayed.&lt;/li&gt;
&lt;li&gt;For queue-triggered functions, complement failure rate alerts with a &lt;strong&gt;queue depth alert&lt;/strong&gt; on the source queue (configured through Azure Monitor metrics, not Application Insights). A growing queue combined with low invocation count means your function is failing at startup, before it even executes: a scenario that produces no &lt;code&gt;requests&lt;/code&gt; rows and won't trigger a failure rate alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Issues and Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "My logs aren't showing up in Application Insights"
&lt;/h3&gt;

&lt;p&gt;It comes down to one of four things, and you can rule them out in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the connection string first.&lt;/strong&gt; Open your function app in the portal, go to Configuration, and confirm &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt; is set and points to the right resource. If it's missing or set to an instrumentation key only (the &lt;code&gt;InstrumentationKey=&amp;lt;guid&amp;gt;&lt;/code&gt; format without an &lt;code&gt;IngestionEndpoint&lt;/code&gt;), nothing reaches Application Insights at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the worker log level config.&lt;/strong&gt; As covered in the two-pipeline gotcha above: &lt;code&gt;host.json&lt;/code&gt; controls the host process, but your &lt;code&gt;Program.cs&lt;/code&gt; or &lt;code&gt;appsettings.json&lt;/code&gt; controls the worker. If you haven't explicitly configured the worker's &lt;code&gt;ApplicationInsights&lt;/code&gt; log level, the SDK default applies: &lt;code&gt;Warning&lt;/code&gt; only. Add this to your &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or remove the default filter rule entirely in &lt;code&gt;Program.cs&lt;/code&gt;, as shown in the setup section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check sampling.&lt;/strong&gt; If requests appear but traces for specific functions don't, sampling may be discarding them. Run the KQL query from the sampling section to see which telemetry types are being dropped. Add &lt;code&gt;Trace&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt; if you need full trace fidelity for a critical function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the &lt;code&gt;Function&lt;/code&gt; log level in &lt;code&gt;host.json&lt;/code&gt;.&lt;/strong&gt; If &lt;code&gt;Function&lt;/code&gt; is set to &lt;code&gt;Warning&lt;/code&gt; or higher, &lt;code&gt;LogInformation&lt;/code&gt; calls from your function code never leave the host. Set it to &lt;code&gt;Information&lt;/code&gt; to restore them.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Dependencies are missing from the Application Map"
&lt;/h3&gt;

&lt;p&gt;When your Application Map shows your function app as an isolated node with no outbound edges, check for a missing &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; call in &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AddApplicationInsightsTelemetryWorkerService()&lt;/code&gt; registers the SDK. &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; is what connects the Functions runtime's &lt;code&gt;ActivitySource&lt;/code&gt; to that SDK so outbound HTTP, SQL, and Azure SDK calls produce dependency telemetry with the correct operation IDs. Without it, dependencies are either not tracked at all or tracked with broken correlation (they appear in the &lt;code&gt;dependencies&lt;/code&gt; table but don't link back to the parent request).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Required for dependency tracking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both calls are present and you're still missing HTTP dependencies: check how &lt;code&gt;HttpClient&lt;/code&gt; is registered. The Application Insights SDK instruments &lt;code&gt;HttpClient&lt;/code&gt; via &lt;code&gt;IHttpClientFactory&lt;/code&gt;. If you're creating &lt;code&gt;HttpClient&lt;/code&gt; instances with &lt;code&gt;new HttpClient()&lt;/code&gt; directly instead of injecting an &lt;code&gt;IHttpClientFactory&lt;/code&gt;-managed instance, those calls bypass the instrumentation entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Not tracked&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Tracked (inject IHttpClientFactory via primary constructor)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  "I see duplicate telemetry for every request"
&lt;/h3&gt;

&lt;p&gt;In the isolated worker model, both the host process and the worker process can emit telemetry for the same function invocation. When both are sending to the same Application Insights resource, you get duplicate &lt;code&gt;requests&lt;/code&gt; entries, inflated counts, and misleading failure rates.&lt;/p&gt;

&lt;p&gt;This is controlled by the &lt;code&gt;telemetryMode&lt;/code&gt; setting at the &lt;strong&gt;root level&lt;/strong&gt; of &lt;code&gt;host.json&lt;/code&gt; (not inside &lt;code&gt;logging&lt;/code&gt;). The default is &lt;code&gt;default&lt;/code&gt;, which allows both sides to emit. Setting it to &lt;code&gt;OpenTelemetry&lt;/code&gt; resolves the duplication, but note that when you do, the &lt;code&gt;logging.applicationInsights&lt;/code&gt; section of &lt;code&gt;host.json&lt;/code&gt; no longer applies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"telemetryMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenTelemetry"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, suppress host-side request telemetry while keeping your worker-side telemetry by raising &lt;code&gt;Host.Results&lt;/code&gt; above &lt;code&gt;Information&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt;'s &lt;code&gt;logLevel&lt;/code&gt; section. The tradeoff: this also removes successful execution records from the portal's Function Monitor tab. Use &lt;code&gt;telemetryMode&lt;/code&gt; when you want clean deduplication without losing host-side visibility.&lt;/p&gt;

&lt;p&gt;To confirm duplication before changing anything, run this in KQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(1h)
| summarize count() by operation_Id
| where count_ &amp;gt; 1
| order by count_ desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any &lt;code&gt;operation_Id&lt;/code&gt; appearing more than once is a duplicated invocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Cold start latency spikes in my metrics"
&lt;/h3&gt;

&lt;p&gt;Cold starts produce latency spikes that look identical to slow execution in your metrics. Before investigating application code, confirm whether a spike is a cold start or an actual regression.&lt;/p&gt;

&lt;p&gt;A cold start request carries a specific pattern: high latency on the first invocation from a given instance, with subsequent requests from the same instance running at normal duration. The &lt;code&gt;cloud_RoleInstance&lt;/code&gt; dimension on each request record identifies the instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(24h)
| where name == "ProcessOrderFunction"
| summarize
    first_request = min(timestamp),
    p50 = percentile(duration, 50),
    p99 = percentile(duration, 99),
    request_count = count()
    by cloud_RoleInstance
| extend is_cold_start_instance = (request_count &amp;lt;= 3)
| order by first_request desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instances where &lt;code&gt;request_count&lt;/code&gt; is 1 or 2 are almost certainly fresh scale-out instances, and their durations are not representative of your steady-state performance. Filter them out when computing your SLA metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(24h)
| where name == "ProcessOrderFunction"
| join kind=inner (
    requests
    | summarize request_count = count() by cloud_RoleInstance
    | where request_count &amp;gt; 5
) on cloud_RoleInstance
| summarize p50 = percentile(duration, 50), p99 = percentile(duration, 99)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the spike appears in warm instances, you have a real slowdown. If it's limited to fresh instances appearing after a scale-out event, it's cold start behavior. The two require different responses: cold starts call for pre-warming strategies or Consumption to Premium plan migration; actual slowdowns point to profiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Alerts fire but I can't find the failing requests"
&lt;/h3&gt;

&lt;p&gt;You set up an alert on exception count, it fires, you open the Failures blade, and the requests that caused the exceptions are gone. Sampling is discarding the evidence.&lt;/p&gt;

&lt;p&gt;By default, &lt;code&gt;Exception&lt;/code&gt; telemetry is sampled alongside everything else. When the SDK keeps one exception and estimates it represents five, the other four are discarded permanently. Your alert fires because the metric aggregation (which runs before sampling discards anything) saw all five. Your query returns only the one that survived.&lt;/p&gt;

&lt;p&gt;The fix is to exclude &lt;code&gt;Exception&lt;/code&gt; from sampling in &lt;code&gt;host.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"applicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"samplingSettings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"maxTelemetryItemsPerSecond"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"excludedTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Request;Exception"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;Request&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; ensures the parent request record is also always kept, so you can correlate the exception back to its invocation through the &lt;code&gt;operation_Id&lt;/code&gt;. Without both, you may find the exception but not the request that caused it.&lt;/p&gt;

&lt;p&gt;If the alert is on a custom metric rather than exceptions, check whether &lt;code&gt;customMetrics&lt;/code&gt; is being sampled. Custom metrics emitted through &lt;code&gt;TelemetryClient.GetMetric()&lt;/code&gt; are not affected by sampling (they're pre-aggregated in the SDK before sending). Custom events emitted with &lt;code&gt;TelemetryClient.TrackEvent()&lt;/code&gt; are sampled, and alerts based on custom event counts can suffer the same problem. Add &lt;code&gt;Event&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; if that's your signal source.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenTelemetry Alternative
&lt;/h2&gt;

&lt;p&gt;The classic Application Insights SDK works well if your entire stack lives in Azure. But if you need to send telemetry to Grafana, Datadog, Jaeger, or any other backend alongside (or instead of) Azure Monitor, you're duplicating instrumentation code for each target. &lt;strong&gt;OpenTelemetry&lt;/strong&gt; solves this at the protocol level: one set of instrumentation, one exporter interface, multiple backends.&lt;/p&gt;

&lt;p&gt;OpenTelemetry is a CNCF project that defines a vendor-neutral API and wire format (OTLP) for traces, metrics, and logs. The same instrumentation code that sends data to Application Insights can also send to Zipkin or Collector pipelines with a config change, not a code rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Setup
&lt;/h3&gt;

&lt;p&gt;Microsoft publishes the &lt;strong&gt;&lt;code&gt;Microsoft.Azure.Functions.Worker.OpenTelemetry&lt;/code&gt;&lt;/strong&gt; package for this purpose, paired with the Azure Monitor exporter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.Azure.Functions.Worker.OpenTelemetry
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package Azure.Monitor.OpenTelemetry.Exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, enable OpenTelemetry output from the Functions host by adding &lt;code&gt;"telemetryMode": "OpenTelemetry"&lt;/code&gt; at the root of your &lt;code&gt;host.json&lt;/code&gt; (the same setting described in the duplicate telemetry section). Then the &lt;code&gt;Program.cs&lt;/code&gt; registration replaces the classic SDK calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.Monitor.OpenTelemetry.Exporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.OpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseFunctionsWorkerDefaults&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAzureMonitorExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// reads APPLICATIONINSIGHTS_CONNECTION_STRING automatically&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UseFunctionsWorkerDefaults()&lt;/code&gt; hooks into the Functions runtime's &lt;code&gt;ActivitySource&lt;/code&gt; for proper distributed trace correlation (the OpenTelemetry equivalent of &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; from the classic SDK). Without it, dependency telemetry won't correlate back to the parent function invocation. See the full setup in &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/EventHubDemo/Program.cs" rel="noopener noreferrer"&gt;EventHubDemo/Program.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The connection string is still required; &lt;code&gt;UseAzureMonitorExporter()&lt;/code&gt; reads &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt; from the environment the same way the classic SDK does. If you also want to export to a second backend (requires the additional &lt;code&gt;OpenTelemetry.Exporter.OpenTelemetryProtocol&lt;/code&gt; package), register it separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseFunctionsWorkerDefaults&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAzureMonitorExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTracing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tracing&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tracing&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOtlpExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;otlp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;otlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:4317"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UseAzureMonitorExporter()&lt;/code&gt; is a cross-cutting registration that configures all signals at once. Chaining signal-specific exporters like &lt;code&gt;AddOtlpExporter&lt;/code&gt; after it in the same builder can throw a &lt;code&gt;NotSupportedException&lt;/code&gt;. Separate &lt;code&gt;AddOpenTelemetry()&lt;/code&gt; calls avoid the conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Gain
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;W3C Trace Context&lt;/strong&gt; is the default propagation format, which means your distributed traces correlate correctly with other OpenTelemetry-instrumented services regardless of what backend they report to. With the classic SDK you get this too, but only within the Application Insights ecosystem; outside it, the format diverges.&lt;/p&gt;

&lt;p&gt;You also get &lt;strong&gt;multi-backend export&lt;/strong&gt;: Azure Monitor for your ops team, a Grafana stack for your platform team, and a local Collector for local debugging, all from the same process. And if you ever migrate off Azure Monitor entirely, you replace one exporter registration, not every &lt;code&gt;TelemetryClient&lt;/code&gt; call in your codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Lose Today
&lt;/h3&gt;

&lt;p&gt;The OpenTelemetry path is not at feature parity with the classic SDK for Functions specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Metrics&lt;/strong&gt; (the real-time stream at &lt;code&gt;monitor.azure.com&lt;/code&gt;) does not work with the distro. It relies on a proprietary push mechanism in the classic SDK that has no OpenTelemetry equivalent yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot Debugger&lt;/strong&gt; is unavailable. It's a classic SDK feature with no OTLP counterpart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-collection gaps&lt;/strong&gt;: some dependency types that the classic SDK instruments automatically (certain Azure SDK operations, Service Bus settlement calls) may not be captured out of the box, depending on which OpenTelemetry instrumentation libraries you've added. You may need to add &lt;code&gt;AddAzureClientsInstrumentation()&lt;/code&gt; or equivalent packages explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: the distro's documentation for Functions scenarios specifically is thin. Most samples target ASP.NET Core web apps; you'll spend time adapting them and testing whether auto-collection works for your trigger types.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Choose Which
&lt;/h3&gt;

&lt;p&gt;Use the &lt;strong&gt;classic SDK&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your entire workload runs on Azure and you have no multi-vendor requirements&lt;/li&gt;
&lt;li&gt;you need Live Metrics or Snapshot Debugger&lt;/li&gt;
&lt;li&gt;you want the richest out-of-the-box experience with the Application Insights portal today&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;strong&gt;OpenTelemetry&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you're sending telemetry to multiple backends, or planning to&lt;/li&gt;
&lt;li&gt;the rest of your services are already OpenTelemetry-instrumented and you need consistent trace propagation across the board&lt;/li&gt;
&lt;li&gt;you're building something that might not always live in Azure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're greenfield on a purely Azure stack, the classic SDK is less configuration for the same result right now. If you're instrumenting a heterogeneous system or building for portability, OpenTelemetry's overhead is worth it; you pay once at setup and gain the flexibility when requirements change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is Part 9 and the final article in the core series. Over nine weeks, this series went from "what is serverless" to querying production telemetry in KQL. If you followed along and built something, you now have a function app with HTTP and queue triggers, proper configuration with Key Vault, a CI/CD pipeline through GitHub Actions, and Application Insights wired up for structured logging, distributed tracing, and alerting. That covers the full lifecycle: build, test, deploy, monitor.&lt;/p&gt;

&lt;p&gt;The companion repository at &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; has working code for every article in the series. Clone it, break things, wire up your own alerts.&lt;/p&gt;

&lt;p&gt;Next week is a bonus article outside the core series: production cost realities on the Consumption plan, and the signals that tell you it's time to move to Flex Consumption or Premium. If you've ever wondered why your monthly bill looked nothing like the pricing calculator, that one is for you.&lt;/p&gt;

&lt;p&gt;When you first wired up monitoring on a production function app, which alert did you set up first: failure rate or latency?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 9: Azure Functions Observability: From Blind Spots to Production Clarity (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Deploying to Azure: CI/CD with GitHub Actions</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 27 Mar 2026 06:36:06 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m</link>
      <guid>https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 8: Deploying to Azure: CI/CD with GitHub Actions (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Introduction: from local to production
&lt;/h2&gt;

&lt;p&gt;Local tooling hides four things you have to own in production: packaging, authentication, configuration injection, and rollback. &lt;code&gt;func start&lt;/code&gt; handles all of them silently; a CI/CD pipeline does not, and the decisions you make about each one compound quickly.&lt;/p&gt;

&lt;p&gt;The gap is easy to miss. Your local environment reads from &lt;code&gt;local.settings.json&lt;/code&gt;, authenticates with your personal identity, and recovers from bad deploys by letting you just restart. Azure does none of that for you. You need a packaging step, a way to authenticate from a pipeline without storing secrets, a strategy for injecting environment-specific configuration, and some mechanism for rolling back when a deploy breaks something.&lt;/p&gt;

&lt;p&gt;This article covers two stages of that journey. First, manual deployment using the Azure CLI and the Functions Core Tools: useful for quick validation and understanding what the automated pipeline will do under the hood. Then a GitHub Actions workflow with two jobs, OIDC authentication (no stored credentials in your repository), deployment slots for zero-downtime releases, and configuration management that keeps secrets out of your pipeline definition entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual deployment options
&lt;/h2&gt;

&lt;p&gt;Before wiring up a full CI/CD pipeline, understand what actually happens when code reaches Azure. Manual deployment gives you that visibility, and it remains useful long after you've automated everything: for one-off hotfixes, for validating a packaging issue, or for deploying to a scratch environment without spinning up a workflow run.&lt;/p&gt;

&lt;h3&gt;
  
  
  func azure functionapp publish
&lt;/h3&gt;

&lt;p&gt;The Core Tools command is the closest thing to a one-stop deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func azure functionapp publish &amp;lt;APP_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, it runs &lt;code&gt;dotnet build --output bin/publish&lt;/code&gt;, creates a &lt;code&gt;.zip&lt;/code&gt; archive (filtered by your &lt;code&gt;.funcignore&lt;/code&gt;), uploads the archive via the Kudu ZipDeploy API (or One Deploy for Flex Consumption plans), and then syncs triggers and restarts the host. By default it also sets &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE=1&lt;/code&gt; on the app, covered in the next subsection.&lt;/p&gt;

&lt;p&gt;Flags you'll reach for regularly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Skip the local build — useful when you've already built in CI&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--no-build&lt;/span&gt;

&lt;span class="c"&gt;# Deploy to a staging slot instead of production&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--slot&lt;/span&gt; staging

&lt;span class="c"&gt;# Push local.settings.json values to app settings (prompts for confirmation)&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--publish-local-settings&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;

&lt;span class="c"&gt;# Verify what files will be included before committing to a deploy&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--list-included-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;--list-included-files&lt;/code&gt; at least once per project. If your archive includes &lt;code&gt;bin/&lt;/code&gt; debug artifacts, test assemblies, or secrets you meant to &lt;code&gt;.funcignore&lt;/code&gt;, you want to catch that before it's sitting on a production host.&lt;/p&gt;

&lt;p&gt;A minimal &lt;code&gt;.funcignore&lt;/code&gt; for a .NET project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*.csproj
*.sln
.git/
.vscode/
local.settings.json
test/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;local.settings.json&lt;/code&gt; is the most important exclusion: it often contains connection strings and keys meant for local development only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure CLI: two commands, two APIs
&lt;/h3&gt;

&lt;p&gt;The Azure CLI gives you two distinct options, and picking the wrong one for your plan type will fail silently or throw a confusing error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Kudu ZipDeploy — works for Consumption, Premium, and Dedicated plans&lt;/span&gt;
az functionapp deployment &lt;span class="nb"&gt;source &lt;/span&gt;config-zip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; &amp;lt;RESOURCE_GROUP&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--src&lt;/span&gt; ./publish.zip

&lt;span class="c"&gt;# One Deploy API — required for Flex Consumption, also valid elsewhere&lt;/span&gt;
az functionapp deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; &amp;lt;RESOURCE_GROUP&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--src-path&lt;/span&gt; ./publish.zip &lt;span class="nt"&gt;--type&lt;/span&gt; zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The older &lt;code&gt;config-zip&lt;/code&gt; command talks directly to Kudu and does no building; you're responsible for providing a publish-ready zip. It does not support &lt;strong&gt;Flex Consumption&lt;/strong&gt;, the newer serverless plan that bypasses Kudu entirely. If you're on Flex Consumption, &lt;code&gt;az functionapp deploy&lt;/code&gt; is the only CLI path that works. It also gives you &lt;code&gt;--clean&lt;/code&gt; to remove files not in the archive and &lt;code&gt;--async&lt;/code&gt; to return immediately without polling for completion.&lt;/p&gt;

&lt;p&gt;A rule of thumb: if you're writing a deploy script that needs to work across plan types, use &lt;code&gt;az functionapp deploy&lt;/code&gt;. If you're on a legacy plan and &lt;code&gt;config-zip&lt;/code&gt; already exists in your runbooks, it's fine to leave it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run-From-Package and why it matters
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE=1&lt;/code&gt; is set, Azure mounts your zip archive as a read-only filesystem at &lt;code&gt;wwwroot&lt;/code&gt; rather than extracting files into it. This is the default behavior when you publish with Core Tools, and it has real production benefits: deployment is &lt;strong&gt;atomic&lt;/strong&gt; (the old package stays mounted until the new one is ready), file-copy locking errors disappear, and cold start times improve because the runtime reads directly from the zip.&lt;/p&gt;

&lt;p&gt;The constraints: &lt;code&gt;wwwroot&lt;/code&gt; becomes read-only (portal-based editing no longer works), the archive has a 1 GB limit, and you should not set this value on Flex Consumption plans, which manage packages differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which method to use
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7lw0jrcxz5vedkpmv3s6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7lw0jrcxz5vedkpmv3s6.png" alt="Which method to use: func publish for development, config-zip for legacy scripts, az functionapp deploy for all plans" width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For anything beyond a one-off fix or an afternoon prototype, these manual commands are the foundation you'll extract into a pipeline. Knowing what each one does makes the GitHub Actions steps in the next section easier to reason about when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions workflow setup
&lt;/h2&gt;

&lt;p&gt;The pieces fit together like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22buyjo6967rdfy4zdab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22buyjo6967rdfy4zdab.png" alt="Deployment pipeline: Push to main, Build Job, Deploy Job with OIDC Login, Deploy to Staging Slot, Swap to Production" width="800" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The build job produces a single artifact. The deploy job authenticates via OIDC, pushes to a staging slot, and swaps it into production.&lt;/p&gt;

&lt;p&gt;The complete workflow is below. Read through it first; the walkthrough after explains the decisions behind each piece.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Azure Functions (.NET 10)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10.0.x'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet restore --locked-mode&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;dotnet publish src/MyFunctionApp&lt;/span&gt;
          &lt;span class="s"&gt;--configuration Release&lt;/span&gt;
          &lt;span class="s"&gt;--output ./output&lt;/span&gt;
          &lt;span class="s"&gt;--runtime linux-x64&lt;/span&gt;
          &lt;span class="s"&gt;--self-contained true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;function-app&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;function-app&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;subscription-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_SUBSCRIPTION_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure/functions-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.FUNCTION_APP_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;slot-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
          &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swap staging to production&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/cli@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;az functionapp deployment slot swap \&lt;/span&gt;
              &lt;span class="s"&gt;--name ${{ vars.FUNCTION_APP_NAME }} \&lt;/span&gt;
              &lt;span class="s"&gt;--resource-group ${{ vars.RESOURCE_GROUP }} \&lt;/span&gt;
              &lt;span class="s"&gt;--slot staging \&lt;/span&gt;
              &lt;span class="s"&gt;--target-slot production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same function app from Parts 1 through 7. The complete source is in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository. Every push to &lt;code&gt;main&lt;/code&gt; builds it, deploys to a staging slot, and swaps to production. No secrets stored, no manual steps, and a rollback is one swap away.&lt;/p&gt;

&lt;p&gt;If your plan doesn't support slots (Consumption with only one slot available, or Flex Consumption), remove the &lt;code&gt;slot-name&lt;/code&gt; parameter and the swap step. The &lt;code&gt;functions-action&lt;/code&gt; will deploy directly to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why two jobs instead of one
&lt;/h3&gt;

&lt;p&gt;The split between &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;deploy&lt;/code&gt; exists for two reasons.&lt;/p&gt;

&lt;p&gt;First, the artifact produced by &lt;code&gt;build&lt;/code&gt; is reusable. If you add a staging environment later, the deploy job can run twice against the same artifact without rebuilding. Build once, deploy to as many environments as you need.&lt;/p&gt;

&lt;p&gt;Second, the &lt;strong&gt;&lt;code&gt;id-token: write&lt;/code&gt; permission&lt;/strong&gt; required for OIDC authentication (covered in the next section) is scoped to the &lt;code&gt;deploy&lt;/code&gt; job only. If you set it at the workflow level, every job gets that elevated permission. Keeping it on the deploy job limits the blast radius if something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  The build job
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;actions/checkout@v4&lt;/code&gt; pulls your code. &lt;code&gt;actions/setup-dotnet@v4&lt;/code&gt; installs the SDK, and the &lt;code&gt;cache: true&lt;/code&gt; option tells it to cache the NuGet package cache between runs.&lt;/p&gt;

&lt;p&gt;That cache only works if your project has a &lt;strong&gt;lock file&lt;/strong&gt;. Add this to your &lt;code&gt;.csproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;RestorePackagesWithLockFile&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/RestorePackagesWithLockFile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then commit the generated &lt;code&gt;packages.lock.json&lt;/code&gt;. Without it, &lt;code&gt;cache: true&lt;/code&gt; has nothing to hash (so every run misses the cache), and &lt;code&gt;--locked-mode&lt;/code&gt; silently regenerates a new lock file instead of validating against a committed one. With both in place, clean builds skip the network entirely for packages that haven't changed.&lt;/p&gt;

&lt;p&gt;The publish step is where .NET 10 requires extra care:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet publish src/MyFunctionApp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; ./output &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime&lt;/span&gt; linux-x64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--self-contained&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--self-contained true&lt;/code&gt; is required for .NET 10. The Azure Functions v4 host runs on .NET 8. If you publish a &lt;strong&gt;framework-dependent&lt;/strong&gt; app targeting .NET 10, the host cannot find the .NET 10 runtime and the deployment fails with exit code 150 (&lt;code&gt;0x96&lt;/code&gt;). A self-contained publish bundles the runtime with your app, so the host's .NET version becomes irrelevant.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;actions/upload-artifact@v4&lt;/code&gt; takes the &lt;code&gt;./output&lt;/code&gt; folder and makes it available to downstream jobs. The &lt;code&gt;name&lt;/code&gt; value (&lt;code&gt;function-app&lt;/code&gt;) is how the deploy job will refer to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The deploy job
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;needs: build&lt;/code&gt; means this job waits for the build to succeed before starting. &lt;code&gt;environment: production&lt;/code&gt; ties the job to a GitHub environment, which lets you add required reviewers or protection rules before any deployment proceeds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;actions/download-artifact@v4&lt;/code&gt; retrieves the artifact by the same name used during upload and places it in &lt;code&gt;./output&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;azure/login@v2&lt;/code&gt; handles authentication using OIDC; the specifics of how to configure this are in the next section. This step must come before &lt;code&gt;functions-action&lt;/code&gt;, and the three secrets (&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;, &lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;, &lt;code&gt;AZURE_SUBSCRIPTION_ID&lt;/code&gt;) must be set in your repository or environment settings.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Azure/functions-action@v1&lt;/code&gt; does the actual deployment. Two parameters are required: &lt;code&gt;app-name&lt;/code&gt; (the name of your Function App in Azure) and &lt;code&gt;package&lt;/code&gt; (the path to your artifact). An optional &lt;code&gt;slot-name&lt;/code&gt; parameter targets a deployment slot if you are using them.&lt;/p&gt;

&lt;p&gt;The deployment method the action uses depends on your hosting plan. Flex Consumption plans use &lt;strong&gt;One Deploy&lt;/strong&gt;; all other plans use &lt;strong&gt;Zip Deploy&lt;/strong&gt;. The action picks this automatically based on your app's plan type, so you do not need to configure it explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  OIDC authentication (no stored secrets)
&lt;/h2&gt;

&lt;p&gt;The workflow above uses three secrets: &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;, &lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;, and &lt;code&gt;AZURE_SUBSCRIPTION_ID&lt;/code&gt;. None of them are actual credentials. That's the point of OIDC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not publish profiles or service principal secrets?
&lt;/h3&gt;

&lt;p&gt;Publish profiles are XML files containing deployment credentials baked into the Function App. They work, but they create problems at scale: they can't be scoped to a branch or environment, they don't expire on a schedule, and if one leaks, anyone with the file can deploy to your app until you manually reset it.&lt;/p&gt;

&lt;p&gt;Service principal secrets are better (they support expiration and RBAC scoping), but you still have a secret stored in GitHub that needs rotating every 6-24 months. Miss a rotation and your pipeline breaks silently on the next deploy.&lt;/p&gt;

&lt;p&gt;OIDC eliminates stored credentials entirely. GitHub mints a short-lived token for each workflow run, Azure validates that token against a federated credential you configure once, and nothing secret ever sits in your repository settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Your workflow requests an OIDC token from GitHub's token service&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;azure/login&lt;/code&gt; action sends that token to Microsoft Entra ID&lt;/li&gt;
&lt;li&gt;Entra validates the token's issuer (&lt;code&gt;token.actions.githubusercontent.com&lt;/code&gt;), audience, and subject claim (which encodes the repo, branch, and environment)&lt;/li&gt;
&lt;li&gt;If the claims match your federated credential configuration, Entra issues an Azure access token&lt;/li&gt;
&lt;li&gt;The access token is used for the deployment, then expires&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The subject claim is what makes this granular. You can restrict a credential to only work from a specific environment (&lt;code&gt;repo:your-org/your-repo:environment:production&lt;/code&gt;), a specific branch, or even pull requests. A token minted from a feature branch won't match a credential scoped to the &lt;code&gt;production&lt;/code&gt; environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup steps
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Create an Entra app registration with a service principal:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad app create &lt;span class="nt"&gt;--display-name&lt;/span&gt; &lt;span class="s2"&gt;"github-deploy-my-func-app"&lt;/span&gt;
az ad sp create &lt;span class="nt"&gt;--id&lt;/span&gt; &amp;lt;APP_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Assign the &lt;code&gt;Website Contributor&lt;/code&gt; role&lt;/strong&gt; scoped to the resource group containing your Function App:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee&lt;/span&gt; &amp;lt;APP_ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Website Contributor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; /subscriptions/&amp;lt;SUB_ID&amp;gt;/resourceGroups/&amp;lt;RG_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Website Contributor&lt;/code&gt; is enough for deploying code. &lt;code&gt;Contributor&lt;/code&gt; works too but grants more access than the pipeline needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Configure a federated identity credential:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github-actions-production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:your-org/your-repo:environment:production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audiences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad app federated-credential create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; &amp;lt;APP_ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; @credential.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;subject&lt;/code&gt; field must match exactly. If your deploy job uses &lt;code&gt;environment: production&lt;/code&gt;, the subject must end with &lt;code&gt;:environment:production&lt;/code&gt;. If you deploy from a branch without an environment, use &lt;code&gt;:ref:refs/heads/main&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Store the IDs as GitHub environment secrets:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to your repository Settings &amp;gt; Environments &amp;gt; production &amp;gt; Environment secrets, and add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;: the Application (client) ID from your app registration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;: your Entra tenant ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AZURE_SUBSCRIPTION_ID&lt;/code&gt;: the subscription containing your Function App&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are identifiers, not credentials. Even if someone reads them, they can't authenticate without a valid OIDC token from your specific repository and environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow permissions
&lt;/h3&gt;

&lt;p&gt;The deploy job needs &lt;code&gt;id-token: write&lt;/code&gt; to mint the OIDC token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set this on the deploy job only, not at the workflow level. The build job doesn't need token-minting permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  One gotcha
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Azure/functions-action&lt;/code&gt; supports two authentication methods: &lt;code&gt;publish-profile&lt;/code&gt; and the &lt;code&gt;azure/login&lt;/code&gt; action. They are &lt;strong&gt;mutually exclusive&lt;/strong&gt;. If you pass a &lt;code&gt;publish-profile&lt;/code&gt; parameter while also using &lt;code&gt;azure/login&lt;/code&gt;, the action uses the publish profile and ignores your OIDC session. Remove the &lt;code&gt;publish-profile&lt;/code&gt; parameter entirely when switching to OIDC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment slots and zero-downtime releases
&lt;/h2&gt;

&lt;p&gt;Deploying directly to production means every release has a moment where either the old code or the new code is partially running. Deployment slots give you a staging URL to validate before any production traffic sees the new version, and an instant rollback if something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each plan supports
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcae24fmuypp8l6htr2vs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcae24fmuypp8l6htr2vs.png" alt="Deployment slots by plan: Consumption 2, Premium 3, Dedicated 1-20, Flex Consumption not supported" width="466" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're on Flex Consumption, skip to the rolling updates section below.&lt;/p&gt;

&lt;h3&gt;
  
  
  The blue-green pattern
&lt;/h3&gt;

&lt;p&gt;Deploy to a staging slot, verify it works, then swap staging into production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy to staging&lt;/strong&gt;: your CI/CD pipeline targets the &lt;code&gt;staging&lt;/code&gt; slot instead of production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt;: hit the staging URL (&lt;code&gt;your-func-app-staging.azurewebsites.net&lt;/code&gt;) with smoke tests or manual checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap&lt;/strong&gt;: Azure switches the routing so staging serves production traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback if needed&lt;/strong&gt;: swap again to revert (the old production code is now in the staging slot)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The swap itself takes seconds. Your users see either the old version or the new version, never a half-deployed state.&lt;/p&gt;

&lt;h3&gt;
  
  
  What swaps and what stays
&lt;/h3&gt;

&lt;p&gt;This trips people up. During a swap, &lt;strong&gt;code and most settings travel together&lt;/strong&gt; from staging to production. But some things are pinned to the slot:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Travels with code (gets swapped):&lt;/strong&gt; general app settings (unless marked sticky), connection strings (unless marked sticky), handler mappings, public certificates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stays with the slot:&lt;/strong&gt; publishing endpoints, custom domains, TLS/SSL certificates, scale settings, IP restrictions, Always On, &lt;code&gt;FUNCTIONS_EXTENSION_VERSION&lt;/code&gt; (sticky by default).&lt;/p&gt;

&lt;h3&gt;
  
  
  Sticky settings that cause problems
&lt;/h3&gt;

&lt;p&gt;Two settings deserve special attention:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;FUNCTIONS_EXTENSION_VERSION&lt;/code&gt;&lt;/strong&gt; is sticky by default. If your staging slot runs &lt;code&gt;~4&lt;/code&gt; and production also runs &lt;code&gt;~4&lt;/code&gt;, this is invisible. But if you ever need to change the version, the stickiness means the setting won't swap with the code. To make it travel with the swap, set &lt;code&gt;WEBSITE_OVERRIDE_STICKY_EXTENSION_VERSIONS=0&lt;/code&gt; on &lt;strong&gt;all&lt;/strong&gt; slots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;WEBSITE_CONTENTSHARE&lt;/code&gt;&lt;/strong&gt; is auto-generated per slot and should never be set manually. Each slot needs its own content share to avoid file locking conflicts. If you see deployment failures mentioning "cannot access file," check whether slots are sharing this value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy-to-slot and swap in GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;slot-name&lt;/code&gt; to the deploy step, then swap using the Azure CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure/functions-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-func-app'&lt;/span&gt;
    &lt;span class="na"&gt;slot-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
    &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swap staging to production&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/cli@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;az functionapp deployment slot swap \&lt;/span&gt;
        &lt;span class="s"&gt;--name my-func-app \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group my-rg \&lt;/span&gt;
        &lt;span class="s"&gt;--slot staging \&lt;/span&gt;
        &lt;span class="s"&gt;--target-slot production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Swap gotchas
&lt;/h3&gt;

&lt;p&gt;Watch for these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running functions are terminated&lt;/strong&gt; during a swap. There is no graceful drain. If you have long-running executions, they will be killed. For timer or queue triggers, the runtime will pick up incomplete work after the swap, but HTTP requests in flight will fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm-up matters.&lt;/strong&gt; After a swap, the new production instances need to initialize. Set &lt;code&gt;WEBSITE_SWAP_WARMUP_PING_PATH&lt;/code&gt; to an endpoint (like a health check) that forces initialization before traffic arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep app names under 32 characters.&lt;/strong&gt; Longer names can cause host ID collisions between slots, leading to unexpected behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Flex Consumption alternative: rolling updates
&lt;/h3&gt;

&lt;p&gt;Flex Consumption doesn't support slots, but it offers rolling updates as an alternative. With &lt;code&gt;siteUpdateStrategy.type&lt;/code&gt; set to &lt;code&gt;RollingUpdate&lt;/code&gt;, Azure replaces instances in batches rather than all at once, giving in-progress executions a 60-minute grace period to complete.&lt;/p&gt;

&lt;p&gt;The trade-off: there's no separate staging URL for validation, no way to split traffic between versions, and rollback means redeploying the previous version rather than an instant swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment configuration in pipelines
&lt;/h2&gt;

&lt;p&gt;A deployment pipeline needs to put the right configuration in the right environment without leaking secrets into workflow files. GitHub Environments, the secrets hierarchy, and Key Vault references each handle a piece of this.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Environments
&lt;/h3&gt;

&lt;p&gt;Environments are configured under your repository's Settings &amp;gt; Environments. Each environment can have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required reviewers&lt;/strong&gt; (up to 6 people who must approve before the deploy job runs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait timers&lt;/strong&gt; (a delay before deployment proceeds, useful for change windows)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment branches&lt;/strong&gt; (restrict which branches can target this environment)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the workflow, &lt;code&gt;environment: production&lt;/code&gt; on a job ties it to that environment's rules. The job will pause and wait for approval if reviewers are configured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets hierarchy
&lt;/h3&gt;

&lt;p&gt;GitHub secrets exist at three levels:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnihqze1g59dtnn7jg4b4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnihqze1g59dtnn7jg4b4.png" alt="GitHub secrets hierarchy: Organization, Repository, and Environment levels with scope and examples" width="752" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the same secret name exists at multiple levels, &lt;strong&gt;environment wins over repository, which wins over organization&lt;/strong&gt;. This means you can set &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt; at the environment level with different values for &lt;code&gt;development&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;production&lt;/code&gt;, each pointing to a different service principal scoped to its own resource group.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting app configuration during deployment
&lt;/h3&gt;

&lt;p&gt;Your Function App needs configuration values beyond what's in the code. The most direct approach is the Azure CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure app settings&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/cli@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;az functionapp config appsettings set \&lt;/span&gt;
        &lt;span class="s"&gt;--name ${{ vars.FUNCTION_APP_NAME }} \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group ${{ vars.RESOURCE_GROUP }} \&lt;/span&gt;
        &lt;span class="s"&gt;--settings \&lt;/span&gt;
          &lt;span class="s"&gt;"ServiceBus__Connection=${{ secrets.SERVICEBUS_CONNECTION }}" \&lt;/span&gt;
          &lt;span class="s"&gt;"FeatureFlags__NewCheckout=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;vars&lt;/code&gt; (GitHub Variables) for non-sensitive configuration and &lt;code&gt;secrets&lt;/code&gt; for anything you wouldn't put in a log file.&lt;/p&gt;

&lt;p&gt;One warning if you manage settings through Bicep or ARM templates instead: the ARM API &lt;strong&gt;replaces&lt;/strong&gt; all app settings on each deployment. If your template omits a setting that exists on the app, that setting gets deleted. The CLI's &lt;code&gt;appsettings set&lt;/code&gt; command merges instead, which is safer for incremental updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-environment workflow
&lt;/h3&gt;

&lt;p&gt;The build-once-deploy-many pattern chains environments with approval gates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="c1"&gt;# ... build steps from earlier ...&lt;/span&gt;

  &lt;span class="na"&gt;deploy-dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# download artifact, azure/login, functions-action&lt;/span&gt;
      &lt;span class="c1"&gt;# (same structure, different secrets per environment)&lt;/span&gt;

  &lt;span class="na"&gt;deploy-staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-dev&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# deploy to staging slot, run smoke tests&lt;/span&gt;

  &lt;span class="na"&gt;deploy-production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-staging&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;  &lt;span class="c1"&gt;# approval gate triggers here&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# swap staging to production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same artifact flows through all three environments. The only things that change are the secrets (different &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt; per environment, each scoped to its own resource group) and the deployment target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Vault integration
&lt;/h3&gt;

&lt;p&gt;This ties back to &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Part 6 (Configuration Done Right)&lt;/a&gt;. Instead of passing secret values through your pipeline, store them in Key Vault and reference them in app settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;ServiceBus__Connection&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;@Microsoft.KeyVault(VaultName=my-kv;SecretName=servicebus-conn)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your pipeline sets the &lt;strong&gt;reference&lt;/strong&gt;, not the secret value. The Function App's managed identity resolves the actual value at runtime using the &lt;code&gt;Key Vault Secrets User&lt;/code&gt; role. The pipeline never sees the secret, and rotating it in Key Vault takes effect without redeployment.&lt;/p&gt;

&lt;p&gt;If you use deployment slots, mark Key Vault references as slot settings when different environments need different secrets (e.g., staging points to a staging Key Vault, production to a production Key Vault).&lt;/p&gt;

&lt;h3&gt;
  
  
  What goes where
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbxbgmxgtnorru9lrhhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbxbgmxgtnorru9lrhhr.png" alt="What goes where: GitHub Secrets for IDs, Key Vault for connection strings and keys, avoid secrets in YAML or Bicep" width="766" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Eight articles, one function app, and a pipeline that deploys itself. If something in your own setup doesn't match what's here, the series navigation links every piece: from the first HTTP trigger through testing to this deployment workflow.&lt;/p&gt;

&lt;p&gt;Do you deploy straight to production or use a staging slot? What made you choose one over the other?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 8: Deploying to Azure: CI/CD with GitHub Actions (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>dotnet</category>
      <category>github</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Figuring out what actually needs a real Azure connection vs. what you can just test with a plain class… that’s where most testing headaches start. I wrote up how I handle it: unit tests, Testcontainers + Azurite, and full func start pipelines.</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Sat, 21 Mar 2026 06:16:23 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/figuring-out-what-actually-needs-a-real-azure-connection-vs-what-you-can-just-test-with-a-plain-548</link>
      <guid>https://dev.to/martin_oehlert/figuring-out-what-actually-needs-a-real-azure-connection-vs-what-you-can-just-test-with-a-plain-548</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" class="crayons-story__hidden-navigation-link"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/martin_oehlert" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1661015%2Fd0bdf508-0244-49d8-8655-aea054d71b86.png" alt="martin_oehlert profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/martin_oehlert" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Martin Oehlert
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Martin Oehlert
                
              
              &lt;div id="story-author-preview-content-3375218" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/martin_oehlert" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1661015%2Fd0bdf508-0244-49d8-8655-aea054d71b86.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Martin Oehlert&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 20&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" id="article-link-3375218"&gt;
          Testing Azure Functions: Unit, Integration, and Local
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/azure"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;azure&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/azurefunctions"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;azurefunctions&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/serverless"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;serverless&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            15 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Testing Azure Functions: Unit, Integration, and Local</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 20 Mar 2026 07:17:37 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml</link>
      <guid>https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 7: Testing Azure Functions: Unit, Integration, and Local (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;Where do you draw the line between what needs a full Azure connection and what can be tested with a plain class instantiation? The isolated worker model makes the answer concrete: the function class is just wiring. Everything testable lives in a service class that knows nothing about Azure.&lt;/p&gt;

&lt;p&gt;Most testing pain comes from not drawing that line early enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision that makes testing possible
&lt;/h2&gt;

&lt;p&gt;Consider a function that does its own work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SqlConnection&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CreateOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ORD-"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;)[..&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"INSERT INTO Orders (OrderId, ProductId, Quantity) VALUES (@OrderId, @ProductId, @Quantity)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Created order {OrderId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreatedResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To unit test this, you need a real &lt;code&gt;SqlConnection&lt;/code&gt;. That means a real database, which means either a running SQL Server, Testcontainers, or a brittle in-memory substitute. Every test becomes an infrastructure test, even for something as simple as verifying that a zero quantity returns 400.&lt;/p&gt;

&lt;p&gt;The fix is to move the logic into a service class, leaving the function with nothing to do except call the service and map the result to a response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CreateOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreatedResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function class is now three lines of routing logic. &lt;code&gt;IOrderService&lt;/code&gt; is a plain interface with no Azure types, no infrastructure, nothing that requires a running host to instantiate.&lt;/p&gt;

&lt;p&gt;This gives you two separate test targets. The service holds the logic and gets fast, isolated unit tests with no framework setup. The function class holds the routing and gets a thin layer of tests that verify the HTTP response shapes. Each layer can be tested on its own terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit testing the service layer
&lt;/h2&gt;

&lt;p&gt;The service has one dependency worth injecting for tests: &lt;code&gt;IOrderRepository&lt;/code&gt;. Here's the full service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IOrderService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ORD-"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;)[..&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Created order {OrderId} for {ProductId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test it, you need xUnit and NSubstitute. The &lt;code&gt;.csproj&lt;/code&gt; is minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;IsTestProject&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/IsTestProject&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Test method names use underscores by convention (MethodName_Condition_Expected) --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;NoWarn&amp;gt;&lt;/span&gt;$(NoWarn);CA1707&lt;span class="nt"&gt;&amp;lt;/NoWarn&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FrameworkReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.App"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Test.Sdk"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"NSubstitute"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"xunit"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"xunit.runner.visualstudio"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;PrivateAssets&amp;gt;&lt;/span&gt;all&lt;span class="nt"&gt;&amp;lt;/PrivateAssets&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"../HttpTriggerDemo/HttpTriggerDemo.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests themselves need no Azure infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderServiceTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Substitute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderServiceTests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NullLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithValidRequest_ReturnsSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithValidRequest_SavesOrderToRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithInvalidQuantity_ReturnsFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;False&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithInvalidQuantity_DoesNotCallRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DidNotReceive&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NullLogger&amp;lt;T&amp;gt;.Instance&lt;/code&gt; is the right choice for service tests. You are testing behavior, not logging output. Using &lt;code&gt;Substitute.For&amp;lt;ILogger&amp;lt;T&amp;gt;&amp;gt;()&lt;/code&gt; to verify that specific log messages were emitted is a fragile approach: log messages are implementation details that change often and aren't part of the service contract. Save NSubstitute for dependencies whose behavior actually matters to the test outcome, like &lt;code&gt;IOrderRepository&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[Theory]&lt;/code&gt; + &lt;code&gt;[InlineData]&lt;/code&gt; handles the validation branches without duplicating test body. Each &lt;code&gt;InlineData&lt;/code&gt; value runs as a separate test in the output, so you get clear signal on exactly which inputs fail. The two &lt;code&gt;[Theory]&lt;/code&gt; blocks above run 3 + 2 = 5 test cases from a handful of lines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Received()&lt;/code&gt; and &lt;code&gt;DidNotReceive()&lt;/code&gt; are NSubstitute's call-count assertions. The second &lt;code&gt;[Fact]&lt;/code&gt; verifies the repository was called with the right data; the second &lt;code&gt;[Theory]&lt;/code&gt; verifies it was never called when validation fails. Together they cover both the happy path and the guard clause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit testing the function class
&lt;/h2&gt;

&lt;p&gt;When you use &lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; (the ASP.NET Core integration mode), the function's &lt;code&gt;HttpRequest&lt;/code&gt; is a standard ASP.NET Core &lt;code&gt;HttpRequest&lt;/code&gt;. That means you can construct a &lt;code&gt;DefaultHttpContext&lt;/code&gt; in tests and pass &lt;code&gt;context.Request&lt;/code&gt; directly to the function method, with no Functions runtime involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunctionTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IOrderService&lt;/span&gt; &lt;span class="n"&gt;_orderService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Substitute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;OrderFunction&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderFunctionTests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder_WhenServiceSucceeds_Returns201Created&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ORD-ABCD1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultHttpContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreatedResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders/ORD-ABCD1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder_WhenServiceFails_Returns400BadRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultHttpContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what these tests do not cover: the &lt;code&gt;[HttpTrigger]&lt;/code&gt; attribute, binding resolution, middleware, or anything the Functions host owns. That's intentional. The function's responsibility is to map a service result to an HTTP response. Two tests cover both outcome branches. Anything beyond that is integration territory.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;[HttpTrigger]&lt;/code&gt; and &lt;code&gt;[FromBody]&lt;/code&gt; attributes are metadata for the runtime. They don't execute during a direct method call, so there's nothing to test or mock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timer triggers
&lt;/h3&gt;

&lt;p&gt;Timer functions follow the same pattern. &lt;code&gt;TimerInfo&lt;/code&gt; is a concrete class from the Functions SDK with settable properties, so you construct it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CleanupFunctionTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CleanupFunction&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NullLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CleanupFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run_WhenOnSchedule_CompletesWithoutError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TimerInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsPastDue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// No exception thrown = the function handled the timer correctly.&lt;/span&gt;
        &lt;span class="c1"&gt;// Timer functions have no return value — the observable outcome is&lt;/span&gt;
        &lt;span class="c1"&gt;// either successful completion or an exception.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run_WhenPastDue_StillCompletes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TimerInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsPastDue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timer function tests are often this minimal. The function's behavior on &lt;code&gt;IsPastDue = true&lt;/code&gt; is to log a warning; there's no meaningful return value to assert on. What you're verifying is that the function reaches completion without throwing, and that the &lt;code&gt;IsPastDue&lt;/code&gt; branch doesn't break anything. If your cleanup function does real work (deleting records, archiving blobs), that work lives in a service and gets tested through the service tests, not through the function.&lt;/p&gt;




&lt;h2&gt;
  
  
  Integration testing with Testcontainers
&lt;/h2&gt;

&lt;p&gt;Unit tests get you 80% of the way. They don't verify that DI registrations are correct, that your database schema matches your queries, or that a real &lt;code&gt;TableClient&lt;/code&gt; call actually persists what you think it does. That's two separate problems: the composition root, and the data layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the composition root
&lt;/h3&gt;

&lt;p&gt;The first failure mode is silent: a service registration is missing, and &lt;code&gt;OrderFunction&lt;/code&gt;'s constructor throws at runtime while every unit test passes. A composition test catches this without Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HostIntegrationTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IHost&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsWebApplication&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// WorkerHostedService opens a gRPC channel to the Functions host.&lt;/span&gt;
                &lt;span class="c1"&gt;// That host doesn't exist in tests — remove it or the build hangs.&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImplementationType&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"WorkerHostedService"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;IOrderService_ResolvesFromDi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WebApplicationFactory&amp;lt;Program&amp;gt;&lt;/code&gt; fails with Azure Functions isolated worker. The model uses gRPC for host-worker communication, and the factory hits a channel URI parsing error when no Functions host is running. The &lt;code&gt;HostBuilder&lt;/code&gt; approach mirrors &lt;code&gt;Program.cs&lt;/code&gt; exactly, with the gRPC listener stripped. This test doesn't call any function logic; it only verifies the container compiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the data layer
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;InMemoryOrderRepository&lt;/code&gt; lets unit tests run fast, but it tells you nothing about whether your actual persistence works. A production implementation using Azure Table Storage looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TableStorageOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TableClient&lt;/span&gt; &lt;span class="n"&gt;tableClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TableEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Quantity"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tableClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEntityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integration test spins up Azurite in Docker via Testcontainers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TableStorageOrderRepositoryTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AzuriteContainer&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzuriteBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SaveAsync_WithValidOrder_PersistsToTableStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TableClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TableStorageOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ORD-TEST01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetEntityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TableEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Quantity"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add one package to the test project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.Azurite"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test run gets a fresh container. No ports to reserve, no cleanup between runs; Testcontainers handles port allocation for parallel CI execution automatically.&lt;/p&gt;

&lt;p&gt;The same pattern covers blob and queue operations. For relational databases, &lt;code&gt;Testcontainers.MsSql&lt;/code&gt; and &lt;code&gt;Testcontainers.PostgreSql&lt;/code&gt; provide the same lifecycle wrapper for SQL Server and Postgres.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local E2E testing with &lt;code&gt;func start&lt;/code&gt; + Azurite
&lt;/h2&gt;

&lt;p&gt;Logic tests cover &lt;code&gt;OrderService&lt;/code&gt;. Repository tests cover &lt;code&gt;TableStorageOrderRepository&lt;/code&gt;. Neither covers what happens when the Functions host receives an HTTP request, routes it through middleware, deserializes the body, calls the function, and returns a response.&lt;/p&gt;

&lt;p&gt;For that, the host must be running. The approach is to start Azurite and &lt;code&gt;func start&lt;/code&gt; together in the test fixture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FunctionsE2ETests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AzuriteContainer&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzuriteBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_funcProcess&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;_funcProcess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProcessStartInfo&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;FileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"start --port 7071"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;WorkingDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFullPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"../../../HttpTriggerDemo"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;EnvironmentVariables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"dotnet-isolated"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;RedirectStandardOutput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;UseShellExecute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;WaitForHostReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_funcProcess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder_WithValidRequest_Returns201&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"http://localhost:7071/api/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;WaitForHostReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TaskCompletionSource&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputDataReceived&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Host started"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrySetResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginOutputReadLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_funcProcess&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entireProcessTree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;func&lt;/code&gt; must be on the PATH. CI pipelines need &lt;code&gt;npm install -g azure-functions-core-tools@4&lt;/code&gt; before these tests run: it's a test infrastructure dependency that bites if you assume it's there.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Kill(entireProcessTree: true)&lt;/code&gt; matters on Windows. &lt;code&gt;func start&lt;/code&gt; spawns child processes; killing just the parent leaves orphaned processes holding port 7071, which causes every subsequent E2E test run in that session to hang on startup.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WaitForHostReady&lt;/code&gt; polls stdout for "Host started". Startup takes 3-10 seconds depending on cold JIT and machine speed. Set the timeout conservatively: a flaky timeout is harder to debug than a slow test.&lt;/p&gt;

&lt;p&gt;Put these tests in a separate project with &lt;code&gt;[Trait("Category", "E2E")]&lt;/code&gt; and exclude them from the fast inner development loop. They're most useful in CI as a gate before deployment, not as daily feedback during development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing an event-driven function
&lt;/h2&gt;

&lt;p&gt;HTTP triggers test cleanly: call the function directly, inspect the return value. Event Hub triggers are different. The function receives a batch of &lt;code&gt;EventData&lt;/code&gt;, deserializes each message, and delegates to a service; the trigger binding itself is provided by the runtime. That runtime can run locally.&lt;/p&gt;

&lt;p&gt;The scenario here is an IoT pipeline: devices publish sensor readings to an Event Hub, and a function consumes the batch, validates each reading, and writes to Cosmos DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  The function
&lt;/h3&gt;

&lt;p&gt;The function stays thin. Deserialize the batch, call the service, nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ISensorProcessor&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;EventHubTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sensor-readings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"EventHubConnection"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing batch of {Count} events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;eventData&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;EventData[]&lt;/code&gt; parameter receives the batch. The function doesn't know or care how many partitions the hub has, how messages were routed, or what retry policy applies: that's the runtime's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit testing the function
&lt;/h3&gt;

&lt;p&gt;Construct &lt;code&gt;EventData&lt;/code&gt; directly with a JSON body and call &lt;code&gt;Run()&lt;/code&gt;. No containers, no emulator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run_WithBatchOfThreeEvents_CallsProcessorThreeTimes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Substitute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ISensorProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NullLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;22.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;25.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;55.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;18.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;72.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;EventData&lt;/span&gt; &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches deserialization bugs and verifies the batch loop without starting anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full trigger integration test
&lt;/h3&gt;

&lt;p&gt;Unit tests verify the dispatch and deserialization logic in isolation. The full pipeline test goes further: a real message flows from Event Hubs through the function and into Cosmos DB.&lt;/p&gt;

&lt;p&gt;The fixture starts three containers in parallel (Azurite for the Functions runtime, the Event Hubs emulator, and the Cosmos DB emulator), then launches &lt;code&gt;func start&lt;/code&gt; as a child process wired to all three. The full source is in &lt;a href="https://github.com/martinoehlert/azure-functions-samples/blob/main/EventHubDemo.Tests/SensorPipelineFixture.cs" rel="noopener noreferrer"&gt;SensorPipelineFixture.cs&lt;/a&gt;. The container declarations and process wiring both require non-obvious configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use latest: Core Tools 4.8 sends an API version that Azurite 3.28.0 (the Testcontainers default) rejects.&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AzuriteContainer&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzuriteBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mcr.microsoft.com/azure-storage/azurite:latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// WithPortBinding pins the host port so localhost:8081 resolves from the func child process.&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CosmosDbContainer&lt;/span&gt; &lt;span class="n"&gt;_cosmos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CosmosDbBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithPortBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithWaitStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForUnixContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UntilMessageIsLogged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Gateway=OK, Explorer=OK"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;EventHubsContainer&lt;/span&gt; &lt;span class="n"&gt;_eventHubs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SensorPipelineFixture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_eventHubs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EventHubsBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAcceptLicenseAgreement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithConfigurationBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EventHubsServiceConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sensor-readings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithWaitStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForUnixContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UntilMessageIsLogged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Emulator Service is Successfully Up!"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testcontainers pins &lt;code&gt;azurite:3.28.0&lt;/code&gt; as its default. Azure Functions Core Tools 4.8 sends API version &lt;code&gt;2024-08-04&lt;/code&gt;; Azurite 3.28.0 rejects that version with a 400. Pinning to &lt;code&gt;latest&lt;/code&gt; resolves it.&lt;/p&gt;

&lt;p&gt;Both the Event Hubs emulator and the Cosmos &lt;code&gt;vnext-preview&lt;/code&gt; image are distroless: no shell, no &lt;code&gt;/bin/sh&lt;/code&gt;. The default Testcontainers port-check wait strategy execs &lt;code&gt;/bin/sh&lt;/code&gt; inside the container to verify the port is listening. On a distroless image, that exec fails and the strategy hangs indefinitely. &lt;code&gt;UntilMessageIsLogged()&lt;/code&gt; watches the container's stdout stream directly, bypassing the shell dependency.&lt;/p&gt;

&lt;p&gt;The Cosmos emulator returns its own internal address in the account metadata it sends back to clients. The test-process &lt;code&gt;CosmosClient&lt;/code&gt; receives &lt;code&gt;localhost:8081&lt;/code&gt; as the endpoint and follows it there. &lt;code&gt;WithPortBinding(8081, 8081)&lt;/code&gt; ensures that host port is pinned, so the &lt;code&gt;func&lt;/code&gt; child process (which constructs its own &lt;code&gt;CosmosClient&lt;/code&gt;) lands on the same address.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithResourceMapping&lt;/code&gt; mounts a JSON configuration file into the Event Hubs emulator container, but it doesn't set the &lt;code&gt;ServiceConfiguration&lt;/code&gt; property the builder reads at &lt;code&gt;Build()&lt;/code&gt; time. The build throws at runtime. &lt;code&gt;WithConfigurationBuilder&lt;/code&gt; uses the fluent API to set &lt;code&gt;ServiceConfiguration&lt;/code&gt; directly, and the configuration is validated at build time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process wiring:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cosmosPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cosmos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMappedPublicPort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cosmosKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cosmos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AccountKey="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AccountKey="&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;CosmosClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CosmosClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_cosmos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CosmosClientOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ConnectionMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConnectionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;HttpClientFactory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CosmosEmulatorHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cosmosPort&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;SerializerOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CosmosSerializationOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PropertyNamingPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CosmosPropertyNamingPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CamelCase&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;startInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CosmosDbConnection"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;$"AccountEndpoint=http://localhost:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cosmosPort&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/;AccountKey=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cosmosKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;func&lt;/code&gt; child process constructs its own &lt;code&gt;CosmosClient&lt;/code&gt; from the &lt;code&gt;CosmosDbConnection&lt;/code&gt; environment variable; it can't share the test process's &lt;code&gt;HttpClient&lt;/code&gt; handler across the process boundary. Passing &lt;code&gt;AccountEndpoint=http://localhost:{port}/&lt;/code&gt; with an explicitly extracted key gives the child process a direct HTTP connection to the emulator without needing the handler.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CosmosEmulatorHandler&lt;/code&gt; is an &lt;code&gt;HttpMessageHandler&lt;/code&gt; that rewrites outgoing requests from the emulator's self-reported internal hostname to &lt;code&gt;localhost:{cosmosPort}&lt;/code&gt;. Without it, the SDK follows the internal address the emulator returns in its account metadata and misses the container.&lt;/p&gt;

&lt;p&gt;The full fixture also implements &lt;code&gt;WaitForFunctionsHostAsync&lt;/code&gt; (polls &lt;code&gt;localhost:7071/admin/host/status&lt;/code&gt; until the host responds) and &lt;code&gt;DisposeAsync&lt;/code&gt; (kills the process tree and disposes all three containers). Both are in the &lt;a href="https://github.com/martinoehlert/azure-functions-samples/blob/main/EventHubDemo.Tests/SensorPipelineFixture.cs" rel="noopener noreferrer"&gt;full source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The test publishes a batch and polls Cosmos DB until the document appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorPipelineFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SensorPipelineTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorPipelineFixture&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PublishedEvent_WithValidReading_AppearsInCosmosDb&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;DeviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"device-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;23.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Humidity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;58.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProducerClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBatchAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProducerClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CosmosClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SensorData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"readings"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetItemQueryIterator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
                &lt;span class="s"&gt;$"SELECT * FROM c WHERE c.deviceId = '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeviceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasMoreResults&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadNextAsync&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;23.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixture takes 60–90 seconds to start. Run it separately from unit tests in CI using xUnit's &lt;code&gt;[Collection]&lt;/code&gt; trait or a test filter.&lt;/p&gt;

&lt;p&gt;Add the packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.EventHubs"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.CosmosDb"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.Azurite"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Azure.Messaging.EventHubs"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Newtonsoft.Json"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cosmos SDK v3 requires Newtonsoft.Json at runtime via an internal dependency. Omitting it produces a &lt;code&gt;FileNotFoundException&lt;/code&gt; at startup with no message connecting it to Cosmos.&lt;/p&gt;




&lt;h2&gt;
  
  
  What can't be emulated locally
&lt;/h2&gt;

&lt;p&gt;Azurite and &lt;code&gt;func start&lt;/code&gt; cover wiring and trigger dispatch. Some behaviors only emerge in Azure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold starts.&lt;/strong&gt; Local tests keep the host warm throughout the run. Consumption plan cold starts in Azure hit 500ms-2s for .NET depending on deployment size. If your SLA depends on p99 latency, that gap only shows in production traffic — local tests give you no signal on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed identity credential resolution.&lt;/strong&gt; &lt;code&gt;DefaultAzureCredential&lt;/code&gt; falls through a chain of credential sources. Locally it uses developer machine credentials or environment variables. In Azure it uses the Managed Identity endpoint. A misconfigured client ID or missing role assignment won't surface until the function runs with a real identity attached. The local credential chain doesn't exercise the same code path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale-out behavior.&lt;/strong&gt; &lt;code&gt;func start&lt;/code&gt; runs one worker. Azure scales to N workers based on trigger backlog. Race conditions, partition contention, and shared-state bugs appear only under concurrent load across multiple instances. No local setup replicates this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KEDA-based scaling decisions.&lt;/strong&gt; Event Hub and Service Bus triggers scale based on message lag, but the scaling decisions come from the infrastructure, not the worker process. There's no local equivalent for how Azure routes partitions across workers as instances scale up.&lt;/p&gt;

&lt;p&gt;The useful takeaway: unit tests and integration tests give fast, reliable feedback on logic and wiring. They don't give confidence about latency under cold conditions, behavior at scale, or cloud-managed auth. Build those signals from production observability (Application Insights, structured logs, alert rules), not from test infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Patterns that cause pain
&lt;/h2&gt;

&lt;p&gt;A few mistakes appear repeatedly in Azure Functions test suites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asserting on log messages.&lt;/strong&gt; &lt;code&gt;Substitute.For&amp;lt;ILogger&amp;lt;T&amp;gt;&amp;gt;()&lt;/code&gt; lets you verify that specific log calls were made. Don't. Log messages are implementation details: they change wording, get split into multiple calls, or get removed during refactoring. When they do, your test breaks without any behavior change. Use &lt;code&gt;NullLogger&amp;lt;T&amp;gt;.Instance&lt;/code&gt; for services and only substitute loggers when logging output is the actual behavior under test (which is almost never).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reaching into the runtime from unit tests.&lt;/strong&gt; &lt;code&gt;[HttpTrigger]&lt;/code&gt;, &lt;code&gt;[FromBody]&lt;/code&gt;, and &lt;code&gt;[QueueTrigger]&lt;/code&gt; are metadata for the runtime to read. They don't execute during a direct method call. Trying to test that binding attributes are present, or that the runtime would route correctly, puts you in the business of testing the Functions SDK rather than your code. The routing table lives in the host config; your job is to test what happens once the host calls your method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using constructors for container lifecycle.&lt;/strong&gt; xUnit creates test class instances before running tests, but &lt;code&gt;StartAsync()&lt;/code&gt; is async. Initializing a &lt;code&gt;AzuriteContainer&lt;/code&gt; in a constructor blocks the thread and causes tests to hang silently. Always use &lt;code&gt;IAsyncLifetime&lt;/code&gt;: &lt;code&gt;InitializeAsync&lt;/code&gt; for startup, &lt;code&gt;DisposeAsync&lt;/code&gt; for teardown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing the service layer twice.&lt;/strong&gt; Once you have thorough &lt;code&gt;OrderServiceTests&lt;/code&gt;, the function-level tests (&lt;code&gt;OrderFunctionTests&lt;/code&gt;) should only cover the HTTP response mapping: does a successful result return 201, does a failure return 400. Repeating the validation and business logic assertions at the function level creates duplicate coverage that breaks together whenever the service contract changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing your testing strategy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2d2hp69aadyyzdcj8au.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2d2hp69aadyyzdcj8au.png" alt="Azure Functions Testing Pyramid" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Infrastructure needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service logic&lt;/td&gt;
&lt;td&gt;Unit test&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function routing&lt;/td&gt;
&lt;td&gt;Unit test&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI wiring + middleware&lt;/td&gt;
&lt;td&gt;HostBuilder trick&lt;/td&gt;
&lt;td&gt;None (gRPC stripped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data layer round-trips&lt;/td&gt;
&lt;td&gt;Testcontainers (SQL/Postgres/Azurite)&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger dispatch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;func start&lt;/code&gt; + Azurite&lt;/td&gt;
&lt;td&gt;Core Tools + Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full pipeline&lt;/td&gt;
&lt;td&gt;Testcontainers Docker image&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start from the top and stop as soon as the tests cover the risk you're managing. For most business logic, unit tests against the service layer are enough. The function class tests add a few minutes of coverage for the HTTP response shapes. Integration and E2E tests are worth the infrastructure cost only when you need to verify wiring, real database behavior, or trigger dispatch.&lt;/p&gt;




&lt;p&gt;Do you unit test your function class directly, or do you treat the service layer as the boundary and skip function-level tests entirely?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 7: Testing Azure Functions: Unit, Integration, and Local (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Configuration Done Right: Settings, Secrets, and Key Vault</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 13 Mar 2026 05:43:09 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h</link>
      <guid>https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 6: Configuration Done Right: Settings, Secrets, and Key Vault (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;You add a Service Bus connection string to &lt;code&gt;appsettings.json&lt;/code&gt;. You deploy. The trigger fails at startup with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.Azure.WebJobs.Host.Listeners.FunctionListenerException:
The listener for function 'ProcessMessage' was unable to start.
Microsoft.Azure.WebJobs.ServiceBus.Listeners.ServiceBusListener:
Connection string not found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your code reads it fine. But the trigger cannot start because the &lt;em&gt;host&lt;/em&gt; never sees &lt;code&gt;appsettings.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Azure Functions has two distinct configuration surfaces that solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;host process&lt;/strong&gt; (&lt;code&gt;func.exe&lt;/code&gt;) resolves trigger and binding connections from environment variables.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;worker process&lt;/strong&gt; (your .NET code) reads &lt;code&gt;IConfiguration&lt;/code&gt;, which includes &lt;code&gt;appsettings.json&lt;/code&gt;, environment variables, and any other sources you add.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two processes share environment variables but nothing else. This article covers how to configure each correctly, how to move secrets to Key Vault without changing your code, and how to use the options pattern for strongly-typed settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5cmfv8pfwiwwbjpa91x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5cmfv8pfwiwwbjpa91x.png" alt="Azure Functions configuration flow diagram showing local.settings.json and Azure App Settings flowing through environment variables to the host and worker processes, with Key Vault references resolved at startup" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All code from this article is available in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository under &lt;code&gt;ConfigurationDemo/&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  local.settings.json
&lt;/h2&gt;

&lt;p&gt;In local development, &lt;code&gt;local.settings.json&lt;/code&gt; is how you supply environment variables to both processes simultaneously. Core Tools (&lt;code&gt;func.exe&lt;/code&gt;) reads this file at startup and injects every entry in the &lt;code&gt;Values&lt;/code&gt; section as a process environment variable before launching the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ServiceBusConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Endpoint=sb://..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__BaseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__TimeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in &lt;code&gt;Values&lt;/code&gt; is a flat string pair, no nesting. For hierarchical settings, use double underscore as the separator: &lt;code&gt;Api__BaseUrl&lt;/code&gt; becomes &lt;code&gt;Api:BaseUrl&lt;/code&gt; in &lt;code&gt;IConfiguration&lt;/code&gt;. All values are strings; the configuration binder converts them to the correct type when binding to options classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ConnectionStrings&lt;/code&gt; section is a trap.&lt;/strong&gt; It exists for ORMs like Entity Framework that expect connection strings under &lt;code&gt;ConnectionStrings:*&lt;/code&gt;. Core Tools loads these with a &lt;code&gt;ConnectionStrings:&lt;/code&gt; prefix — so &lt;code&gt;ConnectionStrings.MyDb&lt;/code&gt; becomes the environment variable &lt;code&gt;ConnectionStrings:MyDb&lt;/code&gt;. The Functions host looks for binding connections by their bare name. Put a Service Bus or Storage connection string in &lt;code&gt;ConnectionStrings&lt;/code&gt; and the trigger cannot find it, even though your application code can read it fine with &lt;code&gt;configuration.GetConnectionString("MyDb")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule: trigger and binding connections go in &lt;code&gt;Values&lt;/code&gt;, always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure ignores this file entirely.&lt;/strong&gt; It is consumed only by Core Tools locally. When you deploy, you configure settings separately in the portal, via CLI, or in Bicep. If you use &lt;code&gt;func azure functionapp publish --publish-local-settings&lt;/code&gt;, only the &lt;code&gt;Values&lt;/code&gt; section is copied. The &lt;code&gt;ConnectionStrings&lt;/code&gt; section is never published — another reason to avoid it.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;local.settings.json&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. The Functions project template does this automatically, but verify it before your first commit. This file will contain connection strings, API keys, and storage credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Azure App Settings
&lt;/h2&gt;

&lt;p&gt;In Azure, every Application Setting is an environment variable injected into the host process. The Functions runtime treats them identically to what &lt;code&gt;local.settings.json&lt;/code&gt; provides locally.&lt;/p&gt;

&lt;p&gt;The portal lists them under &lt;strong&gt;Settings &amp;gt; Environment Variables &amp;gt; App settings&lt;/strong&gt;. Values are encrypted at rest and masked in the UI. Any change to Application Settings causes the function app to restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the Application Settings blade, not the Connection strings blade.&lt;/strong&gt; The Connection strings section in the portal adds a type prefix to the environment variable name. A custom connection string named &lt;code&gt;ServiceBus&lt;/code&gt; becomes &lt;code&gt;CUSTOMCONNSTR_ServiceBus&lt;/code&gt; in the environment. The Service Bus trigger looking for &lt;code&gt;ServiceBus&lt;/code&gt; will not find it.&lt;/p&gt;

&lt;p&gt;The Connection strings portal section exists for ASP.NET compatibility. For Azure Functions, put everything in Application Settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hierarchical settings
&lt;/h3&gt;

&lt;p&gt;Flat environment variables do not support nesting. The convention on Azure (which runs on Linux) is to use double underscore as the hierarchy separator:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App Setting name&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;IConfiguration&lt;/code&gt; key&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Api__BaseUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Api:BaseUrl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Api__TimeoutSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Api:TimeoutSeconds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Colon (&lt;code&gt;:&lt;/code&gt;) works as a separator on Windows only. Double underscore works on both platforms. Always use &lt;code&gt;__&lt;/code&gt; in App Setting names to ensure your functions run correctly whether the app is on a Windows or Linux hosting plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slot settings
&lt;/h3&gt;

&lt;p&gt;Deployment slots each have their own App Settings. By default, settings swap along with the code when you swap slots. &lt;strong&gt;Slot settings&lt;/strong&gt; (sticky settings) stay with the slot and do not swap.&lt;/p&gt;

&lt;p&gt;The common use case: staging and production connect to different databases or service bus namespaces. Mark those connection strings as slot settings so a staging deployment cannot accidentally point production code at the staging database.&lt;/p&gt;

&lt;p&gt;To mark a setting as sticky, check &lt;strong&gt;Deployment slot setting&lt;/strong&gt; when editing it in the portal. In Bicep, set &lt;code&gt;"slotSetting": true&lt;/code&gt; on the app setting object.&lt;/p&gt;

&lt;p&gt;One gotcha: a sticky setting must exist in every slot involved in a swap. If a sticky setting exists in staging but not in production, and you swap, the setting disappears from staging after the swap. Create the setting (with the appropriate value for each environment) in every slot before you start using sticky settings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Vault references
&lt;/h2&gt;

&lt;p&gt;Key Vault references let you store secrets in Azure Key Vault and reference them from App Settings. The Functions host resolves the reference at startup. Your code reads the setting with the same &lt;code&gt;IConfiguration["MyKey"]&lt;/code&gt; call it has always used.&lt;/p&gt;

&lt;p&gt;The reference syntax in the App Setting value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/ApiKey/)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or by name, if the Key Vault is in the same subscription:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Microsoft.KeyVault(VaultName=myvault;SecretName=ApiKey)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both forms produce identical behavior at runtime. Omit the secret version from the URI to always get the latest version. The platform caches the resolved value and re-fetches it every 24 hours. Rotating a secret takes effect automatically within that window without a deployment or restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed references are silent.&lt;/strong&gt; If the reference cannot be resolved (wrong vault name, missing permissions, deleted secret), the App Setting receives the literal reference string as its value: &lt;code&gt;@Microsoft.KeyVault(...)&lt;/code&gt;. This propagates through to your code, which typically throws because it receives an unexpected format instead of the secret value. To diagnose: open the setting in the portal and look for an error status indicator in the edit dialog. In the Azure portal, Platform features &amp;gt; Diagnose and solve problems also has a Key Vault Application Settings Diagnostics detector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managed identity setup
&lt;/h3&gt;

&lt;p&gt;The function app needs permission to read secrets from the vault. The recommended approach is a system-assigned managed identity.&lt;/p&gt;

&lt;p&gt;Enable it in the portal under &lt;strong&gt;Settings &amp;gt; Identity &amp;gt; System assigned &amp;gt; Status: On&lt;/strong&gt;. Via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az webapp identity assign &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;app-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then assign the &lt;strong&gt;Key Vault Secrets User&lt;/strong&gt; role to the identity on the vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Key Vault Secrets User"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;principalId&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"/subscriptions/&amp;lt;sub&amp;gt;/resourceGroups/&amp;lt;rg&amp;gt;/providers/Microsoft.KeyVault/vaults/&amp;lt;vault-name&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--assignee&lt;/code&gt; value is the &lt;code&gt;principalId&lt;/code&gt; from the identity assignment output, not the client ID and not the app's resource ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Vault Secrets User, not Contributor.&lt;/strong&gt; The Contributor role manages the vault as an Azure resource (creating, deleting, modifying it). It does not grant access to read secret values. Key Vault Secrets User grants data-plane read access to secrets. These are separate role planes and are frequently confused.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a vault with RBAC enabled
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;vault-name&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;rg&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-rbac-authorization&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--enable-rbac-authorization true&lt;/code&gt; flag is important. Without it, the vault uses the legacy access policy model. Microsoft's current guidance is to use RBAC for all new vaults. The access policy model has a privilege escalation risk: any user with Contributor on the vault can modify access policies to grant themselves secret access. Under RBAC, only Owner and User Access Administrator can modify role assignments.&lt;/p&gt;

&lt;p&gt;Once the vault exists, add a secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault secret &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault-name&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;vault-name&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"ApiKey"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your-secret&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local development
&lt;/h3&gt;

&lt;p&gt;Key Vault references are a portal-level feature. They do not apply locally. For local development, put the actual secret values directly in &lt;code&gt;local.settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev-key-here"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app code does not change. The same &lt;code&gt;configuration["ApiKey"]&lt;/code&gt; call works locally (reading from the environment variable injected by Core Tools) and in Azure (reading from the Key Vault reference resolved by the platform).&lt;/p&gt;

&lt;p&gt;If your team needs local access to the actual Key Vault for testing, use &lt;code&gt;DefaultAzureCredential&lt;/code&gt; in your service registration and run &lt;code&gt;az login&lt;/code&gt; with an account that has Key Vault Secrets User on the vault. The credential chain tries Azure CLI authentication, so the logged-in developer account gets used automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strongly-typed configuration with the options pattern
&lt;/h2&gt;

&lt;p&gt;Reading configuration with &lt;code&gt;configuration["MyKey"]&lt;/code&gt; works but gives you a stringly-typed API with no validation and no structure. The options pattern solves this by binding a configuration section to a typed class.&lt;/p&gt;

&lt;p&gt;Define the options class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;BaseUrl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;TimeoutSeconds&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register and validate it in &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDataAnnotations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BindConfiguration("Api")&lt;/code&gt; binds from the &lt;code&gt;Api&lt;/code&gt; section of &lt;code&gt;IConfiguration&lt;/code&gt;. After GetSection strips the prefix, &lt;code&gt;ApiOptions.BaseUrl&lt;/code&gt; maps to the &lt;code&gt;Api:BaseUrl&lt;/code&gt; key, which in turn comes from the &lt;code&gt;Api__BaseUrl&lt;/code&gt; environment variable. The same property name works whether the setting originates from &lt;code&gt;appsettings.json&lt;/code&gt;, &lt;code&gt;local.settings.json&lt;/code&gt;, or an Azure App Setting.&lt;/p&gt;

&lt;p&gt;Inject the options into a function class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ApiOptions&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// _api.BaseUrl and _api.TimeoutSeconds are validated and available&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/orders"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ValidateOnStart
&lt;/h3&gt;

&lt;p&gt;Without &lt;code&gt;ValidateOnStart()&lt;/code&gt;, validation fires when &lt;code&gt;.Value&lt;/code&gt; is first accessed. A misconfigured but rarely-invoked function can pass through a cold start and fail only at runtime, when you least expect it. With &lt;code&gt;ValidateOnStart()&lt;/code&gt;, the host throws &lt;code&gt;OptionsValidationException&lt;/code&gt; during startup and refuses to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.Extensions.Options.OptionsValidationException:
  DataAnnotation validation failed for 'ApiOptions' members:
    'BaseUrl' with the error: 'The BaseUrl field is required.'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This converts a silent runtime failure into an explicit startup failure, which is much easier to diagnose.&lt;/p&gt;

&lt;h3&gt;
  
  
  IOptions vs IOptionsSnapshot vs IOptionsMonitor
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt; in Azure Functions. The value is cached per singleton lifetime, which is correct: App Settings do not change at runtime without a restart, and a restart rebuilds the singleton anyway.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; is Scoped. Injecting it into a singleton throws at runtime. Functions does not have the same clear scope-per-request lifecycle as ASP.NET Core, so &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; causes subtle failures.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt; is safe in singletons and works if you need to respond to live configuration changes (for example, from Azure App Configuration with a refresh sentinel). For standard App Settings, it is more complexity than the scenario requires.&lt;/p&gt;

&lt;h3&gt;
  
  
  local.settings.json for options
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Values&lt;/code&gt; section is a flat dictionary. To represent the &lt;code&gt;Api&lt;/code&gt; section locally, use the &lt;code&gt;__&lt;/code&gt; separator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__BaseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__TimeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Azure, create App Settings with the same names: &lt;code&gt;Api__BaseUrl&lt;/code&gt; and &lt;code&gt;Api__TimeoutSeconds&lt;/code&gt;. The binding is identical in both environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three-layer mental model
&lt;/h2&gt;

&lt;p&gt;When a configuration problem comes up, the question to ask is: which process needs this value?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger and binding connections&lt;/strong&gt; are resolved by the host process. They must be environment variables: either a &lt;code&gt;Values&lt;/code&gt; entry in &lt;code&gt;local.settings.json&lt;/code&gt;, an Azure App Setting, or a Key Vault reference on an App Setting. No other configuration source reaches the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application settings&lt;/strong&gt; (anything your code reads via &lt;code&gt;IConfiguration&lt;/code&gt;) come from the full configuration pipeline: environment variables, &lt;code&gt;appsettings.json&lt;/code&gt;, &lt;code&gt;appsettings.{Environment}.json&lt;/code&gt;, and any sources you add in &lt;code&gt;Program.cs&lt;/code&gt;. Because environment variables are part of this pipeline, all App Settings are also available in &lt;code&gt;IConfiguration&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt; live in Key Vault and are surfaced as App Settings via the reference syntax. Your code sees them as ordinary environment variables and reads them through &lt;code&gt;IConfiguration&lt;/code&gt; like any other setting.&lt;/p&gt;

&lt;p&gt;The full working &lt;code&gt;ConfigurationDemo&lt;/code&gt; project — &lt;code&gt;ApiOptions&lt;/code&gt;, registration in &lt;code&gt;Program.cs&lt;/code&gt;, and &lt;code&gt;ProcessOrderFunction&lt;/code&gt; — is in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository. Clone it, copy &lt;code&gt;local.settings.json.example&lt;/code&gt; to &lt;code&gt;local.settings.json&lt;/code&gt;, and run &lt;code&gt;func start&lt;/code&gt; to see &lt;code&gt;ValidateOnStart&lt;/code&gt; in action.&lt;/p&gt;




&lt;p&gt;What do you use to manage secrets in your Azure Functions projects: Key Vault references, Azure App Configuration, or something else? Drop it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  AzureFunctions #DotNet #Azure #Security #KeyVault
&lt;/h1&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 6: Configuration Done Right: Settings, Secrets, and Key Vault (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Understanding the Isolated Worker Model</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 06 Mar 2026 07:21:24 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4</link>
      <guid>https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 5: Understanding the Isolated Worker Model (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem the isolated model solves
&lt;/h2&gt;

&lt;p&gt;You add a NuGet reference to &lt;code&gt;Newtonsoft.Json&lt;/code&gt; 13.0. Your code compiles. Your unit tests pass. You deploy to Azure Functions, and at runtime, your function silently uses version 12.0.3.&lt;/p&gt;

&lt;p&gt;No error. No warning. Just the host's copy of the assembly winning the load, because your function code and the Azure Functions runtime shared a single process.&lt;/p&gt;

&lt;p&gt;This was the &lt;strong&gt;in-process model&lt;/strong&gt;, and for years it was the only way to build .NET Azure Functions. Your function assemblies loaded directly into the same CLR instance as the Functions host. The host pinned its own versions of core packages: &lt;code&gt;Newtonsoft.Json&lt;/code&gt;, &lt;code&gt;Microsoft.Extensions.DependencyInjection&lt;/code&gt;, ASP.NET Core libraries, and dozens of others. If your code depended on a different version, the host's version won at load time. You had no way to override it.&lt;/p&gt;

&lt;p&gt;The version conflict problem went beyond JSON serialization. The host determined which .NET runtime your code ran on. When .NET 7 shipped, you could not target it until the Functions team updated the host. When .NET 8 arrived, the same waiting game. Your application's target framework was not your decision; it was the host's.&lt;/p&gt;

&lt;p&gt;Startup control was equally limited. The in-process model offered &lt;strong&gt;&lt;code&gt;FunctionsStartup&lt;/code&gt;&lt;/strong&gt; as an extension point for dependency injection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;FunctionsStartup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyStartup&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStartup&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FunctionsStartup&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFunctionsHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave you a DI container, but nothing else. No middleware pipeline. No request/response interception. No control over serialization settings, logging providers, or configuration sources beyond what the host exposed. If you wanted to add authentication middleware, or swap the JSON serializer for &lt;code&gt;System.Text.Json&lt;/code&gt;, or wire up OpenTelemetry tracing, you were working against the grain.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FunctionsStartup&lt;/code&gt; was a workaround bolted onto a hosting model that was never designed for extensibility. The isolated worker model replaced it entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two processes, one function app
&lt;/h2&gt;

&lt;p&gt;The isolated model splits your function app into two separate OS processes. The &lt;strong&gt;Azure Functions host&lt;/strong&gt; (&lt;code&gt;func.exe&lt;/code&gt;) handles triggers, bindings, and routing. Your code runs in a separate &lt;strong&gt;worker process&lt;/strong&gt; (&lt;code&gt;dotnet.exe&lt;/code&gt;) with its own CLR, its own assembly loader, and its own dependency graph.&lt;/p&gt;

&lt;p&gt;A single environment variable controls this split. Setting &lt;strong&gt;&lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt;&lt;/strong&gt; to &lt;code&gt;dotnet-isolated&lt;/code&gt; tells the host to spawn a worker process instead of loading your assemblies directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Azure Functions Host (func.exe)
  ├── Trigger listeners (HTTP, Service Bus, Event Hub...)
  ├── Binding infrastructure
  ├── host.json configuration
  └── gRPC server
         ↕ gRPC / Protobuf over HTTP/2
Worker Process (dotnet.exe / your app)
  ├── Program.cs bootstrap
  ├── Your DI container
  ├── Your middleware pipeline
  └── Your function code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two processes communicate over &lt;strong&gt;gRPC&lt;/strong&gt; using Protocol Buffers serialized over HTTP/2. When a trigger fires (an HTTP request arrives, a Service Bus message lands), the host serializes the trigger data into a Protobuf message and sends it to the worker. The worker executes your function, serializes the result, and sends it back.&lt;/p&gt;

&lt;p&gt;These C4 diagrams show the two-process architecture in more detail:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydxm4u1elbmtgyc5xnbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydxm4u1elbmtgyc5xnbt.png" alt="C4 container diagram showing the isolated worker architecture" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxu1a8b5ojkck9rz0g0g7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxu1a8b5ojkck9rz0g0g7.png" alt="C4 dynamic diagram showing the request flow between host and worker" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;Program.cs&lt;/code&gt; sets up the gRPC client, DI container, and middleware pipeline in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsWebApplication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExceptionHandlingMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CorrelationIdMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; registers the gRPC client that talks back to the host, enables ASP.NET Core integration for HTTP triggers, and gives you the middleware pipeline shown above. If you do not need HTTP trigger support, &lt;code&gt;ConfigureFunctionsWorkerDefaults()&lt;/code&gt; does the same setup without the ASP.NET Core integration.&lt;/p&gt;

&lt;p&gt;Because each process loads its own assemblies independently, the version conflict problem disappears. Your worker targets .NET 10 and references &lt;code&gt;Newtonsoft.Json&lt;/code&gt; 13.0.3? That is what runs. The host still uses whatever versions it needs internally, and the two never collide.&lt;/p&gt;

&lt;p&gt;The trade-off is that every function invocation crosses a process boundary. The host serializes trigger data, sends it over gRPC, and the worker deserializes it. On the same machine, the latency cost is negligible for most workloads. Where you will notice it is &lt;strong&gt;cold starts&lt;/strong&gt;: the runtime now needs to spin up two processes instead of one. For high-throughput, latency-sensitive functions that fire thousands of times per second, measure the overhead in your specific scenario. For the vast majority of production workloads (processing orders, handling webhooks, running scheduled cleanup jobs), the isolation is worth far more than the milliseconds it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you gain
&lt;/h2&gt;

&lt;p&gt;The isolated worker model removes real constraints that made the in-process model frustrating in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  No more assembly conflicts
&lt;/h3&gt;

&lt;p&gt;Your worker runs in its own process with its own dependency graph. The host loads whatever versions it needs; your application loads whatever versions you reference. Two processes, two sets of assemblies, zero conflicts. The Newtonsoft.Json problem from the opening of this article cannot happen in the isolated model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full startup control via Program.cs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsWebApplication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces the &lt;code&gt;[assembly: FunctionsStartup(typeof(MyStartup))]&lt;/code&gt; attribute and the &lt;code&gt;Startup&lt;/code&gt; class you had to wire up in the in-process model. The whole application now bootstraps through the &lt;strong&gt;.NET Generic Host&lt;/strong&gt;, the same pattern you already know from ASP.NET Core and worker services.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FunctionsApplication.CreateBuilder(args)&lt;/code&gt; sets up the host builder with Functions-specific defaults. &lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; enables ASP.NET Core integration so your HTTP triggers can work with &lt;code&gt;HttpRequest&lt;/code&gt; and &lt;code&gt;HttpResponse&lt;/code&gt; directly instead of the SDK's custom types.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Services&lt;/code&gt; block is standard dependency injection. &lt;code&gt;AddSingleton&amp;lt;IOrderService, OrderService&amp;gt;()&lt;/code&gt; registers your own service. &lt;code&gt;AddApplicationInsightsTelemetryWorkerService()&lt;/code&gt; and &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; wire up telemetry for the worker process (both are needed: the first adds the base SDK, the second configures Functions-specific log filtering).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;builder.Build().Run()&lt;/code&gt; starts the worker and connects it to the host over gRPC. If you have written a .NET 8 web API, this code should look familiar, because it is the same hosting model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Middleware pipeline
&lt;/h3&gt;

&lt;p&gt;The in-process model had no middleware. If you needed cross-cutting behavior (logging correlation IDs, catching unhandled exceptions, validating tokens) you were stuck wiring it through WebJobs SDK extension points or scattering try/catch blocks across every function.&lt;/p&gt;

&lt;p&gt;The isolated model gives you an ASP.NET Core-style pipeline around every function invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CorrelationIdMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExceptionHandlingMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each middleware runs in order before the function executes, then unwinds in reverse order after. You build one by implementing &lt;strong&gt;&lt;code&gt;IFunctionsWorkerMiddleware&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CorrelationIdMiddleware&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IFunctionsWorkerMiddleware&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FunctionContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FunctionExecutionDelegate&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Runs before the function&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;correlationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Runs after the function&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FunctionContext&lt;/code&gt; gives you access to the invocation metadata, bindings, and the &lt;code&gt;IServiceProvider&lt;/code&gt;. The &lt;code&gt;next&lt;/code&gt; delegate calls either the next middleware in the chain or the function itself. Everything before &lt;code&gt;await next(context)&lt;/code&gt; runs on the way in; everything after runs on the way out.&lt;/p&gt;

&lt;p&gt;In production, you would typically read an incoming correlation ID from a header or message property, fall back to generating one if it is missing, then stash it in &lt;code&gt;context.Items&lt;/code&gt; so the function and downstream services can pick it up. Exception-handling middleware wraps the &lt;code&gt;next&lt;/code&gt; call in a try/catch, logs the failure with structured context, and returns a consistent error response instead of letting the host surface a generic 500.&lt;/p&gt;

&lt;h3&gt;
  
  
  .NET version flexibility
&lt;/h3&gt;

&lt;p&gt;The worker process runs whatever .NET version you target, independently of the host. The host stays on its own runtime; your code stays on yours.&lt;/p&gt;

&lt;p&gt;Today, the isolated model supports .NET 8, .NET 9, .NET 10, and even .NET Framework 4.8 (for teams that cannot migrate legacy libraries). The in-process model was capped at .NET 8 with &lt;code&gt;v4&lt;/code&gt; of the Functions runtime and will never support .NET 9 or later. When .NET 12 ships, you will update your &lt;code&gt;TargetFramework&lt;/code&gt;, redeploy, and move on. No waiting for the Azure Functions team to update the host.&lt;/p&gt;

&lt;p&gt;Your release cycle and the host's release cycle are decoupled. You upgrade on your schedule.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changes from in-process
&lt;/h2&gt;

&lt;p&gt;Most of the migration is mechanical. The conceptual model shifts, but the actual code changes follow a predictable pattern. Once you have seen each one, you can work through a real codebase systematically.&lt;/p&gt;

&lt;h3&gt;
  
  
  The function attribute
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated (with ASP.NET Core integration)&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[FunctionName]&lt;/code&gt; comes from the WebJobs SDK (&lt;code&gt;Microsoft.Azure.WebJobs&lt;/code&gt;). &lt;code&gt;[Function]&lt;/code&gt; comes from the Functions Worker SDK (&lt;code&gt;Microsoft.Azure.Functions.Worker&lt;/code&gt;). The attribute names differ by one word, which makes a global search-and-replace dangerous: you need to update the package reference and the attribute name together, or you will reference an attribute that does not exist in your new dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP types
&lt;/h3&gt;

&lt;p&gt;The isolated model gives you two ways to handle HTTP triggers, and the difference matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Types used&lt;/th&gt;
&lt;th&gt;When to choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ASP.NET Core integration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HttpRequest&lt;/code&gt; / &lt;code&gt;IActionResult&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;New projects, or migrating from in-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HttpRequestData&lt;/code&gt; / &lt;code&gt;HttpResponseData&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Legacy compatibility only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ASP.NET Core integration is the path to take. It means your HTTP functions look exactly like ASP.NET Core controller actions, and all the routing, model binding, and result types you already know apply. It requires two things: the &lt;code&gt;Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore&lt;/code&gt; package, and &lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; in &lt;code&gt;Program.cs&lt;/code&gt; instead of &lt;code&gt;ConfigureFunctionsWorkerDefaults()&lt;/code&gt;. If you find tutorials using &lt;code&gt;HttpRequestData&lt;/code&gt;, they predate the ASP.NET Core integration and are using the older built-in types. You can use either, but the ASP.NET Core path removes an entire class of "why does this work differently than my API controllers?" questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output bindings
&lt;/h3&gt;

&lt;p&gt;In-process used &lt;code&gt;out&lt;/code&gt; parameters for output bindings. Isolated uses return values with attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: out parameters&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;outputMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: return value with output binding attribute&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders-processed"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders-pending"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;processedMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;out&lt;/code&gt; parameter approach was a side effect of how the WebJobs SDK wired up bindings. In isolated, bindings are attributes on the return type, which makes the data flow explicit: what the function returns is what gets written to the binding.&lt;/p&gt;

&lt;p&gt;When you need multiple outputs (for example, writing to a queue and returning an HTTP response), you define a dedicated return type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;MultiOutputResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;QueueOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dead-letter"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DeadLetterMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each property carries its own binding attribute. The runtime inspects the returned record and routes each value to the appropriate destination. This is more verbose than &lt;code&gt;out&lt;/code&gt; parameters for simple cases, but it makes multi-output functions far easier to read: every output is explicit in the return type definition rather than scattered across a function signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency injection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: FunctionsStartup + IFunctionsHostBuilder&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;FunctionsStartup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Startup&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Startup&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FunctionsStartup&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFunctionsHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMyService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: Program.cs (standard Generic Host)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMyService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FunctionsStartup&lt;/code&gt; was a Functions-specific extension point built on top of the Generic Host. In isolated, there is no extension point: your function app &lt;em&gt;is&lt;/em&gt; a Generic Host application. &lt;code&gt;Program.cs&lt;/code&gt; is the entry point, and the &lt;code&gt;Services&lt;/code&gt; property is a standard &lt;code&gt;IServiceCollection&lt;/code&gt;. Delete &lt;code&gt;Startup.cs&lt;/code&gt;, delete the &lt;code&gt;Microsoft.Azure.Functions.Extensions&lt;/code&gt; package reference, and move your service registrations into &lt;code&gt;Program.cs&lt;/code&gt;. There is nothing Functions-specific about how DI works after that.&lt;/p&gt;

&lt;h3&gt;
  
  
  ILogger injection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: ILogger passed as function parameter&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: inject ILogger&amp;lt;T&amp;gt; via constructor&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the in-process model, the runtime injected &lt;code&gt;ILogger&lt;/code&gt; directly into the function method as a parameter. That was a WebJobs SDK feature with no equivalent in the isolated model. In isolated, your function class is an ordinary class that the DI container constructs. You inject &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; through the constructor, exactly as you would in any .NET service. The generic type parameter means your logs are automatically scoped to the class name in Application Insights.&lt;/p&gt;

&lt;p&gt;Every function that currently takes &lt;code&gt;ILogger log&lt;/code&gt; as a method parameter needs to become an instance class with a constructor. That is one of the more time-consuming parts of migration for large codebases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package references and project type
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- In-process --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Library&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- implicit, often omitted --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk.Functions"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"4.x"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Isolated: a real executable --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Exe&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;FrameworkReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.App"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.21.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Sdk"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.17.2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.ApplicationInsights.WorkerService"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.22.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.ApplicationInsights"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;OutputType&amp;gt;Exe&amp;lt;/OutputType&amp;gt;&lt;/code&gt; is not optional. The isolated worker is a process that starts, connects to the host over gRPC, and runs until it is shut down. It is not a library that gets loaded into another process. The old &lt;code&gt;Microsoft.NET.Sdk.Functions&lt;/code&gt; meta-package is replaced by separate packages: the core worker, the build SDK (which handles source generation for bindings), the HTTP ASP.NET Core extension, and two Application Insights packages.&lt;/p&gt;

&lt;p&gt;Your binding extensions also change package namespace. Every &lt;code&gt;Microsoft.Azure.WebJobs.Extensions.*&lt;/code&gt; package becomes a &lt;code&gt;Microsoft.Azure.Functions.Worker.Extensions.*&lt;/code&gt; equivalent. The NuGet package names differ; the binding attributes inside them often keep the same names, which reduces the code changes needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static classes become instance classes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: static class (common pattern)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: instance class required for constructor injection&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static function classes were idiomatic in in-process because the runtime called your method directly by reflection and supplied everything through parameters. Constructor injection is impossible on a static class, so isolated requires instance classes. The compiler will not tell you this immediately: your static class will compile fine, but the runtime will fail to instantiate it because it cannot inject dependencies into a static constructor. Make the class non-static and add a constructor for your dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON serialization
&lt;/h3&gt;

&lt;p&gt;In-process defaulted to Newtonsoft.Json for binding serialization. Isolated defaults to &lt;code&gt;System.Text.Json&lt;/code&gt;. This is the change most likely to produce silent runtime bugs rather than compilation errors.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[JsonProperty("field_name")]&lt;/code&gt; does not exist in &lt;code&gt;System.Text.Json&lt;/code&gt;. The equivalent is &lt;code&gt;[JsonPropertyName("field_name")]&lt;/code&gt;. CamelCase naming defaults differ between the two libraries. Null handling, reference loop handling, and enum serialization all differ. If your functions receive JSON payloads, serialize objects to queues, or return JSON from HTTP triggers, test each one end-to-end after migration. A mismatch between what your function now serializes and what downstream consumers expect will not show up at compile time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Imperative bindings
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;IBinder&lt;/code&gt;, the in-process mechanism for creating bindings at runtime (for example, writing to a blob whose path you only know after reading a message), has no equivalent in isolated. The recommended replacement is injecting the Azure SDK client directly: &lt;code&gt;BlobServiceClient&lt;/code&gt;, &lt;code&gt;QueueClient&lt;/code&gt;, &lt;code&gt;ServiceBusClient&lt;/code&gt;. This is cleaner code in either model: SDK clients are testable, type-safe, and do not require the Functions binding infrastructure to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The .NET 10 requirement
&lt;/h2&gt;

&lt;p&gt;.NET 10 only supports the isolated model. There is no in-process support for .NET 10, and none is planned. If you are starting a new project today and targeting .NET 10, you are already using isolated by necessity; this article just explains the architecture behind it.&lt;/p&gt;

&lt;p&gt;The support matrix makes the direction clear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;.NET version&lt;/th&gt;
&lt;th&gt;In-process&lt;/th&gt;
&lt;th&gt;Isolated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 8 (LTS)&lt;/td&gt;
&lt;td&gt;Yes (final version)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 9 (STS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 10 (LTS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;.NET 8 is the last version the in-process model will ever support. If your function app runs on .NET 8 today, you are already at the ceiling. Staying on in-process means staying on .NET 8 until November 2026, then losing support entirely. Migrating to isolated means you can move to .NET 10 now, get the latest runtime improvements, and upgrade again when .NET 12 arrives without any coordination with the Azure Functions team.&lt;/p&gt;




&lt;h2&gt;
  
  
  The November 2026 deadline
&lt;/h2&gt;

&lt;p&gt;In-process model support ends on November 10, 2026. That date is not arbitrary: it aligns with the end-of-life date for .NET 8 LTS. After that point, in-process function apps receive no security patches, no bug fixes, and no platform updates. The deadline is firm.&lt;/p&gt;

&lt;p&gt;Start your inventory now. This PowerShell script identifies every in-process function app in your subscription:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$FunctionApps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-AzFunctionApp&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$FunctionApps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Runtime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'dotnet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - in-process, needs migration"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Runtime&lt;/code&gt; value of &lt;code&gt;dotnet&lt;/code&gt; means in-process. &lt;code&gt;dotnet-isolated&lt;/code&gt; means already migrated. Run this across every subscription where you might have deployed function apps, including non-production environments where older versions sometimes linger.&lt;/p&gt;

&lt;p&gt;The migration itself is mechanical for most functions: update packages, add &lt;code&gt;Program.cs&lt;/code&gt;, fix compilation errors, update attributes. The problem is not the mechanical work; it is the edge cases that surface during testing. A function that uses &lt;code&gt;IBinder&lt;/code&gt;, a binding attribute with changed properties, a JSON payload that now serializes differently: each one is a small investigation. In a codebase with dozens of functions, those investigations add up.&lt;/p&gt;

&lt;p&gt;The recommended order of attack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the script above and produce a full inventory with function counts per app.&lt;/li&gt;
&lt;li&gt;Start with the simplest apps: HTTP triggers, no output bindings, no &lt;code&gt;IBinder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update packages and &lt;code&gt;Program.cs&lt;/code&gt; first, then fix compilation errors function by function.&lt;/li&gt;
&lt;li&gt;Test locally with &lt;code&gt;func start&lt;/code&gt; before touching Azure.&lt;/li&gt;
&lt;li&gt;Deploy to a staging slot and run your smoke tests before swapping to production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The staging slot step matters more here than in typical deployments. When you swap, the &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; app setting switches from &lt;code&gt;dotnet&lt;/code&gt; to &lt;code&gt;dotnet-isolated&lt;/code&gt;. If your code and the app setting get out of sync even briefly, the app enters an error state. Slots let you validate the new configuration in production infrastructure before making it live.&lt;/p&gt;

&lt;p&gt;Waiting until Q3 2026 to start leaves no room for the edge cases. Start with your least critical app now, learn the migration pattern in a low-stakes context, and work forward from there.&lt;/p&gt;




&lt;h2&gt;
  
  
  One more thing: Flex Consumption is isolated-only
&lt;/h2&gt;

&lt;p&gt;The Flex Consumption plan is Microsoft's newest Azure Functions hosting option. It scales each function independently rather than scaling the whole app, supports always-ready instances that eliminate cold starts for your busiest functions, and bills at a finer granularity than the standard Consumption plan. If any of that sounds appealing, the isolated worker model is a prerequisite.&lt;/p&gt;

&lt;p&gt;In-process cannot run on Flex Consumption at all. The two are architecturally incompatible: Flex Consumption requires the worker process model to manage per-function scaling, and in-process has no worker process to manage.&lt;/p&gt;

&lt;p&gt;If you are evaluating hosting options for a new function app, that decision is already made for you: Flex Consumption is isolated-only, and isolated is where all future platform investment is going. Starting on in-process today means either migrating before you can move to Flex Consumption, or accepting a hosting model that cannot take advantage of the newest platform capabilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration: two surfaces, not one
&lt;/h2&gt;

&lt;p&gt;After migration, one category of bug appears repeatedly: a developer configures something in &lt;code&gt;Program.cs&lt;/code&gt; and it has no effect on trigger behavior. The root cause is always the same: the isolated model has two separate configuration surfaces, one for the host process and one for the worker process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Host configuration&lt;/strong&gt; governs triggers, bindings, and scaling. Two sources feed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;host.json&lt;/code&gt;: extension settings, retry policies, connection concurrency, scale behavior.&lt;/li&gt;
&lt;li&gt;Environment variables and Azure App Service application settings: connection strings that the host uses to connect to Service Bus, Storage, Event Hub, and other binding sources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Worker configuration&lt;/strong&gt; governs your application code. Two sources feed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;appsettings.json&lt;/code&gt;: loaded automatically by &lt;code&gt;FunctionsApplication.CreateBuilder()&lt;/code&gt;, accessible through &lt;code&gt;IConfiguration&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Anything you wire up in &lt;code&gt;Program.cs&lt;/code&gt;: additional configuration providers, secrets, feature flags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical rule: connection strings for bindings go in environment variables (or App Service application settings in production), not in &lt;code&gt;appsettings.json&lt;/code&gt;. The host process that initializes the Service Bus trigger listener or the Storage queue poller reads from environment variables. It cannot read your worker's &lt;code&gt;appsettings.json&lt;/code&gt;. A connection string that lives only in &lt;code&gt;appsettings.json&lt;/code&gt; will work fine for any code in your worker that reads it directly (for example, an Azure SDK client you construct manually) but will cause the binding itself to fail at startup with a cryptic "missing connection string" error.&lt;/p&gt;

&lt;p&gt;Locally, &lt;code&gt;local.settings.json&lt;/code&gt; maps its &lt;code&gt;Values&lt;/code&gt; section into environment variables when the Functions host starts, which is why everything works in local development even when you have not thought carefully about this split. In Azure, you configure application settings in the portal or via deployment scripts, and they become environment variables for both processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration gotchas worth knowing in advance
&lt;/h2&gt;

&lt;p&gt;The following issues are not obvious from the migration documentation and tend to surface late, when you are integrating and testing rather than making mechanical code changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;ILogger&lt;/code&gt; as a method parameter is gone.&lt;/strong&gt; The runtime no longer injects it. Every function that currently takes &lt;code&gt;ILogger log&lt;/code&gt; as a parameter needs to become an instance class with a constructor that accepts &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt;. In large codebases, this touches many files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;JSON serialization changes silently.&lt;/strong&gt; Moving from Newtonsoft.Json to &lt;code&gt;System.Text.Json&lt;/code&gt; changes how your bindings serialize and deserialize data. &lt;code&gt;[JsonProperty]&lt;/code&gt; becomes &lt;code&gt;[JsonPropertyName]&lt;/code&gt;. Null values, camelCase defaults, and enum handling all differ. A function that processes messages from a queue may silently start deserializing them incorrectly if the attribute names change. Test every binding that touches JSON.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application Insights log filtering moves from &lt;code&gt;host.json&lt;/code&gt; to &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/strong&gt; Log levels configured under &lt;code&gt;logging.logLevel&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt; no longer apply to code running in the worker process. To filter worker logs, call &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; in &lt;code&gt;Program.cs&lt;/code&gt; and configure the log level there. Without this, you may find your worker logs missing from Application Insights, or flooded with debug output you expected to filter out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;IAsyncCollector&amp;lt;T&amp;gt;&lt;/code&gt; has no direct equivalent.&lt;/strong&gt; If your functions write multiple messages to a queue or table using &lt;code&gt;IAsyncCollector&amp;lt;T&amp;gt;&lt;/code&gt;, replace it with an array property on a dedicated return type. &lt;code&gt;IAsyncCollector&amp;lt;string&amp;gt;&lt;/code&gt; becomes &lt;code&gt;string[]&lt;/code&gt; on a record that your function returns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blob binding attribute properties changed.&lt;/strong&gt; &lt;code&gt;[Blob("container/path", FileAccess.Write)]&lt;/code&gt; does not exist in the isolated extension. The equivalent is &lt;code&gt;[BlobOutput("container/path")]&lt;/code&gt;. The &lt;code&gt;FileAccess&lt;/code&gt; enum property was removed; the direction is now expressed by whether you use &lt;code&gt;[BlobInput]&lt;/code&gt; or &lt;code&gt;[BlobOutput]&lt;/code&gt;. This is a compilation error, which means you will catch it, but expect to update every blob binding attribute.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom configuration in &lt;code&gt;Program.cs&lt;/code&gt; is invisible to the host.&lt;/strong&gt; If you read a connection string from &lt;code&gt;appsettings.json&lt;/code&gt; in &lt;code&gt;Program.cs&lt;/code&gt; and wire it up to a service, that configuration does not flow to the binding runtime. Trigger connections must come from environment variables. This is a duplicate of the two-surfaces rule above, but it is worth restating because it manifests as a confusing runtime error rather than a compilation failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; and the deployed code must change together.&lt;/strong&gt; If the app setting in Azure says &lt;code&gt;dotnet&lt;/code&gt; but you deploy isolated code (or the reverse), the function app enters an error state on startup. Use deployment slots to change the app setting and deploy the code atomically, then validate in staging before swapping to production.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;The isolated model is the foundation everything else in this series sits on. The HTTP trigger patterns in Part 2 and the timer, queue, and blob triggers in Part 3 all assumed isolated; now you know why those patterns look the way they do. The local development setup in Part 4 works as it does because the worker process can be debugged independently of the host. The architecture is not incidental; it shapes every practical detail.&lt;/p&gt;

&lt;p&gt;If you are migrating an existing in-process app, the &lt;a href="https://learn.microsoft.com/azure/azure-functions/migrate-dotnet-to-isolated-model" rel="noopener noreferrer"&gt;Microsoft migration guide&lt;/a&gt; walks through the steps with tooling support including a migration tool that handles some of the mechanical changes automatically. Use it as a checklist, but read through the sections on JSON serialization and Application Insights filtering before you declare the migration done: those two areas produce the most post-migration bugs.&lt;/p&gt;

&lt;p&gt;If you are starting a new project, start on isolated and .NET 10. The in-process model has no future, and building on it today means doing this migration under deadline pressure later.&lt;/p&gt;

&lt;p&gt;Which part of the migration gave you the most trouble, or are you starting fresh with isolated from day one?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 5: Understanding the Isolated Worker Model (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Local Development Setup: Tools, Debugging, and Hot Reload</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 27 Feb 2026 06:32:38 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925</link>
      <guid>https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4: Local Development Setup: Tools, Debugging, and Hot Reload (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;You add a breakpoint. You press F5. The function executes. The breakpoint never fires.&lt;/p&gt;

&lt;p&gt;This is the most common introduction to Azure Functions local development. The reason is non-obvious: when you debug a .NET isolated worker function, two separate processes run. Your debugger attached to the host process (&lt;code&gt;func.exe&lt;/code&gt;) instead of the worker process (&lt;code&gt;dotnet.exe&lt;/code&gt;), which is where your code actually executes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;local.settings.json&lt;/code&gt; causes a different kind of confusion. Unlike &lt;code&gt;appsettings.json&lt;/code&gt;, it injects environment variables: put a connection string in the wrong section and your bindings silently break. Azurite is a related trap: timer, queue, and blob triggers all use Azure Storage internally, so the emulator has to be running before those trigger types will initialize, even if your function code touches no storage directly.&lt;/p&gt;

&lt;p&gt;This covers the full local setup for Azure Functions with .NET 10 and the isolated worker model. What's not here: deployment, Application Insights (that's Part 9), or unit testing (Part 7).&lt;/p&gt;




&lt;h2&gt;
  
  
  What you need: three installs
&lt;/h2&gt;

&lt;p&gt;Three things have to be installed and working before you can run any Azure Function locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A. .NET 10 SDK&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# expect 10.x.x&lt;/span&gt;
dotnet &lt;span class="nt"&gt;--list-sdks&lt;/span&gt; &lt;span class="c"&gt;# confirm 10.x is listed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you followed Parts 1-3, this is already done. If not, download from &lt;a href="https://dotnet.microsoft.com/download" rel="noopener noreferrer"&gt;dot.net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B. Azure Functions Core Tools v4&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Core Tools provides the &lt;code&gt;func&lt;/code&gt; CLI that starts the local Functions host. Install it with your package manager:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: &lt;code&gt;brew tap azure/functions &amp;amp;&amp;amp; brew install azure-functions-core-tools@4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: &lt;code&gt;winget install Microsoft.Azure.FunctionsCoreTools&lt;/code&gt; or the &lt;a href="https://github.com/Azure/azure-functions-core-tools/releases" rel="noopener noreferrer"&gt;MSI download&lt;/a&gt; (64-bit required for VS Code debugging)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: APT install from Microsoft package feeds (see &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-run-local" rel="noopener noreferrer"&gt;the official instructions&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm (cross-platform)&lt;/strong&gt;: &lt;code&gt;npm i -g azure-functions-core-tools@4 --unsafe-perm true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note on the npm option for Windows: if you are using &lt;code&gt;Microsoft.Azure.Functions.Worker.Sdk&lt;/code&gt; 2.x, which enables &lt;code&gt;dotnet run&lt;/code&gt; support, the npm Core Tools installation does not work correctly for that workflow. Use the winget or MSI install instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# expect 4.x.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;C. Azurite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azurite emulates Azure Blob, Queue, and Table storage locally. Install it once; you will need it running for every non-HTTP function.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VS Code extension&lt;/strong&gt; (recommended for VS Code users): search "Azurite" in the Extensions panel, or install &lt;code&gt;Azurite.azurite&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;code&gt;npm install -g azurite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt;: &lt;code&gt;docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Studio 2026&lt;/strong&gt;: Azurite is bundled and starts automatically when you press F5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old Azure Storage Emulator is deprecated. If you see references to it in older articles or documentation, ignore them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this sequence to confirm everything is wired up correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet &lt;span class="nt"&gt;--version&lt;/span&gt;
func &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Start Azurite: either the VS Code extension (Command Palette: "Azurite: Start") or `azurite` in a terminal&lt;/span&gt;
func init TestProj &lt;span class="nt"&gt;--worker-runtime&lt;/span&gt; dotnet-isolated &lt;span class="nt"&gt;--target-framework&lt;/span&gt; net10.0
&lt;span class="nb"&gt;cd &lt;/span&gt;TestProj
func start
&lt;span class="c"&gt;# Should print: "Host lock lease acquired" with no errors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;SocketException: Unable to connect to the remote server&lt;/code&gt; instead of the lock lease message, Azurite is not running. Start it first, then retry &lt;code&gt;func start&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two-process model
&lt;/h2&gt;

&lt;p&gt;This is the section most local dev tutorials skip, which is why "my breakpoint won't fire" is such a common question.&lt;/p&gt;

&lt;p&gt;In the isolated worker model, two separate processes run every time you start a debug session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────┐
│   func.exe  (the Functions host)         │
│   - Manages triggers                     │
│   - Handles bindings                     │
│   - Routes HTTP requests                 │
│   - Runs on port 7071                    │
└──────────────────────┬───────────────────┘
                       │ gRPC
                       │
┌──────────────────────┴───────────────────┐
│   dotnet.exe  (your worker)              │
│   - Runs your actual function code       │
│   - Gets invoked by the host via gRPC    │
│   - This is where breakpoints fire       │
└──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;func.exe&lt;/code&gt; is the Functions runtime: it manages triggers, handles bindings, and routes incoming requests. &lt;code&gt;dotnet.exe&lt;/code&gt; is your application: it receives invocation requests from the host over gRPC and runs your actual function code. Breakpoints live in &lt;code&gt;dotnet.exe&lt;/code&gt;, not &lt;code&gt;func.exe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This matters for debugging in two ways. First, your debugger must attach to the worker process (&lt;code&gt;dotnet.exe&lt;/code&gt;), not to &lt;code&gt;func.exe&lt;/code&gt;. This is why the &lt;code&gt;launch.json&lt;/code&gt; the Azure Functions extension generates uses &lt;code&gt;"request": "attach"&lt;/code&gt; rather than &lt;code&gt;"request": "launch"&lt;/code&gt;, and why the process ID is set to &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt; instead of a static value. Second, the two processes produce separate log streams with separate configuration: &lt;code&gt;host.json&lt;/code&gt; controls what &lt;code&gt;func.exe&lt;/code&gt; logs, while your &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; calls are configured on the worker side. Changing one does not affect the other.&lt;/p&gt;

&lt;p&gt;For comparison: the deprecated in-process model runs everything in a single process. One process, one debugger attach target. That simplicity is gone with the isolated model, but the isolation is what allows it to run on .NET 10 instead of being locked to .NET 8.&lt;/p&gt;




&lt;h2&gt;
  
  
  local.settings.json: not config, environment variables
&lt;/h2&gt;

&lt;p&gt;A common mental model for &lt;code&gt;local.settings.json&lt;/code&gt; is that it works like &lt;code&gt;appsettings.json&lt;/code&gt;. It does not.&lt;/p&gt;

&lt;p&gt;Everything in the &lt;code&gt;Values&lt;/code&gt; section is read by Core Tools at startup and injected as process environment variables into both &lt;code&gt;func.exe&lt;/code&gt; and the worker process. That is the entire job of this file when running locally. In Azure, there is no &lt;code&gt;local.settings.json&lt;/code&gt; at all: the App Settings page in the portal (or the equivalent in Bicep/ARM/Terraform) sets those same environment variables directly on the hosted function app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MyQueueConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MyServiceBusConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;real-sb-connection-string&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LocalHttpPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7071&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CORS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CORSCredentials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AppDb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=localhost;Database=MyApp;Trusted_Connection=True;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two keys in &lt;code&gt;Values&lt;/code&gt; are required for every project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt;: must be &lt;code&gt;"dotnet-isolated"&lt;/code&gt; (not &lt;code&gt;"dotnet"&lt;/code&gt;, which is the in-process value; using it causes confusing startup failures)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AzureWebJobsStorage&lt;/code&gt;: required for every trigger type except HTTP. The host uses this storage account for timer leases, key management, and Durable Functions. Set to &lt;code&gt;"UseDevelopmentStorage=true"&lt;/code&gt; when Azurite is running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ConnectionStrings&lt;/code&gt; section gotcha&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ConnectionStrings&lt;/code&gt; is for Entity Framework and similar frameworks that call &lt;code&gt;IConfiguration.GetConnectionString("Name")&lt;/code&gt;. It is not for Functions trigger and binding configuration. If you put your Service Bus or Queue connection string here, the binding's &lt;code&gt;Connection&lt;/code&gt; property won't find it, and the error message won't point you at the right place. All binding connection strings must go in &lt;code&gt;Values&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the file is gitignored&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even if your local values only point at Azurite today, a real connection string will appear in this file at some point, whether you add it or a teammate does. Once committed, it lives in git history. The &lt;code&gt;.gitignore&lt;/code&gt; and &lt;code&gt;.funcignore&lt;/code&gt; that &lt;code&gt;func init&lt;/code&gt; generates both exclude &lt;code&gt;local.settings.json&lt;/code&gt; from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing the structure with the team&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The standard approach: commit a &lt;code&gt;local.settings.json.example&lt;/code&gt; file with the same structure but placeholder values. New team members copy it to &lt;code&gt;local.settings.json&lt;/code&gt; and fill in the real values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ServiceBusConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-service-bus-connection-string&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Document in the README which values work with Azurite and which require a real Azure service. Your teammates will thank you the first time they clone the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading values in code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Direct access anywhere in your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyKey"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recommended approach: inject &lt;code&gt;IConfiguration&lt;/code&gt; and read by key name. Since the values are environment variables, the key name is the exact string in &lt;code&gt;Values&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProcessor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connStr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MyQueueConnection"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Part 6 covers structuring values for testability with &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt;, including the double-underscore separator needed for nested configuration in environment variables.&lt;/p&gt;




&lt;h2&gt;
  
  
  Azurite: your local storage account
&lt;/h2&gt;

&lt;p&gt;Azurite emulates the three Azure Storage services locally: Blob, Queue, and Table. Azure Functions uses these services internally, which is why Azurite has to be running even if your function code does not touch storage directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the host needs storage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Functions host uses Azure Storage for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timer trigger lease management&lt;/strong&gt;: prevents duplicate executions when multiple instances run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function key storage&lt;/strong&gt;: host keys and function keys are persisted in blob storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable Functions task hub&lt;/strong&gt;: state and message coordination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Hubs checkpoints&lt;/strong&gt;: tracks which events have been processed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;AzureWebJobsStorage&lt;/code&gt; in &lt;code&gt;local.settings.json&lt;/code&gt; is the connection string for this internal storage use. Only pure HTTP-only projects can skip it. Everything else needs Azurite running first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which service maps to which trigger&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Azurite service&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Used by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blob service&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;Blob trigger, Blob input/output bindings, host key storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue service&lt;/td&gt;
&lt;td&gt;10001&lt;/td&gt;
&lt;td&gt;Queue trigger, Queue output binding, Durable Functions activity queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table service&lt;/td&gt;
&lt;td&gt;10002&lt;/td&gt;
&lt;td&gt;Table input/output bindings, Durable Functions instance state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Azurite Table Storage is still in preview as of February 2026. It works for most local development scenarios but is not production-equivalent.&lt;/p&gt;

&lt;p&gt;You can start individual services if you want to be selective: &lt;code&gt;azurite-blob&lt;/code&gt;, &lt;code&gt;azurite-queue&lt;/code&gt;, or &lt;code&gt;azurite-table&lt;/code&gt; as separate commands. From the VS Code Command Palette: &lt;strong&gt;Azurite: Start Blob Service&lt;/strong&gt;, &lt;strong&gt;Azurite: Start Queue Service&lt;/strong&gt;, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection strings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The shorthand that works when Azurite runs on the default ports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;UseDevelopmentStorage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full explicit form, needed when Azurite is in Docker or using non-default ports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DefaultEndpointsProtocol&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;devstoreaccount1&lt;/code&gt; account name and the key above are the public well-known Azurite credentials, the same for every developer. They carry no security value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common gotchas&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Port conflicts&lt;/strong&gt;: if ports 10000, 10001, or 10002 are already in use, Azurite fails silently or with a cryptic error. Check with &lt;code&gt;lsof -i :10000&lt;/code&gt; (macOS/Linux) or &lt;code&gt;netstat -ano | findstr :10000&lt;/code&gt; (Windows). Run with &lt;code&gt;--blobPort 20000 --queuePort 20001 --tablePort 20002&lt;/code&gt; to use alternate ports, and update your connection strings accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data location&lt;/strong&gt;: running &lt;code&gt;azurite&lt;/code&gt; with no &lt;code&gt;--location&lt;/code&gt; flag writes data files to the current working directory. The VS Code extension stores data in the workspace folder. Run &lt;strong&gt;Azurite: Clean&lt;/strong&gt; from the Command Palette to wipe all data and start fresh, which is useful when you want to test a clean startup sequence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docker networking&lt;/strong&gt;: &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; resolves to &lt;code&gt;127.0.0.1&lt;/code&gt;, meaning the local machine. If your function host runs in a Docker container, &lt;code&gt;127.0.0.1&lt;/code&gt; points inside that container, not at Azurite running on your host. Use &lt;code&gt;host.docker.internal&lt;/code&gt; in the explicit connection string form instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never publish &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; to Azure&lt;/strong&gt;: the Functions host will fail to start. Azure App Settings needs a real storage account connection string, not the Azurite shorthand.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Debugging in VS Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Required extensions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two extensions are needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Functions&lt;/strong&gt; (&lt;code&gt;ms-azuretools.vscode-azurefunctions&lt;/code&gt;): provides the &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt; variable that makes F5 work, and the &lt;strong&gt;Execute Function Now...&lt;/strong&gt; command for triggering non-HTTP functions from the editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C# Dev Kit&lt;/strong&gt; (&lt;code&gt;ms-dotnettools.csdevkit&lt;/code&gt;): the recommended C# extension for new setups. Installs the base C# extension (&lt;code&gt;ms-dotnettools.csharp&lt;/code&gt;) automatically, which provides the &lt;code&gt;coreclr&lt;/code&gt; debugger needed for attaching to the worker process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended additions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azurite&lt;/strong&gt; (&lt;code&gt;Azurite.azurite&lt;/code&gt;): start and stop Azurite directly from the Command Palette without switching to a terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST Client&lt;/strong&gt; (&lt;code&gt;humao.rest-client&lt;/code&gt;): run &lt;code&gt;.http&lt;/code&gt; files against your functions from inside the editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The F5 workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you press F5 on a Functions project, the following sequence runs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;VS Code executes the &lt;code&gt;build (functions)&lt;/code&gt; task from &lt;code&gt;tasks.json&lt;/code&gt; (a &lt;code&gt;dotnet build&lt;/code&gt; with &lt;code&gt;--no-incremental&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The Azure Functions extension starts &lt;code&gt;func start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;func.exe&lt;/code&gt; launches the worker process (&lt;code&gt;dotnet.exe&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The extension resolves the worker PID via &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;coreclr&lt;/code&gt; debugger attaches to the worker process&lt;/li&gt;
&lt;li&gt;Breakpoints activate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9mmw9i7a0an1j7tfk2dm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9mmw9i7a0an1j7tfk2dm.png" alt="VS Code debug session paused at a breakpoint, with the func host running in the terminal and .NET worker threads visible in the Call Stack panel" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated &lt;code&gt;launch.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;func init&lt;/code&gt; for a dotnet-isolated project generates this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Attach to .NET Functions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coreclr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"processId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${command:azureFunctions.pickProcess}"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"request": "attach"&lt;/code&gt; is not a mistake. The worker process is started by &lt;code&gt;func.exe&lt;/code&gt;, not by VS Code, so VS Code cannot launch it. It can only attach to it after the fact. The &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt; variable is what prompts the process picker if the extension cannot auto-detect the right PID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;tasks.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The extension generates five tasks. The critical one is the &lt;code&gt;func: host start&lt;/code&gt; task at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clean (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"clean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/MyProject.csproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/property:GenerateFullPaths=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/consoleloggerparameters:NoSummary"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"problemMatcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$msCompile"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/MyProject.csproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/property:GenerateFullPaths=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/consoleloggerparameters:NoSummary"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clean (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isDefault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"problemMatcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$msCompile"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"func: host start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"host start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/bin/Debug/net10.0"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"isBackground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"problemMatcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$func-dotnet-isolated-watch"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;func&lt;/code&gt; task type points &lt;code&gt;cwd&lt;/code&gt; at the compiled output directory because the isolated worker is an executable that runs from there. The &lt;code&gt;problemMatcher&lt;/code&gt; is &lt;code&gt;$func-dotnet-isolated-watch&lt;/code&gt; (not &lt;code&gt;$func-dotnet-watch&lt;/code&gt;, which is for in-process). The &lt;code&gt;dependsOn&lt;/code&gt; chain means F5 triggers a build before starting the host.&lt;/p&gt;

&lt;p&gt;If you create a project via &lt;code&gt;func init&lt;/code&gt; on the CLI instead of through the VS Code extension, you may not get &lt;code&gt;.vscode&lt;/code&gt; files automatically. Run &lt;strong&gt;Azure Functions: Initialize Project for Use with VS Code&lt;/strong&gt; from the Command Palette to generate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting breakpoints and inspecting state&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click the gutter (left of line numbers) to set a breakpoint. When the function is triggered, execution pauses. From there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variables panel&lt;/strong&gt;: all locals and parameters in scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover over any identifier&lt;/strong&gt;: shows its current value inline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug Console&lt;/strong&gt;: evaluates any C# expression against live state (LINQ works, method calls work)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call Stack panel&lt;/strong&gt;: the full call chain from the gRPC dispatcher into your function code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwug8gr0zqloj0qz8q6xd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwug8gr0zqloj0qz8q6xd.png" alt="VS Code Debug Console evaluating a C# expression against live function state" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing non-HTTP functions from VS Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Command Palette → &lt;strong&gt;Azure Functions: Execute Function Now...&lt;/strong&gt; → select &lt;strong&gt;Local project&lt;/strong&gt; → pick the function.&lt;/p&gt;

&lt;p&gt;This posts to the admin endpoint: &lt;code&gt;POST http://localhost:7071/admin/functions/{FunctionName}&lt;/code&gt; with &lt;code&gt;{"input": ""}&lt;/code&gt;. For queue triggers, set &lt;code&gt;input&lt;/code&gt; to the message body you want to inject.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Opening a subfolder of your project instead of the folder containing &lt;code&gt;host.json&lt;/code&gt; causes F5 to fail without a clear error. The &lt;code&gt;.vscode&lt;/code&gt; folder is not found. Always open the project root.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debugging in Visual Studio 2026
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Visual Studio 2026 (GA since November 2025) is required for .NET 10 development. Visual Studio 2022 cannot target &lt;code&gt;net10.0&lt;/code&gt;. Install Visual Studio 2026 from the standard Visual Studio installer.&lt;/p&gt;

&lt;p&gt;Install the &lt;strong&gt;Azure development&lt;/strong&gt; workload during setup or add it afterward via the Visual Studio Installer. This adds the Functions project templates and Core Tools integration. Update Core Tools separately when Visual Studio prompts, or through &lt;strong&gt;Tools | Options | Projects and Solutions | Azure Functions | Check for Updates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The F5 workflow&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visual Studio builds the project&lt;/li&gt;
&lt;li&gt;Visual Studio launches &lt;code&gt;func.exe&lt;/code&gt; (output visible in the terminal pane)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;func.exe&lt;/code&gt; starts the worker (&lt;code&gt;dotnet.exe&lt;/code&gt;) and reports the worker PID back over gRPC&lt;/li&gt;
&lt;li&gt;Visual Studio automatically attaches its debugger to the worker &lt;code&gt;dotnet.exe&lt;/code&gt; process&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No &lt;code&gt;launch.json&lt;/code&gt; is needed. Visual Studio handles the two-process attach automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Useful VS-specific features&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App Settings dialog&lt;/strong&gt;: right-click the project → &lt;strong&gt;Publish&lt;/strong&gt; → &lt;strong&gt;Hosting&lt;/strong&gt; → &lt;strong&gt;Manage Azure App Service settings&lt;/strong&gt;. It shows a side-by-side view of local &lt;code&gt;local.settings.json&lt;/code&gt; values vs. Azure App Settings, and lets you push individual values to Azure with one click, useful for keeping environments in sync without manually copying strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote debugging&lt;/strong&gt;: attach to a deployed function app from Visual Studio. Requires Premium or App Service plan (not Consumption or Flex), Windows OS, and a Debug build. Available for 48 hours before it auto-disables. Use &lt;strong&gt;Attach to Process&lt;/strong&gt;, set the connection type to &lt;strong&gt;Microsoft Azure App Services&lt;/strong&gt;, then select &lt;code&gt;dotnet.exe&lt;/code&gt; in the process list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Known issue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In some Core Tools versions, starting with debugging can take 60 seconds to attach while starting without debugging is instant. Updating Core Tools via &lt;strong&gt;Tools | Options | Azure Functions&lt;/strong&gt; resolves this in most cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debugging in Rider
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Plugin requirement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Azure Toolkit for Rider&lt;/strong&gt; is not bundled with Rider and is not part of the JetBrains marketplace defaults. Install it from &lt;strong&gt;Settings | Plugins | Marketplace&lt;/strong&gt;, search for "Azure Toolkit for Rider". The current version is v4.6.5 (November 2025), a full rewrite of v3.x. Existing v3.x run configurations are not forward-compatible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The run configuration trap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you open a Functions project, Rider generates two run configurations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;.NET Launch Settings Profile&lt;/strong&gt;: based on &lt;code&gt;launchSettings.json&lt;/code&gt;. This does not work for Azure Functions. Ignore it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Function Host&lt;/strong&gt;: the correct one. Rider calls &lt;code&gt;func.exe&lt;/code&gt; with the right flags internally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F416v2vosjtftj6mhbjrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F416v2vosjtftj6mhbjrs.png" alt="Rider run configuration dialog showing Azure Function Host and .NET Launch Settings Profile entries" width="800" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you see &lt;strong&gt;"Broken configuration due to unavailable plugin"&lt;/strong&gt; after installing or updating to v4.0, delete the old configuration and create a new "Azure Function Host" configuration from scratch via &lt;strong&gt;Run | Edit Configurations | + | Azure Function Host&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select the &lt;strong&gt;Azure Function Host&lt;/strong&gt; configuration and press Shift+F9. Rider:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Builds the project&lt;/li&gt;
&lt;li&gt;Starts &lt;code&gt;func.exe&lt;/code&gt; with &lt;code&gt;--dotnet-isolated-debug&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reads the worker PID from the host's stdout&lt;/li&gt;
&lt;li&gt;Attaches the Rider debugger to the worker process&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each &lt;code&gt;[Function]&lt;/code&gt; method gets gutter icons: the bug icon starts that function in debug mode, and the play icon opens a &lt;code&gt;.http&lt;/code&gt; scratch file with an invocation request ready to send from Rider's built-in HTTP client.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmhwohlu7ta40u11vg7to.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmhwohlu7ta40u11vg7to.png" alt="Rider gutter icons on a function method — bug icon for debug, play icon for HTTP scratch file" width="509" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known bugs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Azure Functions host did not return isolated worker process id"&lt;/strong&gt;: Rider reads the PID from the host log output. If the logging level in &lt;code&gt;host.json&lt;/code&gt; is set above &lt;code&gt;Information&lt;/code&gt;, the PID line is suppressed and Rider cannot detect the process. Fix: set &lt;code&gt;"logging": { "logLevel": { "default": "Information" } }&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt;. JetBrains has marked this as &lt;code&gt;wontfix&lt;/code&gt; upstream: the PID must come from the host log line.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Slow or failed debugger attach&lt;/strong&gt;: start without debugging via the Run button, then attach manually through &lt;strong&gt;Run | Attach to Process&lt;/strong&gt; and select the worker &lt;code&gt;dotnet&lt;/code&gt; process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Port configuration&lt;/strong&gt;: the Azure Function Host run configuration does not read the port from &lt;code&gt;launchSettings.json&lt;/code&gt;. Set the port directly in &lt;strong&gt;Run | Edit Configurations | Function host arguments&lt;/strong&gt;. The default 7071 is fine for most setups.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Hot reload and dotnet watch
&lt;/h2&gt;

&lt;p&gt;Hot reload support for Azure Functions isolated worker depends on which tool starts the session:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Start method&lt;/th&gt;
&lt;th&gt;What happens on save&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visual Studio F5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True hot reload (Edit and Continue). Most method body changes apply without restart.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;dotnet watch&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full process restart. All in-memory state is lost.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;func start&lt;/code&gt;&lt;/strong&gt; (and Rider's Azure Function Host config)&lt;/td&gt;
&lt;td&gt;No change detection. Stop and restart manually.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code F5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full process restart when you stop and re-run.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Visual Studio hot reload&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Changes that apply without a restart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code changes inside existing method bodies&lt;/li&gt;
&lt;li&gt;Adding new methods, properties, or fields to existing types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changes that require a full restart (rude edits):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding a new trigger function (new &lt;code&gt;[Function]&lt;/code&gt; attribute)&lt;/li&gt;
&lt;li&gt;Changing binding attributes (&lt;code&gt;[QueueTrigger]&lt;/code&gt;, &lt;code&gt;[BlobTrigger]&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Changing &lt;code&gt;Program.cs&lt;/code&gt; startup code&lt;/li&gt;
&lt;li&gt;Changes to &lt;code&gt;host.json&lt;/code&gt; or &lt;code&gt;local.settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adding new types or changing method signatures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The binding attribute restriction is the most relevant one for daily Functions development: any time you add a new trigger or modify a binding, you need a full restart.&lt;/p&gt;

&lt;p&gt;One gotcha: mixed-mode debugging (the "Enable native code debugging" checkbox in project properties) breaks hot reload. If hot reload stopped working after enabling that option, that's why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;dotnet watch&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Microsoft.Azure.Functions.Worker.Sdk&lt;/code&gt; 2.0.0+, &lt;code&gt;dotnet run&lt;/code&gt; is supported when Core Tools is installed. &lt;code&gt;dotnet watch&lt;/code&gt; wraps &lt;code&gt;dotnet run&lt;/code&gt; and restarts the process on file changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a full restart, not hot reload. Useful if you want automatic restarts without manually stopping and restarting &lt;code&gt;func start&lt;/code&gt;, but in-memory state does not survive between restarts. On Windows, requires the MSI or winget Core Tools installation (not npm).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;--dotnet-isolated-debug&lt;/code&gt; flag&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func start &lt;span class="nt"&gt;--dotnet-isolated-debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flag starts the worker process, pauses it immediately, and prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Azure Functions .NET Worker (PID: 28664) initialized in debug mode. Waiting for debugger to attach...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process stays paused until you manually attach a debugger. Use it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to debug &lt;code&gt;Program.cs&lt;/code&gt; startup code before any function gets called (the worker pauses before startup completes, so you can attach and step through the entire initialization sequence)&lt;/li&gt;
&lt;li&gt;VS Code or Rider is not auto-attaching to the correct process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For typical VS Code or Rider development, you do not need this flag. The IDE extensions handle process detection automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing non-HTTP triggers locally
&lt;/h2&gt;

&lt;p&gt;Every locally running Functions host exposes an admin API. Any trigger type can be invoked through it, without waiting for the actual trigger to fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The admin endpoint&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Timer trigger (input can be empty)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/CleanupFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": ""}'&lt;/span&gt;

&lt;span class="c"&gt;# Queue trigger (input = the queue message body)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/OrderProcessor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": "{\"orderId\": \"ord-123\", \"amount\": 99.99}"}'&lt;/span&gt;

&lt;span class="c"&gt;# Blob trigger&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/ImageProcessor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": "test-blob-content"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function name in the URL is the string in &lt;code&gt;[Function("...")]&lt;/code&gt;, not the C# method name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simulating real storage events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The admin endpoint is fast for testing, but it bypasses the actual trigger mechanism. For testing the trigger polling behavior, use Azurite directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a message to an Azurite queue: the queue trigger picks it up on the next poll cycle (default: 2 seconds locally)&lt;/li&gt;
&lt;li&gt;Upload a blob to an Azurite container: the blob trigger fires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Azure Storage Explorer&lt;/strong&gt; (the desktop app) and the &lt;strong&gt;Azure Storage&lt;/strong&gt; VS Code extension both work against Azurite. Point them at &lt;code&gt;http://127.0.0.1:10000&lt;/code&gt; and use the well-known &lt;code&gt;devstoreaccount1&lt;/code&gt; credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.http&lt;/code&gt; scratch files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;requests.http&lt;/code&gt; in the project root for quick invocations during development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;### Trigger cleanup manually
POST http://localhost:7071/admin/functions/CleanupFunction
Content-Type: application/json

{"input": ""}

### Test HTTP endpoint
POST http://localhost:7071/api/orders
Content-Type: application/json

{
  "orderId": "ord-123",
  "customerId": "cust-456",
  "amount": 99.99
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;REST Client (VS Code), Rider's built-in HTTP client, and Visual Studio's native &lt;code&gt;.http&lt;/code&gt; editor all execute these files with a click.&lt;/p&gt;




&lt;h2&gt;
  
  
  Productivity tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Two log streams, two configs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the isolated worker model, logging is split across two configuration points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;host.json&lt;/code&gt;&lt;/strong&gt;: controls the Functions host logs (trigger polling, execution lifecycle, binding errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker-side&lt;/strong&gt; (&lt;code&gt;appsettings.json&lt;/code&gt; or &lt;code&gt;Program.cs&lt;/code&gt;): controls your &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changes to one do not affect the other. To see your &lt;code&gt;Information&lt;/code&gt;-level logs during local development without drowning in host noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;host.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"logLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Aggregator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;appsettings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"MyFunctionApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Debug"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Application Insights drops worker logs by default&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you add Application Insights to a local setup and your &lt;code&gt;ILogger&lt;/code&gt; calls stop appearing, this is why: Application Insights registers a default filter rule in the isolated worker model that drops &lt;code&gt;Information&lt;/code&gt; and &lt;code&gt;Debug&lt;/code&gt; logs. To remove it, update &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Application Insights registers a default filter rule that drops everything&lt;/span&gt;
&lt;span class="c1"&gt;// below Warning. Remove it to allow Information-level logs through.&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggerFilterOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;LoggerFilterRule&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderName&lt;/span&gt;
        &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer to avoid touching &lt;code&gt;Program.cs&lt;/code&gt;, you can configure Application Insights log levels through &lt;code&gt;appsettings.json&lt;/code&gt; instead. Add this file to your project root (set "Copy to Output Directory" to "Copy if newer"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using &lt;code&gt;FunctionsApplication.CreateBuilder&lt;/code&gt;, this file is loaded automatically and the filter removal code above is not needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt;, not the instrumentation key&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Microsoft ended instrumentation key ingestion support on March 31, 2025. For any project started after that date, use the connection string setting in &lt;code&gt;local.settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"APPLICATIONINSIGHTS_CONNECTION_STRING"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InstrumentationKey=...;IngestionEndpoint=https://..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not set both. If both are present, newer SDKs ignore the key.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Override log levels without restarting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use double-underscore environment variable overrides in &lt;code&gt;local.settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"AzureFunctionsJobHost__logging__logLevel__default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Debug"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This overrides &lt;code&gt;host.json&lt;/code&gt; log levels without touching the file or restarting &lt;code&gt;func start&lt;/code&gt;. Useful for temporarily enabling verbose output to track down a specific issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;host.json&lt;/code&gt; queue settings for faster debugging&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"queues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"batchSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"visibilityTimeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00:00:05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maxDequeueCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;batchSize: 1&lt;/code&gt;: processes one queue message at a time, making it much easier to follow execution in the logs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;visibilityTimeout: "00:00:05"&lt;/code&gt;: failed messages reappear in 5 seconds instead of the default 10 minutes, so you can iterate on error handling without waiting&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxDequeueCount: 3&lt;/code&gt;: messages move to the poison queue after 3 failures (default is 5)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Terminal aliases&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add to .zshrc / .bashrc&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;fstart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'func start'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;fstartd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'func start --dotnet-isolated-debug'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;fstartv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'func start --verbose'&lt;/span&gt;

&lt;span class="c"&gt;# Start Azurite in background, then start the function host&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;devstart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'azurite --silent --location /tmp/azurite &amp;amp; func start'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;You can now run any trigger type locally, attach a debugger to your actual function code, test non-HTTP functions without waiting for real messages, and reproduce storage-related behaviors against Azurite without touching a live Azure subscription.&lt;/p&gt;

&lt;p&gt;Part 5 goes deeper into the architecture you've been running locally: the isolated worker model. Why Microsoft created a separate worker process, what you gain over the old in-process model, and what the November 2026 end-of-support deadline means for existing projects.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4: Local Development Setup: Tools, Debugging, and Hot Reload (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Beyond HTTP: Timer, Queue, and Blob Triggers</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 20 Feb 2026 06:36:52 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5</link>
      <guid>https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3: Beyond HTTP: Timer, Queue, and Blob Triggers (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;Every .NET team has that one Windows Service nobody wants to touch: the nightly cleanup job, the queue processor, the file watcher duct-taped to a scheduled task. Azure Functions replaces all three with a few lines of C# and a NuGet package.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Part 2&lt;/a&gt;, you built an HTTP-triggered function, the classic request/response pattern. That's the front door. This article opens the rest of the building: the scheduled jobs, the message processors, and the file handlers that run without anyone clicking a button.&lt;/p&gt;

&lt;p&gt;Three trigger types beyond HTTP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timer triggers&lt;/strong&gt;: scheduled jobs on a cron expression. Think nightly cleanups, periodic syncs, report generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue triggers&lt;/strong&gt;: asynchronous message processing. Decouple your services and handle work at your own pace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blob triggers&lt;/strong&gt;: react to files being created or updated in Azure Storage. Image processing, data imports, log analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All examples use .NET 10 with the isolated worker model and C# 14, same as Part 2. Each trigger gets a real-world scenario, working code, and guidance on when to pick it over the alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;If you followed &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Part 2&lt;/a&gt;, you already have .NET 10 SDK, Azure Functions Core Tools v4, and VS Code installed. That setup still applies. Head back to that post if you need to get your environment ready.&lt;/p&gt;

&lt;p&gt;One tool that becomes essential this time: &lt;strong&gt;Azurite&lt;/strong&gt;. For HTTP triggers it was optional, but timer, queue, and blob triggers all rely on Azure Storage for checkpoint and lease management. Without Azurite running locally, none of these triggers will start. Make sure it is installed and running before you continue.&lt;/p&gt;

&lt;p&gt;One thing from Part 2's &lt;code&gt;Program.cs&lt;/code&gt; you won't need here: &lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt;. That call (and the &lt;code&gt;FrameworkReference&lt;/code&gt; to &lt;code&gt;Microsoft.AspNetCore.App&lt;/code&gt;) is specific to HTTP triggers with ASP.NET Core integration. For a project with only timer, queue, and blob triggers, your entry point can be as minimal as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will also need three new NuGet packages, one per trigger type. Each trigger section below shows its specific package, and the Putting It All Together section at the end has the complete &lt;code&gt;.csproj&lt;/code&gt; reference with all packages in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timer Triggers: Scheduled Jobs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What They Are
&lt;/h3&gt;

&lt;p&gt;If you've ever set up a cron job on Linux or a scheduled task on Windows, timer triggers will feel instantly familiar. They're the serverless equivalent: code that runs on a schedule, without you managing the scheduler infrastructure. Think cleanup jobs that purge stale records, nightly report generation, periodic health checks against downstream APIs, cache warming, or syncing data between systems on a regular cadence.&lt;/p&gt;

&lt;h3&gt;
  
  
  CRON Expression Syntax
&lt;/h3&gt;

&lt;p&gt;Azure Functions uses &lt;strong&gt;NCrontab&lt;/strong&gt; format, and here's the first thing that trips people up: it's a &lt;strong&gt;six-field expression&lt;/strong&gt;, not the standard five-field cron you probably know from Linux. The extra field at the beginning represents &lt;strong&gt;seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The format is: &lt;code&gt;{second} {minute} {hour} {day} {month} {day-of-week}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here are the schedules you'll reach for most often:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schedule&lt;/th&gt;
&lt;th&gt;Expression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every 5 minutes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 */5 * * * *&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every hour&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 * * * *&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily at 2 AM UTC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 2 * * *&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekdays at 9 AM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 9 * * 1-5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First of every month&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 0 1 * *&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All times are &lt;strong&gt;UTC by default&lt;/strong&gt;. If you need a different timezone, set the &lt;code&gt;WEBSITE_TIME_ZONE&lt;/code&gt; app setting to a valid timezone identifier (like &lt;code&gt;Eastern Standard Time&lt;/code&gt; on Windows or &lt;code&gt;America/New_York&lt;/code&gt; on Linux). Don't skip this if your business logic is timezone-sensitive: "daily at 2 AM" means very different things in UTC versus your local time.&lt;/p&gt;

&lt;p&gt;And seriously, remember the six fields. If you paste a five-field expression from Stack Overflow, you'll get a cryptic startup error. The leading &lt;code&gt;0&lt;/code&gt; for seconds is easy to forget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Sample
&lt;/h3&gt;

&lt;p&gt;Add the timer extension to your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Timer"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"4.3.1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a timer function that runs a daily cleanup job at 2 AM UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;TriggerDemo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CleanupFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CleanupFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CleanupFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TimerTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0 0 2 * * *"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;TimerInfo&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Running daily cleanup at {Time}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsPastDue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Timer is running late — a scheduled run was missed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Next occurrence: {Next}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScheduleStatus&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Cleanup logic here&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class uses a &lt;strong&gt;primary constructor&lt;/strong&gt; (&lt;code&gt;CleanupFunction(ILogger&amp;lt;CleanupFunction&amp;gt; logger)&lt;/code&gt;) to inject the logger directly. No boilerplate field assignments needed. This is the same dependency injection pattern we used with HTTP triggers.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;[Function(nameof(CleanupFunction))]&lt;/code&gt;&lt;/strong&gt; attribute registers the function with the host. Using &lt;code&gt;nameof()&lt;/code&gt; instead of a hardcoded string keeps things refactor-safe: rename the class and the function name follows automatically.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;[TimerTrigger("0 0 2 * * *")]&lt;/code&gt;&lt;/strong&gt; attribute is where you specify the CRON expression. This one fires once daily at 2 AM UTC.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;TimerInfo&lt;/code&gt;&lt;/strong&gt; parameter is what the runtime hands you. It exposes two properties you'll actually use: &lt;strong&gt;&lt;code&gt;IsPastDue&lt;/code&gt;&lt;/strong&gt; tells you if this execution was delayed (the host was down or restarting when the trigger should have fired), and &lt;strong&gt;&lt;code&gt;ScheduleStatus&lt;/code&gt;&lt;/strong&gt; gives you &lt;strong&gt;&lt;code&gt;Last&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;Next&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;LastUpdated&lt;/code&gt;&lt;/strong&gt; timestamps so you can log or act on schedule metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Externalizing the Schedule
&lt;/h3&gt;

&lt;p&gt;Hardcoding a CRON expression works for demos, but in real projects you'll want different schedules per environment. Your dev cleanup doesn't need to run at the same cadence as production. Use the &lt;strong&gt;&lt;code&gt;%AppSettingName%&lt;/code&gt;&lt;/strong&gt; syntax to pull the schedule from configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TimerTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%CLEANUP_SCHEDULE%"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define &lt;code&gt;CLEANUP_SCHEDULE&lt;/code&gt; in your &lt;code&gt;local.settings.json&lt;/code&gt; for local development and in &lt;strong&gt;App Settings&lt;/strong&gt; for each deployed environment. Now you can run every 30 seconds in dev and every 24 hours in production without changing a line of code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Behaviors and Gotchas
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Singleton execution.&lt;/strong&gt; Only one instance of a timer function runs at a time, even when your function app is scaled out to multiple instances. The runtime uses blob leases to coordinate this. You don't need to build your own distributed locking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UseMonitor.&lt;/strong&gt; Defaults to &lt;code&gt;true&lt;/code&gt; for schedules with intervals of one minute or longer. When enabled, the runtime persists schedule status to blob storage so it can detect missed executions across restarts. That's how &lt;code&gt;IsPastDue&lt;/code&gt; works. For sub-minute schedules, it's disabled automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RunOnStartup.&lt;/strong&gt; Setting this to &lt;code&gt;true&lt;/code&gt; fires the function immediately whenever the host starts. Sounds convenient for testing, but it's a trap in production. Every deployment, every scale-out event, every platform restart triggers your function. Leave it off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No automatic retry by default.&lt;/strong&gt; If your timer function throws an exception, the runtime won't retry it. It simply waits for the next scheduled occurrence. If your cleanup job fails at 2 AM, it won't run again until 2 AM tomorrow. For critical work, add a retry policy to the method: &lt;code&gt;[FixedDelayRetry(5, "00:00:10")]&lt;/code&gt; retries up to five times with a ten-second delay between attempts. One caveat: timer retries don't survive instance failures. If the host crashes mid-retry sequence, the count is lost and won't resume on a new instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Timer Functions Locally
&lt;/h3&gt;

&lt;p&gt;You don't want to wait until 2 AM to test your function. During local development, you can trigger any timer function on demand by POSTing to the admin endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/CleanupFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": null}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns a &lt;strong&gt;202 Accepted&lt;/strong&gt; and fires the function immediately, regardless of the CRON schedule. It's a lifesaver, especially for schedules like "first of every month" where waiting for the natural trigger isn't exactly practical. The same admin endpoint works for any function type, but you'll use it most with timers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queue Triggers: Async Message Processing
&lt;/h2&gt;

&lt;p&gt;Not everything needs to happen right now. When a user places an order, they don't need to wait while you generate an invoice, send a confirmation email, and update your analytics dashboard. Queue triggers let you drop a message onto an Azure Storage Queue and process it asynchronously, decoupling the thing that creates work from the thing that does work.&lt;/p&gt;

&lt;p&gt;This is the backbone of reliable distributed systems. Producers and consumers scale independently, failures don't cascade, and traffic spikes get absorbed by the queue instead of hammering your downstream services.&lt;/p&gt;

&lt;p&gt;Common use cases: order processing, sending emails, image or document processing, fan-out patterns where one event kicks off multiple downstream tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Sample: Order Processor
&lt;/h3&gt;

&lt;p&gt;Add the queue storage extension to your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"5.5.3"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;TriggerDemo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderProcessor&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;OrderMessage&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Processing order {OrderId} for {CustomerId}: {Amount:C}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Process order...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;&lt;code&gt;OrderMessage&lt;/code&gt; record&lt;/strong&gt; defines the shape of your queue message. Records are perfect here: they're immutable, concise, and the positional syntax gives you a clean one-liner. The runtime deserializes the JSON message body straight into this type automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;QueueTrigger("orders")&lt;/code&gt;&lt;/strong&gt; tells the runtime to watch the queue named &lt;code&gt;orders&lt;/code&gt;. Every time a message lands there, your function fires.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;Connection&lt;/code&gt;&lt;/strong&gt; parameter isn't the connection string itself; it's the &lt;em&gt;name&lt;/em&gt; of the setting that holds the connection string. Locally, that's a key in &lt;code&gt;local.settings.json&lt;/code&gt;. In Azure, it's an app setting or a Key Vault reference.&lt;/p&gt;

&lt;p&gt;The runtime handles &lt;strong&gt;auto-deserialization from JSON&lt;/strong&gt; to your POCO or record. But you're not limited to custom types; you can also bind to &lt;code&gt;string&lt;/code&gt; (raw message content), &lt;code&gt;byte[]&lt;/code&gt;, &lt;code&gt;BinaryData&lt;/code&gt;, or &lt;code&gt;QueueMessage&lt;/code&gt; if you need access to metadata like dequeue count or insertion time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queue Output Binding
&lt;/h3&gt;

&lt;p&gt;Often you'll want to chain processing steps: read from one queue, do some work, and drop a message onto another queue. Output bindings handle this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderProcessor&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;OrderMessage&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order {OrderId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"Order &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; processed for &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern here is simple: &lt;strong&gt;the function's return value becomes the output message&lt;/strong&gt;. The &lt;strong&gt;&lt;code&gt;QueueOutput&lt;/code&gt;&lt;/strong&gt; attribute on the method tells the runtime where to send it. Whatever string you return gets written to the &lt;code&gt;notifications&lt;/code&gt; queue. Each function does one job and passes work to the next stage.&lt;/p&gt;

&lt;p&gt;You'll notice this version is synchronous: &lt;code&gt;public string Run(...)&lt;/code&gt; instead of &lt;code&gt;async Task Run(...)&lt;/code&gt;. When the only job is to transform input and return an output message, there's no async work involved. If you need to do async work before emitting the output message, change the return type to &lt;code&gt;Task&amp;lt;string&amp;gt;&lt;/code&gt; and await normally inside.&lt;/p&gt;

&lt;h3&gt;
  
  
  Poison Queue Handling
&lt;/h3&gt;

&lt;p&gt;What happens when processing fails? The runtime retries. And retries again. By default, if a message fails &lt;strong&gt;5 times&lt;/strong&gt; (the &lt;code&gt;maxDequeueCount&lt;/code&gt;), it gets moved to a special queue named &lt;code&gt;{queue-name}-poison&lt;/code&gt; (in our case, &lt;code&gt;orders-poison&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;You can tune this in &lt;code&gt;host.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"queues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maxDequeueCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Poison queues are your safety net, but only if you're watching them. A message sitting in a poison queue means something broke and data isn't being processed. Set up alerts on the poison queue message count. Azure Monitor can do this, or you can write a simple timer trigger (see what we did there?) that checks the count and pings your team in Slack or Teams. Don't let poison messages pile up silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling and Concurrency
&lt;/h3&gt;

&lt;p&gt;The Functions runtime continuously polls the queue and scales out based on queue length. More messages means more instances; you don't configure this, it just happens.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;can&lt;/em&gt; configure in &lt;code&gt;host.json&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;batchSize&lt;/code&gt;&lt;/strong&gt; (default 16): how many messages each instance grabs at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;newBatchThreshold&lt;/code&gt;&lt;/strong&gt; (default &lt;code&gt;batchSize / 2&lt;/code&gt; on Consumption plan, scaled by vCPU count on App Service and Premium plans): when to fetch the next batch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;visibilityTimeout&lt;/code&gt;&lt;/strong&gt;: how long a message stays invisible to other consumers while being processed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One critical thing to understand: &lt;strong&gt;there are no ordering guarantees&lt;/strong&gt;. Messages may be processed out of order, and with at-least-once delivery semantics, the same message could be processed more than once. This means your processing logic must be &lt;strong&gt;idempotent&lt;/strong&gt;, meaning safe to run twice with the same input without creating duplicate side effects. Use an idempotency key (like the &lt;code&gt;OrderId&lt;/code&gt; above) and check whether you've already processed a message before doing the work. Skip this, and you'll eventually process the same order twice on a busy day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Locally
&lt;/h3&gt;

&lt;p&gt;Start Azurite to get a local storage emulator running (you should already have it from the timer trigger section). Your function will connect using the &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; connection string.&lt;/p&gt;

&lt;p&gt;To put test messages on the queue, you've got a few options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Storage Explorer&lt;/strong&gt;: right-click the queue and add a message manually. Great for quick one-offs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure CLI&lt;/strong&gt;: &lt;code&gt;az storage message put --queue-name orders --content '{"OrderId":"123","CustomerId":"C1","Amount":49.99}'&lt;/code&gt; with the &lt;code&gt;--connection-string&lt;/code&gt; flag pointing at Azurite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An HTTP trigger&lt;/strong&gt;: create a simple function that accepts an order via HTTP and writes it to the queue. This is genuinely useful beyond testing; it's often how messages end up on queues in production too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever approach you choose, watch the terminal output from &lt;code&gt;func start&lt;/code&gt;; you'll see your function pick up the message within seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blob Triggers: File Processing
&lt;/h2&gt;

&lt;p&gt;Files show up and need processing. A customer uploads an invoice PDF, a partner drops a data feed into storage, a mobile app pushes photos for moderation. You could poll for new files on a timer, but that's wasteful and slow. Blob triggers let your function react to files being created or updated in Azure Blob Storage. The moment a blob lands, your code runs.&lt;/p&gt;

&lt;p&gt;This is the pattern behind image processing pipelines (generate thumbnails on upload), file format conversion (CSV lands, gets parsed into database rows), log ingestion (application dumps a log file, your function indexes it), data imports, and virus scanning workflows. Anywhere you'd previously have a Windows Service watching a folder or a scheduled job sweeping a directory, blob triggers are the serverless replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blob Trigger vs Input Binding
&lt;/h3&gt;

&lt;p&gt;This distinction confuses people coming from a pure HTTP background, but it matters. Azure provides two blob-related attributes, and they solve different problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Blob Trigger&lt;/th&gt;
&lt;th&gt;Blob Input Binding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Starts function execution&lt;/td&gt;
&lt;td&gt;Reads data during execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When it fires&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;New or modified blob detected&lt;/td&gt;
&lt;td&gt;After another trigger fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attribute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[BlobTrigger]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[BlobInput]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical use case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React to file uploads&lt;/td&gt;
&lt;td&gt;Read a known config or template file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;strong&gt;blob trigger&lt;/strong&gt; is the event source; it &lt;em&gt;causes&lt;/em&gt; your function to run. A &lt;strong&gt;blob input binding&lt;/strong&gt; is a convenience for &lt;em&gt;reading&lt;/em&gt; a blob inside a function that was triggered by something else. For example, you might have a queue trigger that processes orders, and that function needs to read a pricing template stored as a blob. The queue message triggers execution; the blob input binding reads the template. Two different roles, two different attributes.&lt;/p&gt;

&lt;p&gt;Don't use a blob trigger when you already know which blob you need. And don't use an input binding when you need to react to new files arriving. That's the trigger's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Sample: Image Processor
&lt;/h3&gt;

&lt;p&gt;Add the blob storage extension to your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"6.8.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a function that watches an &lt;code&gt;uploads&lt;/code&gt; container and generates a thumbnail every time an image arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;TriggerDemo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImageProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ImageProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ImageProcessor&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;BlobOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"thumbnails/{name}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;BlobTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uploads/{name}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing uploaded image: {Name} ({Size} bytes)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Generate thumbnail...&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;thumbnailBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateThumbnail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;thumbnailBytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;GenerateThumbnail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Thumbnail generation logic&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// placeholder&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;&lt;code&gt;[BlobTrigger("uploads/{name}")]&lt;/code&gt;&lt;/strong&gt; attribute is doing two things at once. First, it tells the runtime to watch the &lt;code&gt;uploads&lt;/code&gt; container for new or modified blobs. Second, the &lt;strong&gt;&lt;code&gt;{name}&lt;/code&gt; path pattern&lt;/strong&gt; is a binding expression; it captures the blob's filename and makes it available as a method parameter. When someone uploads &lt;code&gt;uploads/profile-photo.jpg&lt;/code&gt;, the &lt;code&gt;name&lt;/code&gt; parameter receives &lt;code&gt;"profile-photo.jpg"&lt;/code&gt;. This is how you know &lt;em&gt;which&lt;/em&gt; file triggered your function.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;Connection&lt;/code&gt;&lt;/strong&gt; parameter works the same way it does with queue triggers; it's the name of the app setting that holds your storage connection string, not the connection string itself. &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; points to Azurite during local development.&lt;/p&gt;

&lt;p&gt;The method parameter &lt;strong&gt;&lt;code&gt;byte[] imageData&lt;/code&gt;&lt;/strong&gt; receives the entire blob content as a byte array. This is the simplest binding type and works well when your files fit comfortably in memory. But you're not limited to &lt;code&gt;byte[]&lt;/code&gt;; the runtime supports several binding types depending on your needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Stream&lt;/code&gt;&lt;/strong&gt;: best for large files where you want to process data without loading everything into memory at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BinaryData&lt;/code&gt;&lt;/strong&gt;: a modern Azure SDK type that wraps binary content with convenient conversion methods&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;string&lt;/code&gt;&lt;/strong&gt;: for text-based blobs like CSV, JSON, or log files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;byte[]&lt;/code&gt;&lt;/strong&gt;: when you need the raw bytes (image processing, binary formats)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BlobClient&lt;/code&gt;&lt;/strong&gt;: gives you the full Azure Storage SDK client, useful when you need blob metadata, properties, or want to copy/move the blob rather than just read it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;[BlobOutput("thumbnails/{name}")]&lt;/code&gt;&lt;/strong&gt; attribute on the method tells the runtime to write the return value to a different container. Notice the &lt;code&gt;{name}&lt;/code&gt; pattern appears here too, reusing the same captured filename, so &lt;code&gt;uploads/profile-photo.jpg&lt;/code&gt; produces &lt;code&gt;thumbnails/profile-photo.jpg&lt;/code&gt;. The function's return type matches the output: return a &lt;code&gt;byte[]&lt;/code&gt;, and that's what gets written. This input-container-to-output-container pattern is the standard approach for blob processing, and the separation is deliberate (more on why in the gotchas section).&lt;/p&gt;

&lt;h3&gt;
  
  
  Path Patterns and Filtering
&lt;/h3&gt;

&lt;p&gt;The path pattern in &lt;code&gt;[BlobTrigger]&lt;/code&gt; is more powerful than a simple container name. You can use it to filter which blobs trigger your function and to extract structured metadata from your blob paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filter by file extension&lt;/strong&gt; to react only to specific file types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;BlobTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uploads/{name}.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function only fires for CSV files. Upload a &lt;code&gt;.csv&lt;/code&gt; and the function runs; upload a &lt;code&gt;.png&lt;/code&gt; and nothing happens. The &lt;code&gt;name&lt;/code&gt; parameter receives just the filename without the extension. Upload &lt;code&gt;report-2026.csv&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; is &lt;code&gt;"report-2026"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extract path segments&lt;/strong&gt; when your storage uses a meaningful directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;BlobTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uploads/{category}/{filename}.{ext}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each segment of the path binds to its own parameter. A blob at &lt;code&gt;uploads/invoices/INV-2026-001.pdf&lt;/code&gt; gives you &lt;code&gt;category = "invoices"&lt;/code&gt;, &lt;code&gt;filename = "INV-2026-001"&lt;/code&gt;, and &lt;code&gt;ext = "pdf"&lt;/code&gt;. You can route processing logic based on the category, log the extension, or construct output paths dynamically.&lt;/p&gt;

&lt;p&gt;Design your container directory structure with these patterns in mind. A flat dump of files into a single container works, but a structured layout like &lt;code&gt;uploads/{department}/{year}/{filename}&lt;/code&gt; gives you filtering and metadata for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Gotchas
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Infinite loops will ruin your day.&lt;/strong&gt; If your function triggers on blobs in the &lt;code&gt;uploads&lt;/code&gt; container and writes output back to the same &lt;code&gt;uploads&lt;/code&gt; container, the output blob triggers the function again. Which writes another blob. Which triggers again. This loop runs until you notice it, your storage bill spikes, or you hit a concurrency limit. &lt;strong&gt;Always use separate containers for input and output.&lt;/strong&gt; The code sample above reads from &lt;code&gt;uploads&lt;/code&gt; and writes to &lt;code&gt;thumbnails&lt;/code&gt;. That separation is intentional and non-negotiable. If you remember one thing from this section, make it this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency is worse than you'd expect.&lt;/strong&gt; The default blob trigger uses a polling mechanism that scans for changes. On the Consumption plan, this can take &lt;strong&gt;up to 10 minutes&lt;/strong&gt; to detect a new blob. That's not a typo. Minutes, not seconds. If near-instant detection matters for your scenario (and it usually does), use the &lt;strong&gt;Event Grid-based blob trigger&lt;/strong&gt; instead. It subscribes to Azure Event Grid notifications and fires within seconds of blob creation. For anything going to production, Event Grid is the recommended approach. The polling-based trigger is fine for development and low-urgency workloads, but don't ship it to production assuming instant response times. Locally with Azurite, detection typically takes under 60 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blob receipts track what's been processed.&lt;/strong&gt; The runtime stores receipts in the &lt;code&gt;azure-webjobs-hosts&lt;/code&gt; container to avoid reprocessing the same blob. If processing fails 5 times, the blob's receipt is marked as poisoned and the runtime stops retrying. Unlike queue triggers, there's no separate "poison container"; the receipt just gets flagged. You'll need to monitor your logs to catch these failures, because nothing moves visibly. If you need to reprocess a failed blob, delete its receipt from &lt;code&gt;azure-webjobs-hosts&lt;/code&gt; and the runtime will pick it up again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold starts compound the latency problem.&lt;/strong&gt; On the Consumption plan, if your function app has been idle, it may need to cold-start before it can even begin polling for blobs. Combined with the polling interval, you could be looking at significant delays. Two mitigations: use a timer trigger as a keep-alive (a function that runs every few minutes just to keep the host warm), or switch to an App Service Plan with &lt;strong&gt;Always On&lt;/strong&gt; enabled. The Flex Consumption plan also helps here with its pre-warmed instance pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Trigger
&lt;/h2&gt;

&lt;p&gt;You've now seen three trigger types in action: timer, queue, and blob. Each one solves a fundamentally different problem, and picking the right one usually comes down to a single question: &lt;strong&gt;what event starts the work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "a clock" (something needs to happen at a specific time or on a recurring interval), that's a timer trigger. If the answer is "a message" (another service is telling you there's work to do), that's a queue trigger. If the answer is "a file showed up" (someone uploaded something, or an external system dropped a CSV into storage), that's a blob trigger.&lt;/p&gt;

&lt;p&gt;In practice, the choice is usually obvious once you frame it that way. But here's a quick reference for common scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run at 2 AM daily&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Timer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schedule-based, no external event needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process uploaded files&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Blob&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reacts to blob storage events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background task after user action&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Decouple request from processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process orders asynchronously&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reliable message processing with retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health check every 5 minutes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Timer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Periodic polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETL pipeline from file drops&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Blob&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File arrival triggers processing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A([What starts the work?]) --&amp;gt; B{Clock or schedule?}
    B --&amp;gt;|Yes| Timer[⏰ Timer Trigger]
    B --&amp;gt;|No| C{Message from another service?}
    C --&amp;gt;|Yes| Q{Need ordering or pub/sub?}
    Q --&amp;gt;|No| Queue[📨 Queue Trigger\nStorage Queue]
    Q --&amp;gt;|Yes| SB[🚌 Service Bus Trigger]
    C --&amp;gt;|No| D{File arrived in storage?}
    D --&amp;gt;|Yes| Blob[📁 Blob Trigger]
    D --&amp;gt;|No| HTTP([HTTP Trigger\nor other event])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Timer for time, queue for commands, blob for files.&lt;/strong&gt; If you're unsure, ask yourself whether the trigger is driven by the clock, by another service's intent, or by data arriving in storage. The answer almost always maps cleanly to one of the three.&lt;/p&gt;

&lt;p&gt;And in many real-world systems, you'll combine them. A timer trigger runs nightly to check for missing invoices, drops a message per missing invoice onto a queue, and the queue trigger processes each one. Or a blob trigger picks up an uploaded CSV, validates it, and enqueues individual rows for downstream processing. These triggers compose naturally. That's by design.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Consider Service Bus Over Storage Queues
&lt;/h3&gt;

&lt;p&gt;Everything in this article uses &lt;strong&gt;Azure Storage Queues&lt;/strong&gt;; they're simple, cheap, and included with any storage account you're already using. For most use cases, they're the right starting point.&lt;/p&gt;

&lt;p&gt;But Storage Queues have limits, and you'll hit them eventually. Here's when to look at &lt;strong&gt;Azure Service Bus&lt;/strong&gt; instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need guaranteed ordering.&lt;/strong&gt; Storage Queues offer no ordering guarantees at all. Service Bus &lt;strong&gt;sessions&lt;/strong&gt; give you FIFO processing for messages that share a session ID, which is essential for scenarios where event order matters, like processing state changes for the same entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need pub/sub.&lt;/strong&gt; Storage Queues are point-to-point: one message, one consumer. If the same event needs to reach multiple subscribers (say, an order placed event triggers inventory, billing, and notifications), Service Bus &lt;strong&gt;topics and subscriptions&lt;/strong&gt; let multiple consumers each get their own copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your messages exceed 64 KB.&lt;/strong&gt; That's the Storage Queue message size limit. Service Bus supports messages up to 256 KB on the Standard tier and 100 MB on Premium. If you're passing around documents or large payloads, Storage Queues won't cut it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need dead-letter queues with inspection.&lt;/strong&gt; Storage Queues have the poison queue mechanism we covered earlier, but Service Bus dead-letter queues are richer; you can peek messages, inspect failure reasons, and resubmit without custom tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need duplicate detection or transactions.&lt;/strong&gt; Service Bus can automatically detect and discard duplicate messages within a configurable window, and it supports transactions that span multiple queues or topics. Storage Queues have neither.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;start with Storage Queues. Upgrade to Service Bus when you hit a specific limitation.&lt;/strong&gt; Don't pay the complexity and cost premium upfront "just in case." Storage Queues handle the vast majority of message processing scenarios, and migrating later is not painful. Your function code barely changes. You swap &lt;code&gt;QueueTrigger&lt;/code&gt; for &lt;code&gt;ServiceBusTrigger&lt;/code&gt;, update the connection string, and adjust the NuGet package. The processing logic inside stays the same.&lt;/p&gt;

&lt;p&gt;If you're starting a new project and genuinely aren't sure, go with Storage Queues. You'll know when you've outgrown them. It'll be the moment you need ordering, pub/sub, or messages bigger than 64 KB. Until then, keep it simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing All Triggers Locally with Azurite
&lt;/h2&gt;

&lt;p&gt;You've seen the individual trigger sections mention Azurite in passing. Let's talk about why it's required and how to set up a smooth local development workflow for all three trigger types at once.&lt;/p&gt;

&lt;p&gt;Timer, queue, and blob triggers all depend on &lt;strong&gt;Azure Storage&lt;/strong&gt; behind the scenes, even if your function doesn't explicitly use queues or blobs. Timer triggers store checkpoint data in blob storage so the runtime knows when the last execution happened (that's how &lt;code&gt;IsPastDue&lt;/code&gt; works). Queue triggers obviously need a queue service. Blob triggers use blob storage for both the trigger source and internal bookkeeping through blob receipts. Without a storage account, none of these triggers can even start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azurite&lt;/strong&gt; is Microsoft's official local storage emulator. It gives you blob, queue, and table services running on your machine. No Azure subscription needed, no network latency, no cost. You connect to it with a well-known connection string, and your functions behave exactly as they would against real Azure Storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;local.settings.json&lt;/code&gt; needs exactly two settings to make everything work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt;&lt;/strong&gt; is a shorthand connection string that the Azure SDK recognizes. It tells the runtime to connect to Azurite's default endpoints on &lt;code&gt;localhost&lt;/code&gt;: port 10000 for blobs, 10001 for queues, and 10002 for tables. No keys, no account names, no URLs to configure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting Azurite
&lt;/h3&gt;

&lt;p&gt;Open a terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx azurite &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; /tmp/azurite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--silent&lt;/code&gt; flag suppresses the per-request logging that clutters your terminal (you've got enough output from &lt;code&gt;func start&lt;/code&gt;). The &lt;code&gt;--location&lt;/code&gt; flag tells Azurite where to persist data on disk. Use &lt;code&gt;/tmp/azurite&lt;/code&gt; for throwaway test data, or a project-local folder if you want data to survive reboots. Azurite will create the folder if it doesn't exist.&lt;/p&gt;

&lt;p&gt;Leave this running in its own terminal window. Then, in a second terminal, start your function app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host will connect to Azurite, create its internal containers (&lt;code&gt;azure-webjobs-hosts&lt;/code&gt; for checkpoints, etc.), and register all three trigger types. If Azurite isn't running, you'll see connection-refused errors and the host will fail to start, so always start Azurite first.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Practical Testing Workflow
&lt;/h3&gt;

&lt;p&gt;Once both Azurite and the function host are running, here's how to exercise all three triggers in a single session:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timer triggers&lt;/strong&gt; are the easiest. POST to the admin endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/CleanupFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": null}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get a &lt;strong&gt;202 Accepted&lt;/strong&gt; and see the function execute immediately in the terminal output. No waiting for the CRON schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue triggers&lt;/strong&gt; need a message on the queue. Use Azure Storage Explorer, the Azure CLI, or a quick &lt;code&gt;curl&lt;/code&gt; to push a JSON message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az storage message put &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-name&lt;/span&gt; orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--content&lt;/span&gt; &lt;span class="s1"&gt;'{"OrderId":"ORD-001","CustomerId":"C42","Amount":129.99}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connection-string&lt;/span&gt; &lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;OrderProcessor&lt;/code&gt; function will pick it up within seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blob triggers&lt;/strong&gt; fire when you upload a file to the watched container. Drop a test file in using the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az storage blob upload &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--container-name&lt;/span&gt; uploads &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; test-image.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; ./test-image.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connection-string&lt;/span&gt; &lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detection can take up to 60 seconds locally given the polling-based mechanism. If your function doesn't fire immediately, give it a minute before assuming something is wrong. For production, you'd use Event Grid for near-instant detection, but locally the default polling works fine.&lt;/p&gt;

&lt;p&gt;The admin endpoint (&lt;code&gt;POST http://localhost:7071/admin/functions/{FunctionName}&lt;/code&gt; with &lt;code&gt;{"input": null}&lt;/code&gt;) works for all function types, not just timers. It's your universal "just run this thing right now" button during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;If you've been following along, you already have the individual NuGet packages installed. Here's the complete &lt;code&gt;.csproj&lt;/code&gt; &lt;code&gt;&amp;lt;ItemGroup&amp;gt;&lt;/code&gt; with everything you need for HTTP, timer, queue, and blob triggers in a single project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.51.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Sdk"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.7"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Timer"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"4.3.1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"5.5.3"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"6.8.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first three packages are the foundation: the worker runtime, the SDK tooling, and the HTTP extension you set up in Part 2. The last three are one extension per trigger type. That's the pattern: &lt;strong&gt;each trigger type ships as its own NuGet package&lt;/strong&gt;. You only install what you use, and you can add triggers incrementally as your project grows. Starting with just timers? Leave out the queue and blob packages. Adding blob processing next sprint? Drop in the one-liner and you're ready.&lt;/p&gt;

&lt;p&gt;The extensions share the &lt;code&gt;Microsoft.Azure.Functions.Worker.Extensions.*&lt;/code&gt; naming convention, which makes them easy to discover. If you find yourself searching for a new trigger type down the road (Service Bus, Cosmos DB, Event Grid), it'll follow the same &lt;code&gt;Extensions.{ServiceName}&lt;/code&gt; pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Three more trigger types are now in your toolkit. Timer triggers use six-field CRON expressions and run as singletons even when scaled out; if the host was down when a run was due, &lt;code&gt;IsPastDue&lt;/code&gt; tells you so. Queue triggers give you at-least-once delivery with automatic retries and a poison queue for messages that keep failing. Blob triggers fire on file arrival, not a polling loop; separate input and output containers are non-negotiable.&lt;/p&gt;

&lt;p&gt;A few principles that cut across all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep functions focused.&lt;/strong&gt; One function, one job. Chain them with output bindings rather than cramming everything into a single method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make processing idempotent.&lt;/strong&gt; Messages can be delivered more than once, timer functions can fire late, blob triggers can re-detect files. Design for "safe to run twice" from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use separate containers for blob I/O.&lt;/strong&gt; Writing output to the same container you trigger on creates an infinite loop. Always read from one container and write to another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test locally with Azurite.&lt;/strong&gt; Every trigger type in this article works without an Azure subscription. There's no excuse for deploying untested functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;[LoggerMessage]&lt;/code&gt; for production logging.&lt;/strong&gt; The &lt;code&gt;logger.LogInformation(...)&lt;/code&gt; calls in this article are intentionally straightforward, but they box value types (&lt;code&gt;decimal&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;DateTime&lt;/code&gt;) on every invocation, even when that log level is disabled. Projects with recommended analyzer settings will flag this as CA1873. The fix is the &lt;code&gt;[LoggerMessage]&lt;/code&gt; source generation attribute, which creates strongly-typed log methods at compile time and eliminates the boxing:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Processing order {OrderId}: {Amount:C}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LogOrderProcessing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a performance and code quality concern, not a correctness issue; the inline calls in this article work fine for getting started.&lt;/p&gt;

&lt;p&gt;All the code samples from this article are available in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;sample repository on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next in the series:&lt;/strong&gt; Part 4 (Local Development Setup: Tools, Debugging, and Hot Reload) will take your local workflow to the next level. We'll cover debugging with breakpoints, hot reload for faster iteration, and the tooling that makes Azure Functions development feel as smooth as working on a regular ASP.NET Core app.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3: Beyond HTTP: Timer, Queue, and Blob Triggers (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Your First Azure Function: HTTP Triggers Step-by-Step</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 13 Feb 2026 07:23:29 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8</link>
      <guid>https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 2: Your First Azure Function: HTTP Triggers Step-by-Step (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;Theory doesn't ship features. You learned the why in &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Part 1&lt;/a&gt;—when serverless makes sense, the economics, how it compares to App Service and Container Apps. Now let's build something.&lt;/p&gt;

&lt;p&gt;In this second part of the &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Azure Functions for .NET Developers&lt;/a&gt; series, we'll create a project from scratch, understand every line of generated code, and build three HTTP trigger patterns that cover most real-world API scenarios. By the end, you'll have a working HTTP API running locally on your machine. No Azure subscription required.&lt;/p&gt;

&lt;p&gt;All code from this article is available in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need four tools installed before we start. Here's what to check and where to get them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Verify&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.NET 10 SDK&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dotnet --version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Functions Core Tools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func --version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code + Azure Functions extension&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Latest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azurite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx azurite --version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;.NET 10 SDK&lt;/strong&gt; is required to build and run our function app. It's an LTS release, supported through November 2028. Grab it from &lt;a href="https://dot.net/download" rel="noopener noreferrer"&gt;dot.net/download&lt;/a&gt; if you don't have it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure Functions Core Tools v4&lt;/strong&gt; lets you create, run, and debug functions locally. Install via npm, Homebrew, or Chocolatey:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm (any OS)&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; azure-functions-core-tools@4

&lt;span class="c"&gt;# Homebrew (macOS)&lt;/span&gt;
brew tap azure/functions
brew &lt;span class="nb"&gt;install &lt;/span&gt;azure-functions-core-tools@4

&lt;span class="c"&gt;# Chocolatey (Windows)&lt;/span&gt;
choco &lt;span class="nb"&gt;install &lt;/span&gt;azure-functions-core-tools-4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;VS Code with the Azure Functions extension&lt;/strong&gt; gives you project templates, debugging, and deployment in one place. Visual Studio and Rider work too—use whatever you're comfortable with. The CLI commands in this article work regardless of editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azurite&lt;/strong&gt; emulates Azure Storage locally. The Functions runtime needs a storage connection even for HTTP triggers (it uses storage internally for features like function key management and, in production, for coordinating across instances). Install it globally or run it on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install globally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; azurite

&lt;span class="c"&gt;# Or run on demand without installing&lt;/span&gt;
npx azurite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the verification commands all return version numbers and you have VS Code with the Azure Functions extension installed, you're ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Your First Project
&lt;/h2&gt;

&lt;p&gt;With your tools in place, open a terminal and scaffold a new function app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func init HttpTriggerDemo &lt;span class="nt"&gt;--worker-runtime&lt;/span&gt; dotnet-isolated &lt;span class="nt"&gt;--target-framework&lt;/span&gt; net10.0
&lt;span class="nb"&gt;cd &lt;/span&gt;HttpTriggerDemo
func new &lt;span class="nt"&gt;--template&lt;/span&gt; &lt;span class="s2"&gt;"HTTP trigger"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; Hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command creates the project. &lt;code&gt;--worker-runtime dotnet-isolated&lt;/code&gt; selects the isolated worker model (the modern default—we'll explain what "isolated" means in Part 5). &lt;code&gt;--target-framework net10.0&lt;/code&gt; targets .NET 10.&lt;/p&gt;

&lt;p&gt;Once inside the project directory, &lt;code&gt;func new&lt;/code&gt; adds an HTTP-triggered function called &lt;code&gt;Hello&lt;/code&gt; from a built-in template.&lt;/p&gt;

&lt;p&gt;Your project now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HttpTriggerDemo/
├── Hello.cs              # Your function code
├── Program.cs            # Application entry point
├── HttpTriggerDemo.csproj # Project file
├── host.json             # Runtime configuration
└── local.settings.json   # Local environment variables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Project File
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;HttpTriggerDemo.csproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net10.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AzureFunctionsVersion&amp;gt;&lt;/span&gt;v4&lt;span class="nt"&gt;&amp;lt;/AzureFunctionsVersion&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Exe&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FrameworkReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.App"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.51.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Sdk"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.7"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OutputType: Exe&lt;/code&gt;&lt;/strong&gt; — the isolated worker model runs as a standalone executable, not a class library loaded into the Functions host. This is what gives you full control over the process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FrameworkReference: Microsoft.AspNetCore.App&lt;/code&gt;&lt;/strong&gt; — brings in ASP.NET Core types like &lt;code&gt;HttpRequest&lt;/code&gt; and &lt;code&gt;IActionResult&lt;/code&gt;. Without this, you'd use the lower-level &lt;code&gt;HttpRequestData&lt;/code&gt; API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Extensions.Http.AspNetCore&lt;/code&gt;&lt;/strong&gt; — the bridge that maps ASP.NET Core's HTTP abstractions to the Functions runtime. This is what makes the developer experience feel like writing a regular web API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ImplicitUsings: enable&lt;/code&gt;&lt;/strong&gt; — the compiler automatically includes common &lt;code&gt;using&lt;/code&gt; statements (&lt;code&gt;System&lt;/code&gt;, &lt;code&gt;System.Collections.Generic&lt;/code&gt;, &lt;code&gt;System.Linq&lt;/code&gt;, &lt;code&gt;System.Threading.Tasks&lt;/code&gt;, and others). That's why our code files don't start with a block of &lt;code&gt;using&lt;/code&gt; directives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Nullable: enable&lt;/code&gt;&lt;/strong&gt; — turns on nullable reference types. The compiler warns you when code might dereference &lt;code&gt;null&lt;/code&gt; without checking. You'll see this in action when we read query string values—&lt;code&gt;req.Query["name"]&lt;/code&gt; returns &lt;code&gt;string?&lt;/code&gt;, not &lt;code&gt;string&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Entry Point
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Program.cs&lt;/code&gt; is minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsWebApplication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; is the key line. It enables ASP.NET Core integration—without it, you'd need to use &lt;code&gt;HttpRequestData&lt;/code&gt; and &lt;code&gt;HttpResponseData&lt;/code&gt; instead of the familiar &lt;code&gt;HttpRequest&lt;/code&gt; and &lt;code&gt;IActionResult&lt;/code&gt;. For HTTP-triggered functions, this is almost always what you want.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FunctionsApplication.CreateBuilder(args)&lt;/code&gt; is a convenience factory introduced in Worker SDK 2.0. It returns a pre-configured &lt;code&gt;IHostApplicationBuilder&lt;/code&gt; with Functions defaults already wired up—logging, JSON serializer options, and middleware. The key &lt;code&gt;using&lt;/code&gt; to remember is &lt;code&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/code&gt;, which brings the factory method into scope. You may also see an older pattern that starts with &lt;code&gt;new HostBuilder()&lt;/code&gt; and calls &lt;code&gt;.ConfigureFunctionsWorkerDefaults()&lt;/code&gt;—it still works, but requires more manual setup (for example, explicit &lt;code&gt;ConfigureAppConfiguration()&lt;/code&gt; calls to load &lt;code&gt;appsettings.json&lt;/code&gt;). &lt;code&gt;func init&lt;/code&gt; scaffolds &lt;code&gt;FunctionsApplication.CreateBuilder&lt;/code&gt; by default, so stick with it for new projects.&lt;/p&gt;

&lt;p&gt;This is also where you'd register services for dependency injection, add middleware, or configure logging. We'll keep it simple for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Generated Code
&lt;/h2&gt;

&lt;p&gt;With the project structure in place, let's look at the function itself. The &lt;code&gt;func new&lt;/code&gt; command created a file called &lt;code&gt;Hello.cs&lt;/code&gt; with a complete working function. Before we run it, let's understand what each part does. Open &lt;code&gt;Hello.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"C# HTTP trigger function processed a request."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome to Azure Functions!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break this apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;public class Hello(ILogger&amp;lt;Hello&amp;gt; logger)&lt;/code&gt;&lt;/strong&gt; uses a C# 12 primary constructor. The &lt;code&gt;ILogger&amp;lt;Hello&amp;gt;&lt;/code&gt; parameter is injected by the dependency injection container automatically—no need to write a separate constructor or field assignment. The logger is available throughout the class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;[Function("Hello")]&lt;/code&gt;&lt;/strong&gt; registers this method with the Functions runtime. The string &lt;code&gt;"Hello"&lt;/code&gt; becomes the function's name and appears in the URL path: &lt;code&gt;http://localhost:7071/api/Hello&lt;/code&gt;. If you rename the class but keep the attribute string the same, the URL stays the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;[HttpTrigger(AuthorizationLevel.Function, "get", "post")]&lt;/code&gt;&lt;/strong&gt; is where the behavior is defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AuthorizationLevel.Function&lt;/code&gt; means callers need a function-specific API key to invoke this endpoint. Other options are &lt;code&gt;Anonymous&lt;/code&gt; (no key required) and &lt;code&gt;Admin&lt;/code&gt; (requires the master host key). When running locally, authorization is bypassed regardless of the level you set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"get", "post"&lt;/code&gt; lists the allowed HTTP methods. Requests using other methods receive a 405 Method Not Allowed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;HttpRequest req&lt;/code&gt;&lt;/strong&gt; is the full ASP.NET Core request object. Query strings, headers, body, route values—it's all there. If you've written ASP.NET Core controllers or minimal APIs, this is the same type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;IActionResult&lt;/code&gt;&lt;/strong&gt; is the return type. &lt;code&gt;OkObjectResult&lt;/code&gt; maps to a 200 response. The runtime serializes the object to JSON (or returns it as plain text for strings) and sends it back to the caller. All the familiar result types work: &lt;code&gt;BadRequestResult&lt;/code&gt;, &lt;code&gt;NotFoundResult&lt;/code&gt;, &lt;code&gt;CreatedResult&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run It
&lt;/h3&gt;

&lt;p&gt;Before we modify anything, let's see this function in action. Start Azurite in a separate terminal (or in the background):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx azurite &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; /tmp/azurite &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start the Functions runtime from your project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output listing your function's URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Functions:

        Hello: [GET,POST] http://localhost:7071/api/Hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it in another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:7071/api/Hello
&lt;span class="c"&gt;# → Welcome to Azure Functions!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a working HTTP endpoint, running locally, no Azure subscription needed. Keep &lt;code&gt;func start&lt;/code&gt; running—we'll build on this function next.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP Trigger Deep Dive
&lt;/h2&gt;

&lt;p&gt;With a running function under your belt, let's build three patterns that cover the majority of real-world HTTP function scenarios. After each change, stop &lt;code&gt;func start&lt;/code&gt; with &lt;strong&gt;Ctrl+C&lt;/strong&gt; and restart it to pick up the new code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: GET with Query String
&lt;/h3&gt;

&lt;p&gt;The simplest pattern—read a value from the URL and return a response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HelloFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello function triggered"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Hello, &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;req.Query["name"]&lt;/code&gt; reads the &lt;code&gt;name&lt;/code&gt; parameter from the query string. If it's missing, the value is &lt;code&gt;null&lt;/code&gt;, so we fall back to &lt;code&gt;"world"&lt;/code&gt; with the null-coalescing operator. Note the &lt;code&gt;string?&lt;/code&gt; — the query collection returns a nullable type, and with &lt;code&gt;&amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;&lt;/code&gt; in the project file, the compiler enforces this.&lt;/p&gt;

&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:7071/api/Hello?name=Azure"&lt;/span&gt;
&lt;span class="c"&gt;# → Hello, Azure!&lt;/span&gt;

curl &lt;span class="s2"&gt;"http://localhost:7071/api/Hello"&lt;/span&gt;
&lt;span class="c"&gt;# → Hello, world!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern works well for simple lookups—search queries, filtering lists, or any GET request where the parameters are short and cacheable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: POST with JSON Body
&lt;/h3&gt;

&lt;p&gt;For creating or updating resources, you'll typically accept a JSON request body. C# records and ASP.NET Core model binding make this clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;FromBodyAttribute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromBodyAttribute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CreateOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order for {ProductId} x{Quantity}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreatedResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several things happening here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;record CreateOrderRequest&lt;/code&gt;&lt;/strong&gt; defines an immutable data type. Records are ideal for request and response DTOs—they give you value equality, a clean &lt;code&gt;ToString()&lt;/code&gt;, and immutability out of the box. No need to write a class with properties and a constructor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;[FromBody] CreateOrderRequest order&lt;/code&gt;&lt;/strong&gt; tells the ASP.NET Core model binder to deserialize the JSON request body into a &lt;code&gt;CreateOrderRequest&lt;/code&gt; instance. If the JSON doesn't match (missing required fields, wrong types), the runtime returns a 400 Bad Request automatically. One gotcha: with ASP.NET Core integration enabled, &lt;code&gt;FromBody&lt;/code&gt; exists in two namespaces—&lt;code&gt;Microsoft.Azure.Functions.Worker.Http&lt;/code&gt; and &lt;code&gt;Microsoft.AspNetCore.Mvc&lt;/code&gt;. You need the Functions Worker version. The &lt;code&gt;using&lt;/code&gt; alias at the top of the file resolves the ambiguity so you can use &lt;code&gt;[FromBody]&lt;/code&gt; without a fully qualified name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Route = "orders"&lt;/code&gt;&lt;/strong&gt; overrides the default route. Without it, the URL would be &lt;code&gt;/api/CreateOrder&lt;/code&gt; (derived from the function name). With it, the URL becomes &lt;code&gt;/api/orders&lt;/code&gt;—cleaner and more RESTful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CreatedResult&lt;/code&gt;&lt;/strong&gt; returns HTTP 201 with a &lt;code&gt;Location&lt;/code&gt; header pointing to the new resource. Standard REST semantics.&lt;/p&gt;

&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/api/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"productId":"SKU-001","quantity":3}'&lt;/span&gt;
&lt;span class="c"&gt;# → {"productId":"SKU-001","quantity":3}&lt;/span&gt;
&lt;span class="c"&gt;# (with 201 status and Location header)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Content-Type: application/json&lt;/code&gt; header is required. Without it, model binding won't kick in and you'll get a deserialization error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Route Parameters
&lt;/h3&gt;

&lt;p&gt;For resource-oriented APIs, you'll want parameters embedded in the URL path rather than the query string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GetProduct"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"products/{category:alpha}/{id:int?}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Looking up {Category}, id={Id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Route parameters&lt;/strong&gt; are defined in curly braces inside the &lt;code&gt;Route&lt;/code&gt; string and captured as method parameters matched by name. The runtime extracts &lt;code&gt;category&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; from the URL and passes them directly to your method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route constraints&lt;/strong&gt; validate parameters before your code runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:alpha&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Letters only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;electronics&lt;/code&gt; matches, &lt;code&gt;123&lt;/code&gt; doesn't&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;42&lt;/code&gt; matches, &lt;code&gt;abc&lt;/code&gt; doesn't&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:int?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional integer&lt;/td&gt;
&lt;td&gt;Parameter can be omitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:guid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GUID format&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{id:guid}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:length(5)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exact string length&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{code:length(5)}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:min(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum integer value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{page:min(1)}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a constraint fails, the runtime returns a 404—your function never executes.&lt;/p&gt;

&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:7071/api/products/electronics/42
&lt;span class="c"&gt;# → {"category":"electronics","id":42}&lt;/span&gt;

curl http://localhost:7071/api/products/electronics
&lt;span class="c"&gt;# → {"category":"electronics","id":null}&lt;/span&gt;

curl http://localhost:7071/api/products/123/42
&lt;span class="c"&gt;# → 404 (constraint :alpha rejects "123")&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have three working patterns that cover most real-world HTTP function scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running and Testing Locally
&lt;/h2&gt;

&lt;p&gt;Restart &lt;code&gt;func start&lt;/code&gt; now that all three functions are in place. You should see all of them listed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Azure Functions Core Tools
Core Tools Version: 4.6.0

Functions:

        CreateOrder: [POST] http://localhost:7071/api/orders

        GetProduct: [GET] http://localhost:7071/api/products/{category:alpha}/{id:int?}

        Hello: [GET] http://localhost:7071/api/Hello
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For quick reference, here are all three test commands together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pattern 1: GET with query string&lt;/span&gt;
curl &lt;span class="s2"&gt;"http://localhost:7071/api/Hello?name=Azure"&lt;/span&gt;

&lt;span class="c"&gt;# Pattern 2: POST with JSON body&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/api/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"productId":"SKU-001","quantity":3}'&lt;/span&gt;

&lt;span class="c"&gt;# Pattern 3: Route parameters&lt;/span&gt;
curl http://localhost:7071/api/products/electronics/42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use Postman, the REST Client extension for VS Code, or HTTPie—whatever fits your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on authorization:&lt;/strong&gt; when running locally, &lt;code&gt;AuthorizationLevel.Function&lt;/code&gt; is bypassed. You don't need to pass a function key. This makes local development frictionless. Once deployed, clients will need to include the key as a query parameter (&lt;code&gt;?code=&amp;lt;key&amp;gt;&lt;/code&gt;) or in the &lt;code&gt;x-functions-key&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;If things don't work on the first try, check these common issues:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Can't determine project language"&lt;/strong&gt; or &lt;strong&gt;"No job functions found"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.csproj&lt;/code&gt; file is missing required settings. Verify it targets &lt;code&gt;net10.0&lt;/code&gt;, has &lt;code&gt;&amp;lt;AzureFunctionsVersion&amp;gt;v4&amp;lt;/AzureFunctionsVersion&amp;gt;&lt;/code&gt;, and includes the Worker SDK packages. Run &lt;code&gt;dotnet build&lt;/code&gt; separately to check for compilation errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port 7071 already in use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another Functions host (or another process) is using the default port. Either kill it or start on a different port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func start &lt;span class="nt"&gt;--port&lt;/span&gt; 7072
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Value cannot be null: provider" or storage connection errors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azurite isn't running. The Functions runtime needs a storage emulator even for HTTP triggers. Start Azurite, or verify that &lt;code&gt;local.settings.json&lt;/code&gt; contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; tells the SDK to connect to Azurite on its default ports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON deserialization errors on POST requests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two common causes: missing the &lt;code&gt;Content-Type: application/json&lt;/code&gt; header, or property name mismatches between your JSON and your C# record. By default, the serializer is case-insensitive, so &lt;code&gt;productId&lt;/code&gt; and &lt;code&gt;ProductId&lt;/code&gt; both work. But the property names must exist on the target type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The listener for function X was unable to start"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually a runtime version mismatch. Confirm &lt;code&gt;func --version&lt;/code&gt; returns 4.x and your &lt;code&gt;.csproj&lt;/code&gt; has &lt;code&gt;&amp;lt;AzureFunctionsVersion&amp;gt;v4&amp;lt;/AzureFunctionsVersion&amp;gt;&lt;/code&gt;. If you recently upgraded the Core Tools, also run &lt;code&gt;dotnet clean&lt;/code&gt; and rebuild.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We went from an empty directory to a running HTTP API with three production patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Query strings&lt;/strong&gt; for simple reads (&lt;code&gt;GET /api/Hello?name=Azure&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON bodies&lt;/strong&gt; for mutations (&lt;code&gt;POST /api/orders&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route parameters&lt;/strong&gt; for resource-oriented endpoints (&lt;code&gt;GET /api/products/electronics/42&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All running locally, no Azure subscription needed.&lt;/p&gt;

&lt;p&gt;The isolated worker model with ASP.NET Core integration gives you familiar types—&lt;code&gt;HttpRequest&lt;/code&gt;, &lt;code&gt;IActionResult&lt;/code&gt;, &lt;code&gt;[FromBody]&lt;/code&gt;—so writing Azure Functions feels like writing any other .NET web API. The main difference is that Azure handles the hosting, scaling, and infrastructure.&lt;/p&gt;

&lt;p&gt;The code samples in this article are deliberately simple. In a real project, you'd add input validation, error handling, and probably connect to a database or external service. But the patterns are the same—the &lt;code&gt;[HttpTrigger]&lt;/code&gt; attribute, the route configuration, the model binding. Once you understand these building blocks, everything else is regular C#.&lt;/p&gt;

&lt;p&gt;All code from this article is available in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository—clone it, run &lt;code&gt;func start&lt;/code&gt;, and experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next in This Series
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP Triggers&lt;/a&gt;&lt;/strong&gt; explores timer, queue, and blob triggers—the event-driven patterns that make Functions powerful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup&lt;/a&gt;&lt;/strong&gt; covers the tools, debugging techniques, and productivity tips that make daily development smooth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;&lt;/strong&gt; explains what "isolated" means, why Microsoft created this model, how it differs from the legacy in-process model, and what it means for your code.&lt;/p&gt;

&lt;p&gt;And yes—we'll cover &lt;strong&gt;deploying to Azure with CI/CD via GitHub Actions&lt;/strong&gt; later in the series, so everything you're building locally will make it to the cloud.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 2: Your First Azure Function: HTTP Triggers Step-by-Step (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
    <item>
      <title>I Replaced nvm, Homebrew Go, and Homebrew Python with a Single Tool</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Tue, 10 Feb 2026 09:11:54 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/i-replaced-nvm-homebrew-go-and-homebrew-python-with-a-single-tool-28g7</link>
      <guid>https://dev.to/martin_oehlert/i-replaced-nvm-homebrew-go-and-homebrew-python-with-a-single-tool-28g7</guid>
      <description>&lt;p&gt;I maintain a symlink-based dotfiles repo for macOS/zsh. Over time, I'd accumulated three different approaches to managing runtime versions: nvm for Node.js, &lt;code&gt;brew install go&lt;/code&gt; for Go, and &lt;code&gt;brew install python@3.13&lt;/code&gt; for Python. Each had its own quirks, its own update ritual, and its own startup cost. &lt;a href="https://mise.jdx.dev/" rel="noopener noreferrer"&gt;mise&lt;/a&gt; consolidates all of them into one tool with a single config file.&lt;/p&gt;

&lt;p&gt;This article walks through the actual migration: what changed, what broke, and the non-obvious tricks that made it worth doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point: an nvm lazy-load hack
&lt;/h2&gt;

&lt;p&gt;nvm is slow. Loading it adds 300-500ms to every new shell. My workaround was a lazy-load wrapper in &lt;code&gt;zprofile&lt;/code&gt; that deferred the cost until you actually called &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, or friends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# NVM (lazy-loaded for fast startup)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NVM_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.nvm"&lt;/span&gt;

_nvm_lazy_load&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  unfunction nvm node npm npx corepack 2&amp;gt;/dev/null
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/nvm.sh"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/nvm.sh"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

nvm&lt;span class="o"&gt;()&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; nvm &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
node&lt;span class="o"&gt;()&lt;/span&gt;     &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
npm&lt;span class="o"&gt;()&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; npm &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
npx&lt;span class="o"&gt;()&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; npx &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
corepack&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; corepack &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked, but it was 15 lines of shell plumbing to avoid a startup penalty from a tool that only manages Node. Go and Python had no version management at all — I just installed whatever Homebrew gave me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mise config
&lt;/h2&gt;

&lt;p&gt;mise replaces all of that with a TOML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tools]&lt;/span&gt;
&lt;span class="py"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"22"&lt;/span&gt;
&lt;span class="py"&gt;go&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.24"&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.13"&lt;/span&gt;

&lt;span class="nn"&gt;[settings]&lt;/span&gt;
&lt;span class="py"&gt;idiomatic_version_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. &lt;code&gt;mise install&lt;/code&gt; downloads the right versions. The &lt;code&gt;idiomatic_version_file&lt;/code&gt; setting means mise reads &lt;code&gt;.nvmrc&lt;/code&gt;, &lt;code&gt;.node-version&lt;/code&gt;, &lt;code&gt;.python-version&lt;/code&gt;, and &lt;code&gt;.go-version&lt;/code&gt; files from existing projects — so teams using nvm or pyenv don't need to change anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shell integration: the dual activation trick
&lt;/h2&gt;

&lt;p&gt;This is the part that took some thought. mise offers two modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shims&lt;/strong&gt; (&lt;code&gt;mise activate zsh --shims&lt;/code&gt;): Lightweight stubs in &lt;code&gt;~/.local/share/mise/shims/&lt;/code&gt; that resolve to the right binary. Fast, works everywhere, but they don't auto-switch versions when you &lt;code&gt;cd&lt;/code&gt; into a project with an &lt;code&gt;.nvmrc&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Activate&lt;/strong&gt; (&lt;code&gt;mise activate zsh&lt;/code&gt;): Hooks into your shell's &lt;code&gt;precmd&lt;/code&gt;/&lt;code&gt;chpwd&lt;/code&gt; to dynamically update &lt;code&gt;PATH&lt;/code&gt;. Supports auto-switching, but doesn't run in non-interactive contexts (scripts, IDE terminals running commands, cron jobs).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You need both.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;zprofile&lt;/code&gt; (runs for all sessions, including non-interactive):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Mise — shims for non-interactive sessions (scripts, IDEs, cron)&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mise activate zsh &lt;span class="nt"&gt;--shims&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;plugins.zsh&lt;/code&gt; (sourced only in interactive shells via &lt;code&gt;zshrc&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_cache_source mise mise activate zsh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The activate call in interactive shells overrides the shims with real &lt;code&gt;PATH&lt;/code&gt; entries, enabling directory-aware version switching. Non-interactive sessions fall back to shims, which resolve to whatever version the config specifies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;_cache_source&lt;/code&gt; + zcompile trick
&lt;/h2&gt;

&lt;p&gt;That &lt;code&gt;_cache_source&lt;/code&gt; call above is doing more than it looks. Running &lt;code&gt;mise activate zsh&lt;/code&gt; generates a shell script on every new shell — and that generation adds 25-300ms depending on the machine. For a shell that opens in 50ms total, that's unacceptable.&lt;/p&gt;

&lt;p&gt;The solution: cache the output and compile it to zsh bytecode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_cache_source&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift
  local &lt;/span&gt;&lt;span class="nv"&gt;cache_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZSH_COMP_CACHE&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;.zsh"&lt;/span&gt;
  &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nv"&gt;stale&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;N.mh+24&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; &lt;span class="nv"&gt;$#stale&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
    zcompile &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what it does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs &lt;code&gt;mise activate zsh&lt;/code&gt; and captures the output to a cache file&lt;/li&gt;
&lt;li&gt;Compiles it to zsh bytecode with &lt;code&gt;zcompile&lt;/code&gt; (creates a &lt;code&gt;.zwc&lt;/code&gt; file)&lt;/li&gt;
&lt;li&gt;Sources the cached file on every subsequent shell open&lt;/li&gt;
&lt;li&gt;Regenerates after 24 hours (the &lt;code&gt;(N.mh+24)&lt;/code&gt; glob qualifier checks mtime)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why does the compiled version load faster? When you &lt;code&gt;source&lt;/code&gt; a plain &lt;code&gt;.zsh&lt;/code&gt; file, zsh reads the text, tokenizes the shell syntax, and parses it into an internal representation — every single time. &lt;code&gt;zcompile&lt;/code&gt; does that work once and writes the result as a &lt;code&gt;.zwc&lt;/code&gt; file — a pre-parsed "wordcode" format that zsh can memory-map and load directly, skipping the tokenize-and-parse steps entirely. For a large generated script like &lt;code&gt;mise activate&lt;/code&gt; output, this eliminates the most expensive part of &lt;code&gt;source&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The clever bit is that zsh handles &lt;code&gt;.zwc&lt;/code&gt; lookup transparently. When you run &lt;code&gt;source foo.zsh&lt;/code&gt;, zsh first checks for &lt;code&gt;foo.zsh.zwc&lt;/code&gt;. If the &lt;code&gt;.zwc&lt;/code&gt; exists and is newer than the source file, zsh loads the compiled version without being told to. This means &lt;code&gt;_cache_source&lt;/code&gt; doesn't need any special logic to prefer the compiled file — &lt;code&gt;source "$cache_file"&lt;/code&gt; does the right thing on its own.&lt;/p&gt;

&lt;p&gt;And if the &lt;code&gt;.zwc&lt;/code&gt; is stale or missing? Zsh silently falls back to parsing the source file the normal way. Nothing breaks — you just lose the speed benefit until the cache regenerates. A stale &lt;code&gt;.zwc&lt;/code&gt; can never serve incorrect code because zsh won't use it if the source has changed. This makes the whole approach safe to leave running unattended.&lt;/p&gt;

&lt;p&gt;The result: mise activation costs &amp;lt;5ms instead of 25-300ms. This same function works for any "run a command, source the output" pattern — I use it for fzf, direnv, and mise.&lt;/p&gt;

&lt;p&gt;This is probably the most reusable thing in the whole migration. If you maintain a zsh config and source any tool's &lt;code&gt;init&lt;/code&gt; or &lt;code&gt;activate&lt;/code&gt; output, wrap it in something like this.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For the full story on this caching approach — including a subtle glob qualifier bug that silently breaks it — see &lt;a href="https://dev.to/martin_oehlert/from-14s-to-53ms-optimizing-zsh-startup-on-macos-5f09"&gt;From 1.4s to 53ms: Optimizing zsh Startup on macOS&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Brew changes
&lt;/h2&gt;

&lt;p&gt;The cleanup was straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remove:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nvm&lt;/code&gt; — replaced by mise&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node&lt;/code&gt; — installed by mise&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go&lt;/code&gt; — installed by mise&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;python@3.13&lt;/code&gt; — installed by mise&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yarn&lt;/code&gt; — moved to corepack (see below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Added:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mise&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One gotcha: some Homebrew formulae declare &lt;code&gt;node&lt;/code&gt; as a dependency. In my case, &lt;code&gt;markdownlint-cli2&lt;/code&gt; depends on the Homebrew &lt;code&gt;node&lt;/code&gt; formula. Removing &lt;code&gt;node&lt;/code&gt; from the Brewfile triggers warnings on &lt;code&gt;brew bundle&lt;/code&gt;, but it's cosmetic — mise's shims satisfy the actual runtime need. The formula's dependency declaration is about the build, not about your shell having &lt;code&gt;node&lt;/code&gt; on &lt;code&gt;PATH&lt;/code&gt;. I left it as-is and the warnings are harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corepack for yarn
&lt;/h2&gt;

&lt;p&gt;With nvm gone, yarn no longer comes from Homebrew either. Node ships with corepack, which manages yarn (and pnpm) natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;corepack &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates symlinks inside the Node install directory. The catch: when mise installs a new Node version, those symlinks live in the old version's directory. You need to re-run &lt;code&gt;corepack enable&lt;/code&gt; after &lt;code&gt;mise install node@&amp;lt;new-version&amp;gt;&lt;/code&gt;. It's a one-liner, but easy to forget when you bump Node versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration procedure
&lt;/h2&gt;

&lt;p&gt;The key insight is that nvm and mise can coexist. This makes the migration safe to do in phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Install mise alongside nvm:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;brew install mise&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;mise/config.toml&lt;/code&gt; with your current versions&lt;/li&gt;
&lt;li&gt;Add the dual activation (shims in &lt;code&gt;zprofile&lt;/code&gt;, activate in &lt;code&gt;plugins.zsh&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Open a new shell, verify &lt;code&gt;node --version&lt;/code&gt;, &lt;code&gt;go version&lt;/code&gt;, &lt;code&gt;python3 --version&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Verify everything works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run your builds, tests, dev servers&lt;/li&gt;
&lt;li&gt;Check IDE integration (VS Code, JetBrains)&lt;/li&gt;
&lt;li&gt;Verify non-interactive contexts: &lt;code&gt;zsh -c 'node --version'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;corepack enable&lt;/code&gt; and confirm &lt;code&gt;yarn --version&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — Remove nvm:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete the lazy-load block from &lt;code&gt;zprofile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;nvm&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;go&lt;/code&gt;, &lt;code&gt;python@3.13&lt;/code&gt;, &lt;code&gt;yarn&lt;/code&gt; from &lt;code&gt;Brewfile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;brew bundle cleanup --force&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Commit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Safety net:&lt;/strong&gt; nvm's data (&lt;code&gt;~/.nvm/&lt;/code&gt;) stays untouched through all of this. If something breaks, you can restore the old &lt;code&gt;zprofile&lt;/code&gt; block and you're back to where you started. The whole thing is one &lt;code&gt;git checkout -- shell/zprofile&lt;/code&gt; away from reverting.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;.nvmrc&lt;/code&gt; compatibility
&lt;/h2&gt;

&lt;p&gt;If your team uses &lt;code&gt;.nvmrc&lt;/code&gt; files (and most Node teams do), the &lt;code&gt;idiomatic_version_file = true&lt;/code&gt; setting in mise's config handles it. When you &lt;code&gt;cd&lt;/code&gt; into a directory with an &lt;code&gt;.nvmrc&lt;/code&gt;, mise reads it and switches to the specified Node version — same as nvm would.&lt;/p&gt;

&lt;p&gt;This also works for &lt;code&gt;.node-version&lt;/code&gt;, &lt;code&gt;.python-version&lt;/code&gt;, and &lt;code&gt;.go-version&lt;/code&gt;. So mise supports the conventions from nvm, nodenv, pyenv, and goenv without any extra configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final result
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;zprofile&lt;/code&gt; went from 30 lines to 18. The nvm lazy-load hack — 15 lines of shell functions — became a single &lt;code&gt;eval&lt;/code&gt; line. Three separate version management approaches (nvm, Homebrew Go, Homebrew Python) became one config file.&lt;/p&gt;

&lt;p&gt;Shell startup stayed at ~0.05s thanks to the caching strategy. The dual activation means shims handle non-interactive contexts while the full activate gives you directory-aware switching in your terminal.&lt;/p&gt;

&lt;p&gt;The commit tells the story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Replace nvm with mise for Node.js, Go, and Python version management

8 files changed, 17 insertions(+), 19 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More deletions than insertions. That's usually a good sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else mise can do
&lt;/h2&gt;

&lt;p&gt;This article focused on version management, but mise is a broader dev environment tool. Here's what else it offers once you have it installed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Task runner
&lt;/h3&gt;

&lt;p&gt;mise has a &lt;a href="https://mise.jdx.dev/tasks/" rel="noopener noreferrer"&gt;built-in task runner&lt;/a&gt; that replaces Makefiles and npm scripts. Tasks defined in &lt;code&gt;mise.toml&lt;/code&gt; (or as standalone scripts in a &lt;code&gt;mise-tasks/&lt;/code&gt; directory) run with the full mise environment — correct tool versions and env vars already set. Dependencies between tasks execute in parallel by default, and &lt;code&gt;mise watch&lt;/code&gt; re-runs tasks on file changes. For projects that already have a &lt;code&gt;mise.toml&lt;/code&gt; for version management, adding tasks means one fewer tool in the chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://mise.jdx.dev/environments/" rel="noopener noreferrer"&gt;&lt;code&gt;[env]&lt;/code&gt; section&lt;/a&gt; in &lt;code&gt;mise.toml&lt;/code&gt; sets project-level environment variables that activate when you &lt;code&gt;cd&lt;/code&gt; into the directory — the same behavior as direnv, without a separate tool. It supports dotenv files, required variables with validation, and redactions for secrets that shouldn't appear in logs. Earlier in this article, &lt;code&gt;_cache_source&lt;/code&gt; caches direnv's activation output alongside mise's — mise's native env management could replace direnv entirely, removing one more tool from the shell startup chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hooks
&lt;/h3&gt;

&lt;p&gt;mise can run &lt;a href="https://mise.jdx.dev/hooks.html" rel="noopener noreferrer"&gt;shell commands on directory enter/leave events&lt;/a&gt; and after tool installations. The postinstall hook is particularly relevant to this migration — it could automate the &lt;code&gt;corepack enable&lt;/code&gt; step that's currently manual after Node upgrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lockfile for reproducible environments
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://mise.jdx.dev/dev-tools/mise-lock.html" rel="noopener noreferrer"&gt;&lt;code&gt;mise.lock&lt;/code&gt;&lt;/a&gt; pins exact tool versions and checksums per platform, similar to &lt;code&gt;package-lock.json&lt;/code&gt;. For teams sharing a &lt;code&gt;mise.toml&lt;/code&gt;, the lockfile ensures everyone gets the same binary — not just the same version range. Enable it with &lt;code&gt;mise settings lockfile=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article covered the version management migration, but mise's scope goes well beyond that — it's closer to a unified dev environment than a version manager.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The full &lt;a href="https://github.com/MO2k4/config" rel="noopener noreferrer"&gt;dotfiles repo&lt;/a&gt; and migration commit are available on GitHub. The &lt;code&gt;_cache_source&lt;/code&gt; function lives in &lt;code&gt;shell/zshrc.d/completions.zsh&lt;/code&gt; if you want to steal it.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>zsh</category>
      <category>shell</category>
      <category>devtools</category>
    </item>
    <item>
      <title>From 1.4s to 53ms: Optimizing zsh Startup on macOS</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Mon, 09 Feb 2026 07:29:09 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/from-14s-to-53ms-optimizing-zsh-startup-on-macos-5f09</link>
      <guid>https://dev.to/martin_oehlert/from-14s-to-53ms-optimizing-zsh-startup-on-macos-5f09</guid>
      <description>&lt;p&gt;Every time I opened a terminal, I waited. Not long — maybe a second and a half — but long enough to notice. Long enough to be annoying. I finally decided to profile my zsh startup, and what I found took it from &lt;strong&gt;1.4 seconds down to 53 milliseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiling with zprof
&lt;/h2&gt;

&lt;p&gt;Zsh has a built-in profiler. Add &lt;code&gt;zmodload zsh/zprof&lt;/code&gt; at the top of your &lt;code&gt;.zshrc&lt;/code&gt; and &lt;code&gt;zprof&lt;/code&gt; at the bottom, then open a new shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# top of .zshrc&lt;/span&gt;
zmodload zsh/zprof

&lt;span class="c"&gt;# ... your config ...&lt;/span&gt;

&lt;span class="c"&gt;# bottom of .zshrc&lt;/span&gt;
zprof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My initial profile told a clear story:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Culprit&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;% of startup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVM (&lt;code&gt;nvm.sh&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;~430ms&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completion subprocesses (kubectl, helm, gh, ...)&lt;/td&gt;
&lt;td&gt;~400ms&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;compinit&lt;/code&gt; (full rebuild every time)&lt;/td&gt;
&lt;td&gt;~240ms&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;brew shellenv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~30ms&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go env GOPATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~20ms&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything else&lt;/td&gt;
&lt;td&gt;~280ms&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four of these five are subprocess calls — things like &lt;code&gt;eval "$(brew shellenv)"&lt;/code&gt; or &lt;code&gt;source &amp;lt;(kubectl completion zsh)&lt;/code&gt; that fork a process just to produce some static text. That's the low-hanging fruit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization 1: Lazy-load NVM
&lt;/h2&gt;

&lt;p&gt;NVM was the single biggest offender. Sourcing &lt;code&gt;nvm.sh&lt;/code&gt; on every shell startup cost ~430ms, and I don't use &lt;code&gt;node&lt;/code&gt; in every terminal session. The fix: wrapper functions that defer loading until you actually call &lt;code&gt;nvm&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NVM_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.nvm"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/nvm.sh"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/nvm.sh"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NVM_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.nvm"&lt;/span&gt;

_nvm_lazy_load&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  unfunction nvm node npm npx corepack 2&amp;gt;/dev/null
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/nvm.sh"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/nvm.sh"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

nvm&lt;span class="o"&gt;()&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; nvm &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
node&lt;span class="o"&gt;()&lt;/span&gt;     &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
npm&lt;span class="o"&gt;()&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; npm &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
npx&lt;span class="o"&gt;()&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; npx &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
corepack&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; _nvm_lazy_load&lt;span class="p"&gt;;&lt;/span&gt; corepack &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper functions replace themselves on first call via &lt;code&gt;unfunction&lt;/code&gt;, then delegate to the real command. Cost at startup: zero. Cost on first &lt;code&gt;node&lt;/code&gt; invocation: ~430ms (once).&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization 2: Hardcode static values
&lt;/h2&gt;

&lt;p&gt;Several lines in my config were spawning subprocesses to compute values that never change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before — subprocess every startup&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;/opt/homebrew/bin/brew shellenv&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;go &lt;span class="nb"&gt;env &lt;/span&gt;GOPATH&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/bin"&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These produce the same output every time. Just paste the result directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After — zero subprocesses&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HOMEBREW_PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HOMEBREW_CELLAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/Cellar"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HOMEBREW_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/bin:/opt/homebrew/sbin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MANPATH&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MANPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;":&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MANPATH&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;:&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;INFOPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/share/info:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFOPATH&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/go"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$GOPATH&lt;/span&gt;&lt;span class="s2"&gt;/bin"&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave a comment like &lt;code&gt;# regenerate with: brew shellenv&lt;/code&gt; so future-you knows where the values came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization 3: Cache completions into fpath
&lt;/h2&gt;

&lt;p&gt;This was the big one. My original config eagerly sourced completions from 12 different tools on every shell startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before — 12 subprocesses, every startup&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; kubectl  &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;kubectl completion zsh&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; helm     &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;helm completion zsh&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; minikube &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;minikube completion zsh&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; gh       &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;gh completion &lt;span class="nt"&gt;-s&lt;/span&gt; zsh&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;# ... 8 more tools&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;source &amp;lt;(tool completion zsh)&lt;/code&gt; forks a subprocess AND evaluates thousands of lines of shell code. Minikube's completion alone is 5,000 lines.&lt;/p&gt;

&lt;p&gt;The fix has two parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For completions:&lt;/strong&gt; write them to files in an fpath directory. Compinit loads these lazily — only when you actually press TAB on that command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ZSH_COMP_CACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.zsh-completion-cache"&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZSH_COMP_CACHE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZSH_COMP_CACHE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

_cache_fpath&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift
  local &lt;/span&gt;&lt;span class="nv"&gt;cache_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZSH_COMP_CACHE&lt;/span&gt;&lt;span class="s2"&gt;/_&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nv"&gt;stale&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;N.mh+24&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; &lt;span class="nv"&gt;$#stale&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; kubectl &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _cache_fpath kubectl kubectl completion zsh
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; helm    &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _cache_fpath helm    helm completion zsh
&lt;span class="c"&gt;# ... etc&lt;/span&gt;

&lt;span class="nv"&gt;fpath&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nv"&gt;$ZSH_COMP_CACHE&lt;/span&gt; &lt;span class="nv"&gt;$fpath&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For plugins that must run at startup&lt;/strong&gt; (fzf keybindings, direnv hook, oh-my-posh prompt), cache their init output and &lt;code&gt;zcompile&lt;/code&gt; for faster sourcing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_cache_source&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift
  local &lt;/span&gt;&lt;span class="nv"&gt;cache_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZSH_COMP_CACHE&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;.zsh"&lt;/span&gt;
  &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nv"&gt;stale&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;N.mh+24&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; &lt;span class="nv"&gt;$#stale&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
    zcompile &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

_cache_source fzf fzf &lt;span class="nt"&gt;--zsh&lt;/span&gt;
_cache_source direnv direnv hook zsh
_cache_source oh-my-posh oh-my-posh init zsh &lt;span class="nt"&gt;--config&lt;/span&gt; ~/.poshthemes/theme.omp.json &lt;span class="nt"&gt;--print&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both functions use a 24-hour cache expiry via zsh glob qualifiers. Delete &lt;code&gt;~/.zsh-completion-cache&lt;/code&gt; to force a refresh.&lt;/p&gt;

&lt;p&gt;I also cached &lt;code&gt;compinit&lt;/code&gt; itself — a full rebuild only runs once per day, and otherwise &lt;code&gt;compinit -C&lt;/code&gt; skips straight to the dump file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;autoload &lt;span class="nt"&gt;-Uz&lt;/span&gt; compinit
&lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nv"&gt;zcompdump_stale&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;~/.zcompdump&lt;span class="o"&gt;(&lt;/span&gt;N.mh+24&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; &lt;span class="nv"&gt;$#zcompdump_stale&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;compinit
&lt;span class="k"&gt;else
  &lt;/span&gt;compinit &lt;span class="nt"&gt;-C&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; zcompile ~/.zcompdump &lt;span class="o"&gt;}&lt;/span&gt; &amp;amp;!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug that almost ruined everything
&lt;/h2&gt;

&lt;p&gt;After implementing all of this, I ran &lt;code&gt;time zsh -i -c exit&lt;/code&gt;. The result: &lt;strong&gt;1.59 seconds&lt;/strong&gt;. &lt;em&gt;Slower&lt;/em&gt; than before.&lt;/p&gt;

&lt;p&gt;I profiled again and saw this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;num  calls                time            self            name
-----------------------------------------------------------------
 1)   15   1180.06  97.34%  1169.02  96.43%  _cache_completion
 2)    1     26.83   2.21%     7.49   0.62%  compinit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caching function was taking 97% of startup time across 15 calls. The caches existed on disk but were being &lt;strong&gt;regenerated every single time&lt;/strong&gt;. The staleness check was broken.&lt;/p&gt;

&lt;p&gt;I restructured the approach — separating completions (fpath-based, lazy) from plugins (source-based, eager) — and tried again. Same problem: &lt;code&gt;_cache_fpath&lt;/code&gt; at 72%, &lt;code&gt;compinit&lt;/code&gt; doing full rebuilds.&lt;/p&gt;

&lt;p&gt;The bug was in this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="c"&gt;#qN.mh+24) ]]; then&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks reasonable. The glob qualifier &lt;code&gt;(#qN.mh+24)&lt;/code&gt; means "match if the file is older than 24 hours, with N (nullglob) to return empty string if no match." The &lt;code&gt;-n&lt;/code&gt; test checks if the result is non-empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem: glob qualifiers don't expand inside &lt;code&gt;[[ ]]&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Zsh's &lt;code&gt;[[ ]]&lt;/code&gt; conditional construct does not perform filename generation (globbing). The string &lt;code&gt;"$cache_file"(#qN.mh+24)&lt;/code&gt; is treated as the literal path with &lt;code&gt;(#qN.mh+24)&lt;/code&gt; appended as text. Since that string is always non-empty, the condition is &lt;strong&gt;always true&lt;/strong&gt;. Every cache was being regenerated on every startup. The caching was doing nothing.&lt;/p&gt;

&lt;p&gt;The same bug affected the &lt;code&gt;compinit&lt;/code&gt; staleness check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Also broken — compinit was doing a full rebuild every time&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; ~/.zcompdump&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="c"&gt;#qN.mh+24) ]]; then&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; expand the glob into an array variable first, then check its length:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nv"&gt;stale&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;N.mh+24&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cache_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; &lt;span class="nv"&gt;$#stale&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regular variable assignments DO perform globbing. The &lt;code&gt;(N.mh+24)&lt;/code&gt; qualifier (no &lt;code&gt;#q&lt;/code&gt; prefix needed outside &lt;code&gt;[[ ]]&lt;/code&gt;) expands the glob, and &lt;code&gt;$#stale&lt;/code&gt; gives us the match count. If the file is older than 24 hours, &lt;code&gt;stale&lt;/code&gt; contains one element; otherwise it's empty.&lt;/p&gt;

&lt;p&gt;This is a subtle footgun. The code looks correct, it doesn't produce errors, and the caches &lt;em&gt;are&lt;/em&gt; created — they're just never &lt;em&gt;reused&lt;/em&gt;. Without profiling, you'd never know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;zsh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;span class="go"&gt;zsh -i -c exit  0.03s user 0.02s system 93% cpu 0.053 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;53 milliseconds.&lt;/strong&gt; A 96% reduction from 1.4 seconds.&lt;/p&gt;

&lt;p&gt;Here's what each optimization contributed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimization&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lazy-load NVM&lt;/td&gt;
&lt;td&gt;~430ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache completions into fpath (lazy compinit)&lt;/td&gt;
&lt;td&gt;~500ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache plugin init scripts + zcompile&lt;/td&gt;
&lt;td&gt;~200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcode brew/go/cargo&lt;/td&gt;
&lt;td&gt;~50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;compinit -C (cached dump)&lt;/td&gt;
&lt;td&gt;~170ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1,350ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first shell open after 24 hours takes a couple of seconds to regenerate caches, but every subsequent shell is instant. You can force a full refresh anytime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.zsh-completion-cache ~/.zcompdump&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Profile first.&lt;/strong&gt; &lt;code&gt;zprof&lt;/code&gt; told me exactly where the time was going. Don't guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subprocess calls add up.&lt;/strong&gt; Each &lt;code&gt;eval $(...)&lt;/code&gt; or &lt;code&gt;source &amp;lt;(...)&lt;/code&gt; forks a process. Twelve of them cost almost a full second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fpath &amp;gt; source for completions.&lt;/strong&gt; Compinit loads completion functions lazily from fpath. Don't eagerly source thousands of lines you might never use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test your caching actually works.&lt;/strong&gt; A cache that regenerates every time is worse than no cache — it has the overhead of both the generation AND the file I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glob qualifiers don't work inside &lt;code&gt;[[ ]]&lt;/code&gt;.&lt;/strong&gt; This is the kind of bug that looks correct, produces no errors, and silently destroys your performance. Expand globs into variables first.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>zsh</category>
      <category>performance</category>
      <category>shell</category>
    </item>
  </channel>
</rss>
