<?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: Polymind</title>
    <description>The latest articles on DEV Community by Polymind (@polymind).</description>
    <link>https://dev.to/polymind</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%2Forganization%2Fprofile_image%2F12885%2Fb1a89aa9-d1e5-468d-aac0-a0a83be81baa.png</url>
      <title>DEV Community: Polymind</title>
      <link>https://dev.to/polymind</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/polymind"/>
    <language>en</language>
    <item>
      <title>Publishing Agents over A2A and Consuming Them from Durable Agents with Microsoft Agent Framework</title>
      <dc:creator>Tatsuro Shibamura</dc:creator>
      <pubDate>Mon, 27 Apr 2026 13:55:51 +0000</pubDate>
      <link>https://dev.to/polymind/publishing-agents-over-a2a-and-consuming-them-from-durable-agents-with-microsoft-agent-framework-1b09</link>
      <guid>https://dev.to/polymind/publishing-agents-over-a2a-and-consuming-them-from-durable-agents-with-microsoft-agent-framework-1b09</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Microsoft's &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Agent Framework&lt;/a&gt; recently went GA, so I sat down to actually learn it. The official samples and docs tend to mix old and new APIs, and pull in more dependencies than the framework actually requires — so this post strips everything down to the smallest amount of code that still demonstrates the parts I care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exposing an agent over &lt;strong&gt;A2A&lt;/strong&gt; with a few lines on top of ASP.NET Core Minimal APIs&lt;/li&gt;
&lt;li&gt;Consuming an A2A-published agent and using it as a regular &lt;code&gt;AIAgent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Letting an LLM-powered orchestrator call A2A agents &lt;strong&gt;as tools&lt;/strong&gt; (&lt;code&gt;AsAIFunction&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Hosting all of the above as &lt;strong&gt;Durable Agents&lt;/strong&gt; on Azure Functions + Durable Task Scheduler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All samples target Azure OpenAI via API key. If you want to use Foundry projects or other providers instead, check the official docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/agents/providers/azure-openai" rel="noopener noreferrer"&gt;Azure OpenAI provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/agents/providers/microsoft-foundry" rel="noopener noreferrer"&gt;Microsoft Foundry provider&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I won't cover basic agent definition here — building a simple agent with Agent Framework is straightforward, and I want to focus specifically on &lt;strong&gt;A2A&lt;/strong&gt; and &lt;strong&gt;Durable Agents&lt;/strong&gt;, where the docs are scattered and (in the case of &lt;em&gt;consuming&lt;/em&gt; A2A agents) effectively missing. If anything below feels like it's skipping over the basics, the &lt;a href="https://learn.microsoft.com/en-us/agent-framework/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; fill that in.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/microsoft" rel="noopener noreferrer"&gt;
        microsoft
      &lt;/a&gt; / &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;
        agent-framework
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A framework for building, orchestrating and deploying AI agents and multi-agent workflows with support for Python and .NET.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/microsoft/agent-framework/docs/assets/readme-banner.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fagent-framework%2FHEAD%2Fdocs%2Fassets%2Freadme-banner.png" alt="Microsoft Agent Framework"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Welcome to Microsoft Agent Framework!&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://discord.gg/b5zjErwbQM" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/172182e7ee126e7016f7c829285513a89265949c6c0d18d7915e4d72fbe7a0df/68747470733a2f2f646362616467652e6c696d65732e70696e6b2f6170692f7365727665722f62357a6a45727762514d3f7374796c653d666c6174" alt="Microsoft Foundry Discord"&gt;&lt;/a&gt;
&lt;a href="https://learn.microsoft.com/en-us/agent-framework/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/23a2e314351e1dd34ec18b58e9e3545ee67203474d0506e67af030be734b6ac2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d532532304c6561726e2d446f63756d656e746174696f6e2d626c7565" alt="MS Learn Documentation"&gt;&lt;/a&gt;
&lt;a href="https://pypi.org/project/agent-framework/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fe7d8b6f8589c3ed2d12fdbfafe9b3666cfbb04318d370dd244ac94bb8e19111/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f6167656e742d6672616d65776f726b" alt="PyPI"&gt;&lt;/a&gt;
&lt;a href="https://www.nuget.org/profiles/MicrosoftAgentFramework/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e0b2f852b271d32d28453968d3392acd914ab1383970f5e8c213df7e601807d1/68747470733a2f2f696d672e736869656c64732e696f2f6e756765742f762f4d6963726f736f66742e4167656e74732e4149" alt="NuGet"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Welcome to Microsoft's comprehensive multi-language framework for building, orchestrating, and deploying AI agents with support for both .NET and Python implementations. This framework provides everything from simple chat agents to complex multi-agent workflows with graph-based orchestration.&lt;/p&gt;
&lt;p&gt;
  &lt;a href="https://www.youtube.com/watch?v=AAgdMhftj8w" title="Watch the full Agent Framework introduction (30 min)" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/03be07cfc1271b3db878247acb254e099b035c47fd3766c4805bf06a8dd33300/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f414167644d6866746a38772f687164656661756c742e6a7067" alt="Watch the full Agent Framework introduction (30 min)" width="480"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.youtube.com/watch?v=AAgdMhftj8w" rel="nofollow noopener noreferrer"&gt;
    Watch the full Agent Framework introduction (30 min)
  &lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📋 Getting Started&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;📦 Installation&lt;/h3&gt;
&lt;/div&gt;

&lt;p&gt;Python&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pip install agent-framework
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; This will install all sub-packages, see `python/packages` for individual packages.&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; It may take a minute on first install on Windows.&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;.NET&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;dotnet add package Microsoft.Agents.AI&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;📚 Documentation&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://learn.microsoft.com/agent-framework/overview/agent-framework-overview" rel="nofollow noopener noreferrer"&gt;Overview&lt;/a&gt;&lt;/strong&gt; - High level overview of the framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://learn.microsoft.com/agent-framework/tutorials/quick-start" rel="nofollow noopener noreferrer"&gt;Quick Start&lt;/a&gt;&lt;/strong&gt; - Get started with a simple agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://learn.microsoft.com/agent-framework/tutorials/overview" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;&lt;/strong&gt; - Step by step tutorials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/user-guide/overview" rel="nofollow noopener noreferrer"&gt;User Guide&lt;/a&gt;&lt;/strong&gt; - In-depth user guide for building agents and workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel" rel="nofollow noopener noreferrer"&gt;Migration from Semantic Kernel&lt;/a&gt;&lt;/strong&gt; - Guide to migrate from Semantic Kernel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-autogen" rel="nofollow noopener noreferrer"&gt;Migration from AutoGen&lt;/a&gt;&lt;/strong&gt; - Guide to migrate from AutoGen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still have questions? Join our &lt;a href="https://github.com/microsoft/agent-framework/./COMMUNITY.md#public-community-office-hours" rel="noopener noreferrer"&gt;weekly office hours&lt;/a&gt; or…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Exposing an agent over A2A
&lt;/h2&gt;

&lt;p&gt;A year ago I wouldn't have bothered exposing my own agents over A2A for other agents to consume. But as LLMs and agents have gotten better — and as the tasks I want to delegate have gotten heavier and more specialized — the case for A2A has gotten a lot stronger.&lt;/p&gt;

&lt;p&gt;Agent Framework supports both sides of A2A: publishing and consuming. If you have even a passing familiarity with ASP.NET Core Minimal APIs, publishing an agent is genuinely easy. Auth and the rest of the production concerns are still on you, but they're standard ASP.NET Core problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/integrations/a2a" rel="noopener noreferrer"&gt;Microsoft Learn: A2A integration&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The official sample is heavier than it needs to be. The only package you actually need (besides your LLM provider) is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Microsoft.Agents.AI.Hosting.A2A.AspNetCore" rel="noopener noreferrer"&gt;Microsoft.Agents.AI.Hosting.A2A.AspNetCore&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once installed, you get &lt;code&gt;MapA2AHttpJson&lt;/code&gt; and &lt;code&gt;MapA2AJsonRpc&lt;/code&gt; extension methods that hang off the same &lt;code&gt;WebApplication&lt;/code&gt; you already use for Minimal APIs. Below is a small two-agent host: a &lt;code&gt;WriterAgent&lt;/code&gt; that drafts Japanese social media posts and a &lt;code&gt;ReviewerAgent&lt;/code&gt; that judges them.&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;A2A&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;A2A.AspNetCore&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;Azure.AI.OpenAI&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;OpenAI.Chat&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;WebApplication&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chatClient&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;AzureOpenAIClient&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="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AOAI_ENDPOINT"&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;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApiKeyCredential&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;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AOAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5.4"&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;writerAgent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;SNS&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;engaging&lt;/span&gt; &lt;span class="n"&gt;Japanese&lt;/span&gt; &lt;span class="n"&gt;social&lt;/span&gt; &lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;based&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="n"&gt;provided&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Make&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;natural&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;easy&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;appropriate&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;broad&lt;/span&gt; &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Do&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;invent&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;implied&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Return&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="n"&gt;without&lt;/span&gt; &lt;span class="n"&gt;explanations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;quotation&lt;/span&gt; &lt;span class="n"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="s"&gt;""", name: "&lt;/span&gt;&lt;span class="n"&gt;WriterAgent&lt;/span&gt;&lt;span class="s"&gt;");
&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reviewerAgent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;SNS&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;reviewer&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Review&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;Japanese&lt;/span&gt; &lt;span class="n"&gt;social&lt;/span&gt; &lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;clarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;natural&lt;/span&gt; &lt;span class="n"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engagement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appropriateness&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;broad&lt;/span&gt; &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;whether&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt; &lt;span class="n"&gt;characters&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;fewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;acceptable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;respond&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APPROVED&lt;/span&gt;
    &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;needs&lt;/span&gt; &lt;span class="n"&gt;improvement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provide&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Japanese&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="s"&gt;""", name: "&lt;/span&gt;&lt;span class="n"&gt;ReviewerAgent&lt;/span&gt;&lt;span class="s"&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;AddA2AServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writerAgent&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;AddA2AServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reviewerAgent&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;app&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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapA2AJsonRpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writerAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/writer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapA2AJsonRpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reviewerAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/reviewer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapWellKnownAgentCard&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;AgentCard&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;"WriterAgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Creates a Japanese SNS post from a user-provided theme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SupportedInterfaces&lt;/span&gt; &lt;span class="p"&gt;=&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;AgentInterface&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;Url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://localhost:5062/writer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ProtocolBinding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"JSONRPC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ProtocolVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0"&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;"/writer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapWellKnownAgentCard&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;AgentCard&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;"ReviewerAgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Reviews a Japanese SNS post for clarity, tone, and suitability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SupportedInterfaces&lt;/span&gt; &lt;span class="p"&gt;=&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;AgentInterface&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://localhost:5062/reviewer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ProtocolBinding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"JSONRPC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ProtocolVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0"&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;"/reviewer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&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;The shape of it: define your agents, call &lt;code&gt;AddA2AServer&lt;/code&gt; once per agent, map each agent to a path with &lt;code&gt;MapA2AHttpJson&lt;/code&gt; / &lt;code&gt;MapA2AJsonRpc&lt;/code&gt;, and finally publish the agent card with &lt;code&gt;MapWellKnownAgentCard&lt;/code&gt;. In this sample the cards live at &lt;code&gt;/writer/.well-known/agent-card.json&lt;/code&gt; and &lt;code&gt;/reviewer/.well-known/agent-card.json&lt;/code&gt;, which is what consumers will look for.&lt;/p&gt;

&lt;p&gt;The agent card boilerplate is mildly annoying, but the actual &lt;em&gt;publishing&lt;/em&gt; is dead simple. Looking at the API surface, A2A doesn't really seem designed around hosting multiple agents per process — you can do it (as above), it just feels slightly against the grain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consuming an A2A agent
&lt;/h2&gt;

&lt;p&gt;Now the consumer side. The official docs and samples don't show this clearly anywhere I could find, so I had to feel my way through it. The good news: the package list is, again, just one entry.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Microsoft.Agents.AI.A2A/" rel="noopener noreferrer"&gt;Microsoft.Agents.AI.A2A&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a side note, Agent Framework's NuGet package names are convention-driven, so you can usually guess the right package from what you're trying to do. That's a small thing, but appreciated.&lt;/p&gt;

&lt;p&gt;This sample doesn't reference OpenAI at all — there's no LLM in this consumer. That's worth internalizing: &lt;strong&gt;Agent Framework doesn't require an LLM&lt;/strong&gt;. Plenty of workflows are deterministic enough that you don't need one. To consume an A2A agent, start with &lt;code&gt;A2ACardResolver&lt;/code&gt;, call &lt;code&gt;GetAIAgentAsync()&lt;/code&gt;, and you get back a regular &lt;code&gt;AIAgent&lt;/code&gt; you can use like any other.&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;A2A&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;writerResolver&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;A2ACardResolver&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:5062/writer/"&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;reviewerResolver&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;A2ACardResolver&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:5062/reviewer/"&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;writerAgent&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;writerResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAIAgentAsync&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;reviewerAgent&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;reviewerResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAIAgentAsync&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;writerResponse&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;writerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAsync&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;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Write about the features and benefits of Claude Opus 4.7"&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;reviewerResponse&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;reviewerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAsync&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;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;writerResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Writer: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;writerResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&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;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Reviewer: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reviewerResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&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;One sharp edge worth flagging: the URL passed to &lt;code&gt;A2ACardResolver&lt;/code&gt; &lt;strong&gt;must end with a trailing &lt;code&gt;/&lt;/code&gt;&lt;/strong&gt;. Without it, the agent card download fails. If you're hosting one agent at the root it doesn't matter, but as soon as you have multiple agents at sub-paths (like above) it bites.&lt;/p&gt;

&lt;p&gt;Run it and you'll see both agents called in sequence, each printing its result.&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%2Fch51fn21eytn116il6vo.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%2Fch51fn21eytn116il6vo.png" alt="Sequential execution result of Writer and Reviewer agents over A2A" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's powerful here isn't the code — it's that &lt;code&gt;AIAgent&lt;/code&gt; is the abstraction. Once you have one in hand, you don't care whether it's a local agent or a remote A2A agent. That's the win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using an A2A agent as a tool
&lt;/h2&gt;

&lt;p&gt;Calling agents in a fixed sequence is fine when you don't need an LLM in the middle, but in practice you often &lt;em&gt;do&lt;/em&gt; want an LLM deciding which agent to call and when. Agent Framework lets you turn an &lt;code&gt;AIAgent&lt;/code&gt; into a tool callable by another agent, which makes building autonomous-ish workflows surprisingly low-effort.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/journey/agents-as-tools" rel="noopener noreferrer"&gt;Agents as tools (journey)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/agents/tools/#using-an-agent-as-a-function-tool" rel="noopener noreferrer"&gt;Using an agent as a function tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mechanic is just one extension method: call &lt;code&gt;AsAIFunction()&lt;/code&gt; on an &lt;code&gt;AIAgent&lt;/code&gt; and you get something an orchestrator agent can use as a tool.&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;A2A&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;Azure.AI.OpenAI&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.Agents.AI&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;OpenAI.Chat&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;chatClient&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;AzureOpenAIClient&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;"AOAI_ENDPOINT"&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;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApiKeyCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AOAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5.4"&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;writerResolver&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;A2ACardResolver&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:5062/writer/"&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;reviewerResolver&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;A2ACardResolver&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:5062/reviewer/"&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;writerAgent&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;writerResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAIAgentAsync&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;reviewerAgent&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;reviewerResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAIAgentAsync&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;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;        &lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;specialized&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;creating&lt;/span&gt; &lt;span class="n"&gt;social&lt;/span&gt; &lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;Based&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="n"&gt;provided&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;registered&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engaging&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;suitable&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;posting&lt;/span&gt; &lt;span class="n"&gt;publicly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

        &lt;span class="n"&gt;Always&lt;/span&gt; &lt;span class="n"&gt;follow&lt;/span&gt; &lt;span class="n"&gt;these&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;social&lt;/span&gt; &lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;based&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;.&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="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;reviewer&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;clarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appeal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="n"&gt;unnatural&lt;/span&gt; &lt;span class="n"&gt;phrasing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;reviewer&lt;/span&gt; &lt;span class="n"&gt;provides&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="n"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;suggested&lt;/span&gt; &lt;span class="n"&gt;improvements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;feedback&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ask&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;revised&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Repeat&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="n"&gt;cycle&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;good&lt;/span&gt; &lt;span class="n"&gt;enough&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;After&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;produce&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="n"&gt;social&lt;/span&gt; &lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Do&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="n"&gt;intermediate&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;explanations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Japanese&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Keep&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;concise&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;natural&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;single&lt;/span&gt; &lt;span class="n"&gt;social&lt;/span&gt; &lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;emojis&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hashtags&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;appropriate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;overuse&lt;/span&gt; &lt;span class="n"&gt;them&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="s"&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;"SocialPostAgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;writerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIFunction&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;reviewerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIFunction&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAsync&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;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Write about the features and benefits of Claude Opus 4.7"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&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;Text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wires up an orchestrator that calls Writer and Reviewer iteratively. If you were doing this sequentially, you'd have to write the "did the reviewer approve? if not, loop" logic yourself. Here, the LLM decides — it'll call the tools as many times as it needs to before producing the final answer.&lt;/p&gt;

&lt;p&gt;The actual answer text is less interesting than the &lt;strong&gt;tool call trace&lt;/strong&gt;: in my run, the orchestrator called Writer, then Reviewer, then took the Reviewer's feedback and called Writer again. That kind of iterative refinement, expressed entirely through tool calls, falls out of the abstraction for free.&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%2Fe7vm2pubqkhh11ziv0ux.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%2Fe7vm2pubqkhh11ziv0ux.png" alt="Tool call trace showing iterative Writer and Reviewer invocations" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fact that you can write multi-agent flows like this &lt;strong&gt;without thinking about A2A at all&lt;/strong&gt; is, to me, the strongest argument for Agent Framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running on Durable Agents
&lt;/h2&gt;

&lt;p&gt;Last piece: the Durable Agents extension. Agents tend to run long, and hosting long-running things reliably is its own problem. Durable Agents runs on top of Durable Functions, which is a battle-tested execution layer for exactly this kind of workload.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/integrations/azure-functions" rel="noopener noreferrer"&gt;Microsoft Learn: Azure Functions integration&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll need an extra package, and at the time of writing there's a bug where you also have to add the Durable Task Scheduler package or you'll hit errors. That's fine — DTS gives you a much nicer view of agent execution history anyway, so plan to use it from the start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Microsoft.Agents.AI.Hosting.AzureFunctions/" rel="noopener noreferrer"&gt;Microsoft.Agents.AI.Hosting.AzureFunctions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Converting the earlier samples to Durable Agents is mostly a matter of swapping &lt;code&gt;WebApplication.CreateBuilder&lt;/code&gt; for &lt;code&gt;FunctionsApplication.CreateBuilder&lt;/code&gt; and calling &lt;code&gt;ConfigureDurableAgents&lt;/code&gt;. The agent definitions themselves barely change.&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;A2A&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.Agents.AI.Hosting.AzureFunctions&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.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="nf"&gt;ConfigureFunctionsWebApplication&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;writerResolver&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;A2ACardResolver&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;$"&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;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AGENT_HOST"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt;/writer/"&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;reviewerResolver&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;A2ACardResolver&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;$"&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;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AGENT_HOST"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt;/reviewer/"&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;writerAgent&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;writerResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAIAgentAsync&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;reviewerAgent&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;reviewerResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAIAgentAsync&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;ConfigureDurableAgents&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;=&amp;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="nf"&gt;AddAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writerAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enableHttpTrigger&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="n"&gt;enableMcpToolTrigger&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reviewerAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enableHttpTrigger&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="n"&gt;enableMcpToolTrigger&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="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;That on its own doesn't invoke anything. With Durable Agents you typically pair it with a Durable Functions orchestrator. (I'm assuming Durable Functions familiarity here — see the &lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview" rel="noopener noreferrer"&gt;Durable Functions docs&lt;/a&gt; if not.)&lt;/p&gt;

&lt;p&gt;The orchestrator below loops up to five times: call Writer, call Reviewer, exit if Reviewer says &lt;code&gt;APPROVED&lt;/code&gt;, otherwise feed the feedback back into the next Writer call. Session history is managed automatically per session, so just passing the Reviewer's text directly to the next Writer call is enough.&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;Function&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;RunOrchestrator&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RunOrchestrator&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;OrchestrationTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;TaskOrchestrationContext&lt;/span&gt; &lt;span class="n"&gt;context&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;message&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="n"&gt;GetInput&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;&amp;gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;writerAgent&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="nf"&gt;GetAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WriterAgent"&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;writerSession&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;writerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSessionAsync&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;reviewerAgent&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="nf"&gt;GetAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReviewerAgent"&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;reviewerSession&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;reviewerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSessionAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;for&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;i&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;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&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;writerResponse&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;writerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAsync&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;&amp;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;writerSession&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;reviewerResponse&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;reviewerAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAsync&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;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;writerResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reviewerSession&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;reviewerResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"APPROVED"&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;writerResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&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;reviewerResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&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="s"&gt;"FAILED"&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 new concept here is &lt;strong&gt;Session&lt;/strong&gt;, which — as the name suggests — corresponds to an agent's conversation history. With Durable Agents, sessions are backed by Durable Entities under the hood, so they're strongly isolated and scale well.&lt;/p&gt;

&lt;p&gt;Trigger the orchestrator (e.g., from an HTTP-triggered function) and you can watch the whole thing execute in DTS. The DTS UI has gotten genuinely good — both timeline and chat views are available per agent, so you can inspect every message that flowed between Writer and Reviewer. Building this kind of observability yourself is painful; getting it for free out of Durable Agents + DTS is a real productivity multiplier.&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%2Fnpx356502puu5vb43b6x.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%2Fnpx356502puu5vb43b6x.png" alt="Durable Task Scheduler showing the orchestrator execution history" width="800" height="524"&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%2Fpgbjiajdlq91viaeywdk.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%2Fpgbjiajdlq91viaeywdk.png" alt="Per-agent timeline and chat view in DTS" width="791" height="761"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;I wanted to also dig into how Durable Agents behaves when an A2A agent is invoked as a tool call, but this post is already long enough — that's a separate write-up. AI agents pair very well with Flex Consumption and Container Apps, so I'll keep tracking Agent Framework as it evolves.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>ai</category>
      <category>azure</category>
      <category>agents</category>
    </item>
    <item>
      <title>OpenApiWeaver: Generate Type-Safe C# Clients from OpenAPI at Compile Time with Source Generators</title>
      <dc:creator>Tatsuro Shibamura</dc:creator>
      <pubDate>Fri, 10 Apr 2026 06:59:59 +0000</pubDate>
      <link>https://dev.to/polymind/openapiweaver-generate-type-safe-c-clients-from-openapi-at-compile-time-with-source-generators-5e79</link>
      <guid>https://dev.to/polymind/openapiweaver-generate-type-safe-c-clients-from-openapi-at-compile-time-with-source-generators-5e79</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;OpenApiWeaver&lt;/strong&gt;, a C# library that generates HTTP clients from OpenAPI definitions at &lt;strong&gt;compile time&lt;/strong&gt; using Source Generators. Just drop your OpenAPI file into your project, add one &lt;code&gt;ItemGroup&lt;/code&gt; entry, and build — no codegen CLI, no reflection at runtime, full NRT and &lt;code&gt;required&lt;/code&gt; support, and clean handling of string-based enums via &lt;code&gt;readonly record struct&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/shibayan" rel="noopener noreferrer"&gt;
        shibayan
      &lt;/a&gt; / &lt;a href="https://github.com/shibayan/openapi-weaver" rel="noopener noreferrer"&gt;
        openapi-weaver
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      OpenAPI documents into strongly typed C# HTTP clients at build time with an incremental Roslyn source generator.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OpenApiWeaver&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/shibayan/openapi-weaver/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/shibayan/openapi-weaver/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://www.nuget.org/packages/OpenApiWeaver/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/be008e002c946c2526eb9b50222631cdc5c5a6973a1c136578a914ebca2321cc/68747470733a2f2f62616467656e2e6e65742f6e756765742f64742f4f70656e417069576561766572" alt="Downloads"&gt;&lt;/a&gt;
&lt;a href="https://www.nuget.org/packages/OpenApiWeaver" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/67dba0420bd7a5429b07b735645e71c19a5dac4b3b4d14246326a87ab6105ebb/68747470733a2f2f696d672e736869656c64732e696f2f6e756765742f762f4f70656e417069576561766572" alt="NuGet"&gt;&lt;/a&gt;
&lt;a href="https://github.com/shibayan/openapi-weaver/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d747b780724c413b5075cdcd04f89094a38a2e4d87c4623ee60b24c0786b00f5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f736869626179616e2f6f70656e6170692d776561766572" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenApiWeaver&lt;/strong&gt; is an incremental Roslyn source generator that turns OpenAPI 3.x documents, including OpenAPI 3.2, into strongly typed C# HTTP clients at build time. No runtime code generation, no reflection - just plain C# emitted during compilation.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Start&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. Install the package&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight highlight-text-xml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&amp;lt;&lt;span class="pl-ent"&gt;ItemGroup&lt;/span&gt;&amp;gt;
  &amp;lt;&lt;span class="pl-ent"&gt;PackageReference&lt;/span&gt; &lt;span class="pl-e"&gt;Include&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;OpenApiWeaver&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;Version&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;x.y.z&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;PrivateAssets&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;all&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; /&amp;gt;
&amp;lt;/&lt;span class="pl-ent"&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. Add your OpenAPI document&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight highlight-text-xml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&amp;lt;&lt;span class="pl-ent"&gt;ItemGroup&lt;/span&gt;&amp;gt;
  &amp;lt;&lt;span class="pl-ent"&gt;OpenApiWeaverDocument&lt;/span&gt; &lt;span class="pl-e"&gt;Include&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;openapi\petstore.yaml&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
                         &lt;span class="pl-e"&gt;ClientName&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;PetstoreClient&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
                         &lt;span class="pl-e"&gt;Namespace&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Contoso.Generated&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; /&amp;gt;
&amp;lt;/&lt;span class="pl-ent"&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Use &lt;code&gt;OpenApiWeaverDocument&lt;/code&gt; rather than &lt;code&gt;AdditionalFiles&lt;/code&gt;; the package's MSBuild targets project these items into compiler inputs automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Use the generated client&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight highlight-source-cs notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;client&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;PetstoreClient&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;accessToken&lt;/span&gt;&lt;span class="pl-c1"&gt;:&lt;/span&gt; &lt;span class="pl-s"&gt;"your-token"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-c"&gt;// Operations are grouped by OpenAPI tag&lt;/span&gt;
&lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;pet&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;client&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-s1"&gt;Pets&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;GetAsync&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;petId&lt;/span&gt;&lt;span class="pl-c1"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/shibayan/openapi-weaver" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Why another OpenAPI client generator?
&lt;/h2&gt;

&lt;p&gt;If you've needed a C# client from an OpenAPI definition, you've probably reached for OpenAPI Generator, AutoRest, or NSwag. They all work, but each has its quirks, and I've often found myself compromising on one thing or another.&lt;/p&gt;

&lt;p&gt;What I really wanted was dead simple: &lt;strong&gt;drop an OpenAPI file in my project, and have the client appear seamlessly at build time&lt;/strong&gt; — no external tools, no checked-in generated files, no separate codegen step in CI. That's exactly what Source Generators are for, so I built &lt;strong&gt;OpenApiWeaver&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design goals
&lt;/h2&gt;

&lt;p&gt;The core idea is to push as much work as possible into compile time via Source Generators. This gives us two nice properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No runtime reflection&lt;/strong&gt; in the generated code itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type-safe output&lt;/strong&gt; that takes full advantage of modern C# features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the generated code leans on recent language features, consumers need &lt;strong&gt;.NET 8 or later&lt;/strong&gt;. In practice, this shouldn't be a real constraint for anyone today.&lt;/p&gt;

&lt;p&gt;There are plenty of similar libraries out there, so I put a lot of effort into the &lt;strong&gt;quality of the generated code&lt;/strong&gt;. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nullable reference types and &lt;code&gt;required&lt;/code&gt; modifiers are applied faithfully based on the OpenAPI schema&lt;/li&gt;
&lt;li&gt;Enums — particularly string-based ones — are generated in a form that's actually pleasant to use from C#&lt;/li&gt;
&lt;li&gt;XML doc comments are emitted from OpenAPI &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;summary&lt;/code&gt; fields, so IntelliSense just works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generated client code itself contains no reflection. However, because it relies on &lt;code&gt;System.Text.Json&lt;/code&gt;, reflection is still used internally there. That means &lt;strong&gt;NativeAOT is not supported yet&lt;/strong&gt; — but once the &lt;code&gt;System.Text.Json&lt;/code&gt; source generator can be composed cleanly with this one, it should be achievable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Usage is about as simple as I could make it: install the &lt;code&gt;OpenApiWeaver&lt;/code&gt; package from NuGet, and you're done. Once installed, you get a new MSBuild item type called &lt;code&gt;OpenApiWeaverDocument&lt;/code&gt;. Point its &lt;code&gt;Include&lt;/code&gt; attribute at your OpenAPI definition file:&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;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;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;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;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"OpenApiWeaver"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.0.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;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;OpenApiWeaverDocument&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"petstore.json"&lt;/span&gt; &lt;span class="na"&gt;ClientName=&lt;/span&gt;&lt;span class="s"&gt;"PetStoreClient"&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;&lt;code&gt;OpenApiWeaverDocument&lt;/code&gt; also accepts &lt;code&gt;ClientName&lt;/code&gt; and &lt;code&gt;Namespace&lt;/code&gt; attributes, which let you override the generated client's class name and namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get after a build
&lt;/h2&gt;

&lt;p&gt;That's the whole setup. Build the project, and the client is generated from the OpenAPI definition. For larger specs, the generator splits clients per OpenAPI &lt;strong&gt;Tag&lt;/strong&gt;, so you end up with one client class per logical grouping in your API — which keeps things manageable.&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%2Fnprp8sh7umc68e7ef5sr.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%2Fnprp8sh7umc68e7ef5sr.png" alt="Generated clients split per OpenAPI tag" width="438" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sample &lt;code&gt;petstore.json&lt;/code&gt; has &lt;code&gt;Pet&lt;/code&gt;, &lt;code&gt;Store&lt;/code&gt;, and &lt;code&gt;User&lt;/code&gt; tags, and you can see a client was generated for each.&lt;/p&gt;

&lt;p&gt;Each client exposes its operations as methods, so for the most part you can drive development purely through IntelliSense. (The naming of auto-generated methods still has room for improvement, I'll admit.)&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%2Fq1l5qbii2rvfskr5xsos.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%2Fq1l5qbii2rvfskr5xsos.png" alt="IntelliSense showing generated client methods" width="415" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the OpenAPI definition includes &lt;code&gt;description&lt;/code&gt; or &lt;code&gt;summary&lt;/code&gt; fields, those flow through to XML doc comments on the generated classes and methods, so you get nice tooltips in the editor:&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%2Fxgl8h4ud7jq9l5gynpae.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%2Fxgl8h4ud7jq9l5gynpae.png" alt="Tooltip showing XML doc comments from OpenAPI descriptions" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The generated type definitions
&lt;/h2&gt;

&lt;p&gt;Operations are relatively straightforward, but the type definitions generated from &lt;code&gt;Schemas&lt;/code&gt; are where I spent most of my effort. Take the &lt;code&gt;Pet&lt;/code&gt; class from the sample — here's what actually gets generated:&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;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Pet&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pet&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Id&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonIgnore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonIgnoreCondition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WhenWritingNull&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&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;long&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;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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Name&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&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;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;Name&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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Category&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonIgnore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonIgnoreCondition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WhenWritingNull&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"category"&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;Category&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="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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// PhotoUrls&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"photoUrls"&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="n"&gt;IReadOnlyList&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;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PhotoUrls&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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Tags&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonIgnore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonIgnoreCondition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WhenWritingNull&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tags"&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;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;Tags&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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Status&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;remarks&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// pet status in the store&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/remarks&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonIgnore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonIgnoreCondition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WhenWritingNull&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&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;Pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusEnum&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Status&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="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Pet.StatusEnum&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;remarks&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// pet status in the store&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/remarks&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonConverter&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;StatusEnumJsonConverter&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;readonly&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;StatusEnum&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;Value&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;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;StatusEnum&lt;/span&gt; &lt;span class="n"&gt;Available&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="s"&gt;"available"&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;readonly&lt;/span&gt; &lt;span class="n"&gt;StatusEnum&lt;/span&gt; &lt;span class="n"&gt;Pending&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="s"&gt;"pending"&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;readonly&lt;/span&gt; &lt;span class="n"&gt;StatusEnum&lt;/span&gt; &lt;span class="n"&gt;Sold&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="s"&gt;"sold"&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="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StatusEnumJsonConverter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JsonConverter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StatusEnum&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;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;StatusEnum&lt;/span&gt; &lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;Utf8JsonReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;typeToConvert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerOptions&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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StatusEnum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&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;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Utf8JsonWriter&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StatusEnum&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;JsonSerializerOptions&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="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteStringValue&lt;/span&gt;&lt;span class="p"&gt;(&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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pointing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NRT and &lt;code&gt;required&lt;/code&gt; are applied correctly, so mistakes get caught at compile time&lt;/li&gt;
&lt;li&gt;String-based enums are &lt;strong&gt;not&lt;/strong&gt; mapped to C# &lt;code&gt;enum&lt;/code&gt;. Instead they become &lt;code&gt;readonly record struct&lt;/code&gt;, which lets you treat them as strings while still keeping named well-known values like &lt;code&gt;Available&lt;/code&gt;, &lt;code&gt;Pending&lt;/code&gt;, &lt;code&gt;Sold&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This string-enum pattern has become pretty common in modern .NET libraries, and I think it strikes a good balance between type safety and the reality that string enums in OpenAPI often need to tolerate unknown values.&lt;/p&gt;

&lt;p&gt;There are a few other niceties I won't cover here — the full documentation goes into more detail:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://shibayan.github.io/openapi-weaver/" rel="noopener noreferrer"&gt;OpenApiWeaver documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on the OpenAPI parser
&lt;/h2&gt;

&lt;p&gt;OpenApiWeaver only owns the "generate a client from an already-parsed OpenAPI document" half of the problem. Parsing itself is delegated to &lt;strong&gt;&lt;code&gt;Microsoft.OpenApi&lt;/code&gt;&lt;/strong&gt;, which keeps the surface area small. The docs say OpenAPI 3.x is supported, but really it's whatever that library supports:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://devblogs.microsoft.com/openapi/openapi-net-release-announcements/" rel="noopener noreferrer"&gt;OpenAPI.NET release announcements (Microsoft DevBlogs)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing note: this was built with AI coding tools
&lt;/h2&gt;

&lt;p&gt;One last thing worth sharing. This entire library — Source Generator, runtime pieces, and documentation — was built heavily with AI coding assistance. I did the PoC in Codex with GPT-5.4, then moved to GitHub Copilot Chat using GPT-5.4 and Opus 4.6 High for the actual implementation.&lt;/p&gt;

&lt;p&gt;Honestly, I've always considered hand-writing Source Generator code to be a painful endeavor, and I'd been putting off projects like this for exactly that reason. Leaning on AI coding tools let me focus on &lt;strong&gt;design decisions&lt;/strong&gt; — API shape, code quality goals, naming — while the assistant handled most of the mechanical work of emitting C# source.&lt;/p&gt;

&lt;p&gt;From PoC to release, including docs, it took about &lt;strong&gt;three days&lt;/strong&gt; of real work. That still surprises me.&lt;/p&gt;

&lt;p&gt;If you try OpenApiWeaver out, I'd love to hear what you think — issues and PRs welcome on the repo.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>openapi</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Built a Lightweight GitHub Action for Deploying to Azure Static Web Apps</title>
      <dc:creator>Tatsuro Shibamura</dc:creator>
      <pubDate>Mon, 06 Apr 2026 06:17:15 +0000</pubDate>
      <link>https://dev.to/polymind/built-a-lightweight-github-action-for-deploying-to-azure-static-web-apps-1ame</link>
      <guid>https://dev.to/polymind/built-a-lightweight-github-action-for-deploying-to-azure-static-web-apps-1ame</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I created &lt;a href="https://github.com/marketplace/actions/deploy-azure-static-web-apps" rel="noopener noreferrer"&gt;&lt;code&gt;shibayan/swa-deploy&lt;/code&gt;&lt;/a&gt; — a lightweight GitHub Action that &lt;strong&gt;only deploys&lt;/strong&gt; to Azure Static Web Apps, without the Docker-based build overhead of the official action. It wraps the same &lt;code&gt;StaticSitesClient&lt;/code&gt; that SWA CLI uses internally, includes automatic caching, and supports both Deployment Token and &lt;code&gt;azure/login&lt;/code&gt; authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with the Official Action
&lt;/h2&gt;

&lt;p&gt;When deploying static sites (built with Astro, Vite, etc.) to Azure Static Web Apps, the standard approach is to use the official &lt;a href="https://github.com/marketplace/actions/azure-static-web-apps-deploy" rel="noopener noreferrer"&gt;&lt;code&gt;Azure/static-web-apps-deploy&lt;/code&gt;&lt;/a&gt; action that gets auto-generated when you link a GitHub repo to your SWA resource.&lt;/p&gt;

&lt;p&gt;Unlike other Azure deployment actions (e.g., for App Service or Azure Functions), this action uses &lt;strong&gt;Oryx&lt;/strong&gt; — the build engine used across Azure App Service — to build your application internally. It runs inside Docker, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The build process is a black box with hidden behaviors&lt;/li&gt;
&lt;li&gt;Docker image creation adds significant overhead&lt;/li&gt;
&lt;li&gt;Customizing the build pipeline is limited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While Oryx is a capable multi-platform build engine, the Docker-based approach makes deploys slower than they need to be, especially when you just want to push pre-built static files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;skip_app_build&lt;/code&gt; Option Isn't Enough
&lt;/h2&gt;

&lt;p&gt;You can set &lt;code&gt;skip_app_build: true&lt;/code&gt; in the official action to skip the application build step. This lets you build in GitHub Actions and only deploy through the action. However, it still needs to pull/build the Docker image, so the overhead remains.&lt;/p&gt;

&lt;p&gt;For reference, the official documentation covers this configuration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/static-web-apps/build-configuration?tabs=aat&amp;amp;pivots=github-actions" rel="noopener noreferrer"&gt;Build configuration for Azure Static Web Apps | Microsoft Learn&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using SWA CLI: Better, But Not Ideal
&lt;/h2&gt;

&lt;p&gt;To avoid the Docker overhead, I switched to using the &lt;a href="https://learn.microsoft.com/en-us/azure/static-web-apps/static-web-apps-cli-deploy" rel="noopener noreferrer"&gt;Static Web Apps CLI&lt;/a&gt; for deploy-only workflows. This approach works well — it's lighter than Docker — but comes with its own trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Installation time&lt;/strong&gt;: &lt;code&gt;npm install -g @azure/static-web-apps-cli&lt;/code&gt; takes a while&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache complexity&lt;/strong&gt;: To speed things up, you need to add npm global cache steps, which adds more workflow YAML than the actual deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cognitive overhead&lt;/strong&gt;: You end up thinking more about caching npm packages than deploying your app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: &lt;code&gt;shibayan/swa-deploy&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I finally decided to build a dedicated GitHub Action that does one thing well: &lt;strong&gt;deploy to Azure Static Web Apps&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/shibayan" rel="noopener noreferrer"&gt;
        shibayan
      &lt;/a&gt; / &lt;a href="https://github.com/shibayan/swa-deploy" rel="noopener noreferrer"&gt;
        swa-deploy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A GitHub Action to deploy prebuilt frontend assets and Azure Functions APIs to Azure Static Web Apps
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Deploy Azure Static Web Apps&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/shibayan/swa-deploy/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/shibayan/swa-deploy/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/shibayan/swa-deploy/./badges/coverage.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fshibayan%2Fswa-deploy%2FHEAD%2F.%2Fbadges%2Fcoverage.svg" alt="Coverage"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A GitHub Action that deploys prebuilt frontend assets, Azure Functions APIs, and
&lt;code&gt;staticwebapp.config.json&lt;/code&gt; to
&lt;a href="https://learn.microsoft.com/azure/static-web-apps/" rel="nofollow noopener noreferrer"&gt;Azure Static Web Apps&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It follows the same deployment model as &lt;code&gt;swa deploy&lt;/code&gt; from the
&lt;a href="https://azure.github.io/static-web-apps-cli/" rel="nofollow noopener noreferrer"&gt;Azure Static Web Apps CLI&lt;/a&gt; —
download the &lt;code&gt;StaticSitesClient&lt;/code&gt; binary, resolve paths, and upload content using
a deployment token. When &lt;code&gt;deployment-token&lt;/code&gt; is omitted, this action can also
resolve the token at runtime through Azure Resource Manager after &lt;code&gt;azure/login&lt;/code&gt;
The binary is cached automatically across workflow runs.&lt;/p&gt;
&lt;div class="markdown-alert markdown-alert-note"&gt;
&lt;p class="markdown-alert-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This action &lt;strong&gt;does not build&lt;/strong&gt; your application. Run your build step before
calling this action.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Deploy a built frontend&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-yaml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Deploy&lt;/span&gt;
&lt;span class="pl-ent"&gt;on&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;push&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;branches&lt;/span&gt;: &lt;span class="pl-s"&gt;[master]&lt;/span&gt;

&lt;span class="pl-ent"&gt;jobs&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;deploy&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:
      - &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/checkout@v4&lt;/span&gt;

      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Build&lt;/span&gt;
        &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/span&gt;

      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Deploy to Azure Static Web Apps&lt;/span&gt;
        &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;deploy&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/shibayan/swa-deploy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Under the hood, this action uses the same &lt;code&gt;StaticSitesClient&lt;/code&gt; binary that SWA CLI uses. It's essentially a thin wrapper around that CLI with automatic caching built in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using a Deployment Token
&lt;/h3&gt;

&lt;p&gt;The simplest approach — just pass your deployment 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="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;Deploy to Azure Static Web Apps&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&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;shibayan/swa-deploy@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-location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
    &lt;span class="na"&gt;deployment-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using &lt;code&gt;azure/login&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you prefer using &lt;code&gt;azure/login&lt;/code&gt; with a service principal or federated credentials, pass &lt;code&gt;app-name&lt;/code&gt; instead. The action will automatically look up the resource via the Azure Resource Manager API, retrieve the deployment token, and deploy:&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;Azure login&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Azure Static Web Apps&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;shibayan/swa-deploy@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-location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&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;my-static-web-app&lt;/span&gt;
    &lt;span class="na"&gt;resource-group-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-resource-group&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;resource-group-name&lt;/code&gt; parameter is optional. You only need it if you have multiple SWA resources with the same name across different resource groups in your subscription — since SWA names are unique at the resource group scope, not the subscription scope.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Use This?
&lt;/h2&gt;

&lt;p&gt;Compared to the official action and SWA CLI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Docker overhead&lt;/strong&gt; — no image build step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic caching&lt;/strong&gt; — the &lt;code&gt;StaticSitesClient&lt;/code&gt; binary is cached automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple workflow YAML&lt;/strong&gt; — no npm cache boilerplate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt; — does exactly one thing: deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've migrated all of my SWA deployments to this action and it's been working great.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Note
&lt;/h2&gt;

&lt;p&gt;The entire action was built using GitHub Copilot Chat with GPT-5.4 and Claude Opus 4.6, starting from the official template. AI-assisted development made it straightforward to implement and test.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>github</category>
      <category>actions</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Deep Dive into Azure Cosmos DB Physical Partitions</title>
      <dc:creator>Tatsuro Shibamura</dc:creator>
      <pubDate>Tue, 31 Mar 2026 14:21:33 +0000</pubDate>
      <link>https://dev.to/polymind/deep-dive-into-azure-cosmos-db-physical-partitions-49bb</link>
      <guid>https://dev.to/polymind/deep-dive-into-azure-cosmos-db-physical-partitions-49bb</guid>
      <description>&lt;p&gt;When using Azure Cosmos DB effectively, the most important aspect is designing the partition key. However, when we talk about “partitions” in Cosmos DB, there are actually two types: &lt;strong&gt;logical partitions&lt;/strong&gt; and &lt;strong&gt;physical partitions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This topic is briefly covered in the official documentation, so it’s a good idea to review it first. In most cases, physical partitions are fully managed by Azure, so you rarely need to think about them.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#physical-partitions" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#physical-partitions" rel="noopener noreferrer" class="c-link"&gt;
            Partitioning and horizontal scaling - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn about partitioning, logical, physical partitions in Azure Cosmos DB, best practices when choosing a partition key, and how to manage logical partitions.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;When designing your data model, the partition key corresponds to logical partitions. Both logical and physical partitions have their own limits, but in most cases, you only need to care about the &lt;strong&gt;20 GB limit of logical partitions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Although you usually don’t need to think about physical partitions, understanding them can be very helpful during design. So here’s a concise summary based on my experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Basics of Physical Partitions
&lt;/h2&gt;

&lt;p&gt;As described in the official documentation, physical partitions are part of Cosmos DB’s internal implementation and represent the &lt;strong&gt;unit of compute that processes queries and operations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each physical partition has &lt;strong&gt;four replicas&lt;/strong&gt;, and quorum-based consistency is implemented across them.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/global-dist-under-the-hood" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/global-dist-under-the-hood" rel="noopener noreferrer" class="c-link"&gt;
            Global Distribution With - Under the Hood - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            This article provides technical details relating to global distribution of Azure Cosmos DB
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;In globally distributed configurations, replication is performed at the &lt;strong&gt;physical partition level&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A container always has at least one physical partition. However, Azure automatically increases the number of physical partitions when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storage exceeds &lt;strong&gt;50 GB&lt;/strong&gt;, or&lt;/li&gt;
&lt;li&gt;Provisioned throughput exceeds &lt;strong&gt;10,000 RU/s&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Throughput and Physical Partitions
&lt;/h2&gt;

&lt;p&gt;Each physical partition has a maximum throughput of &lt;strong&gt;10,000 RU/s&lt;/strong&gt;. In practice, however, I’ve found that hitting the &lt;strong&gt;50 GB storage limit&lt;/strong&gt; happens more often than hitting the RU limit.&lt;/p&gt;

&lt;p&gt;A common issue occurs when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A container originally has &lt;strong&gt;4000 RU/s&lt;/strong&gt; on a single physical partition&lt;/li&gt;
&lt;li&gt;Storage exceeds 50 GB&lt;/li&gt;
&lt;li&gt;The system splits into &lt;strong&gt;2 physical partitions&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since RU is evenly distributed across physical partitions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 partition → 4000 RU&lt;/li&gt;
&lt;li&gt;2 partitions → 2000 RU each&lt;/li&gt;
&lt;li&gt;4 partitions → 1000 RU each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This effectively &lt;strong&gt;reduces the throughput available per partition&lt;/strong&gt;, which can lead to sudden &lt;strong&gt;429 Too Many Requests&lt;/strong&gt; errors.&lt;/p&gt;

&lt;p&gt;In such cases, you should follow the official guidance and &lt;strong&gt;adjust RU accordingly&lt;/strong&gt;:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/scaling-provisioned-throughput-best-practices" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/scaling-provisioned-throughput-best-practices" rel="noopener noreferrer" class="c-link"&gt;
            Best Practices for Scaling Provisioned Throughput - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn about best practices for scaling manual and autoscale provisioned throughput for databases and containers.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you already know your data will exceed 50 GB, it can be a good strategy to &lt;strong&gt;provision enough RU upfront&lt;/strong&gt; so that the required number of physical partitions are allocated early.&lt;/p&gt;

&lt;p&gt;Also note that when new physical partitions are created, data replication occurs, temporarily consuming additional RU. This can lead to &lt;strong&gt;temporary throughput degradation&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connection Modes and Physical Partitions
&lt;/h2&gt;

&lt;p&gt;To improve performance, the C# and Java SDKs recommend using &lt;strong&gt;Direct mode&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/sql/sql-sdk-connection-modes" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/sql/sql-sdk-connection-modes" rel="noopener noreferrer" class="c-link"&gt;
            SQL SDK Connectivity Modes - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn about the different connectivity modes available on the Azure Cosmos DB SQL SDKs.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;As the name suggests, Direct mode connects directly to backend physical partitions, reducing overhead.&lt;/p&gt;

&lt;p&gt;To make this work, the SDK must know &lt;strong&gt;which physical partition stores a given partition key&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve enabled Application Insights, you may have seen requests to &lt;code&gt;pkranges&lt;/code&gt; during application startup. This API returns the mapping between partition keys and physical partitions:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/rest/api/cosmos-db/get-partition-key-ranges" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/rest/api/cosmos-db/get-partition-key-ranges" rel="noopener noreferrer" class="c-link"&gt;
            Get Partition Key Ranges - Azure Cosmos DB REST API | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Get partition key ranges REST API syntax. Request and response headers, body, status codes and examples.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;This API is not intended for direct use in user code.&lt;/p&gt;

&lt;p&gt;Because this mapping is fetched when a container is first accessed, the &lt;strong&gt;first request can be slower&lt;/strong&gt;. However, recent versions of the .NET SDK provide an API to explicitly initialize this mapping:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.cosmosclient.createandinitializeasync?view=azure-dotnet" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.cosmosclient.createandinitializeasync?view=azure-dotnet" rel="noopener noreferrer" class="c-link"&gt;
            CosmosClient.CreateAndInitializeAsync Method (Microsoft.Azure.Cosmos) - Azure for .NET Developers | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Creates a new CosmosClient with the account endpoint URI string and TokenCredential. In addition to that it initializes the client with containers provided i.e The SDK warms up the caches and connections before the first call to the service is made. Use this to obtain lower latency while startup of your application. CosmosClient is thread-safe. Its recommended to maintain a single instance of CosmosClient per lifetime of the application which enables efficient connection management and performance. Please refer to the performance guide. 
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;By calling this during application warm-up, you can &lt;strong&gt;minimize overhead for subsequent requests&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-Partition Queries and Physical Partitions
&lt;/h2&gt;

&lt;p&gt;In Cosmos DB design, &lt;strong&gt;cross-partition queries should generally be avoided&lt;/strong&gt;. However, this rule can be relaxed if all data fits within a single physical partition.&lt;/p&gt;

&lt;p&gt;In the following documentation, the term “partition” refers to &lt;strong&gt;physical partitions&lt;/strong&gt;:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-query-container" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-query-container" rel="noopener noreferrer" class="c-link"&gt;
            Query a Container - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn how to query containers in Azure Cosmos DB by using both in-partition and cross-partition queries.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If all logical partitions are contained within a single physical partition, indexes can be used effectively, making cross-partition queries relatively low-cost.&lt;/p&gt;

&lt;p&gt;Problems arise when a container spans multiple physical partitions, as queries must be executed across all of them, increasing cost.&lt;/p&gt;

&lt;p&gt;The most efficient cross-partition query is one that targets &lt;strong&gt;only partition keys within a single physical partition&lt;/strong&gt;, but this is not something you can control directly from user code.&lt;/p&gt;

&lt;p&gt;Instead, you should rely on SDK features. A good example is the &lt;code&gt;ReadMany&lt;/code&gt; API, which efficiently handles cross-partition access.&lt;/p&gt;

&lt;p&gt;That said, designing everything around cross-partition queries should always be avoided.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change Feed and Physical Partitions
&lt;/h2&gt;

&lt;p&gt;Physical partitions are closely related to the &lt;strong&gt;Change Feed&lt;/strong&gt; feature.&lt;/p&gt;

&lt;p&gt;While Change Feed guarantees ordering at the logical partition level, its actual processing is based on &lt;strong&gt;physical partitions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is easier to understand when looking at the Pull model.&lt;/p&gt;

&lt;p&gt;In the Pull model, parallel processing is achieved using the &lt;code&gt;FeedRange&lt;/code&gt; class, which is assigned per physical partition. By distributing &lt;code&gt;FeedRange&lt;/code&gt; across multiple machines, you can process Change Feed in parallel.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/change-feed-pull-model?tabs=dotnet" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/change-feed-pull-model?tabs=dotnet" rel="noopener noreferrer" class="c-link"&gt;
            Change Feed Pull Model - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn how to use the Azure Cosmos DB change feed pull model to read the change feed. Understand the differences between the change feed pull model and the change feed processor.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.feedrange?view=azure-dotnet" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.feedrange?view=azure-dotnet" rel="noopener noreferrer" class="c-link"&gt;
            FeedRange Class (Microsoft.Azure.Cosmos) - Azure for .NET Developers | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Represents a unit of feed consumption that can be used as unit of parallelism. 
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;As a result, even a single Change Feed Processor can only scale up to the number of physical partitions. In other words, processing is effectively &lt;strong&gt;single-threaded per physical partition&lt;/strong&gt;, which simplifies writing logic that depends on ordering.&lt;/p&gt;

&lt;p&gt;However, this also means that &lt;strong&gt;scale-out limits are reached relatively quickly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To address this, you should minimize processing within each Change Feed Processor and instead create &lt;strong&gt;multiple smaller processors for different purposes&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Metrics and Physical Partitions
&lt;/h2&gt;

&lt;p&gt;By now, you should have a solid understanding of physical partitions, so let’s move on to monitoring.&lt;/p&gt;

&lt;p&gt;Even with careful partition key design, data can still become skewed toward specific physical partitions (so-called &lt;strong&gt;hot partitions&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;To identify them, you can split metrics by &lt;code&gt;PartitionKeyRangeId&lt;/code&gt;.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/monitor-normalized-request-units#how-to-monitor-for-hot-partitions" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/monitor-normalized-request-units#how-to-monitor-for-hot-partitions" rel="noopener noreferrer" class="c-link"&gt;
            Monitor Normalized Request Units - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn how to monitor the normalized request unit usage of an operation in Azure Cosmos DB. Owners of an Azure Cosmos DB account can understand which operations are consuming more request units.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Recently, new metrics such as &lt;strong&gt;Physical Partition Throughput&lt;/strong&gt; have been introduced, making it easier to troubleshoot partition-level issues.&lt;/p&gt;

&lt;p&gt;You can also view these metrics in &lt;strong&gt;Cosmos DB Insights in Azure Monitor&lt;/strong&gt;, which significantly improves observability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Operations on Physical Partitions (Preview)
&lt;/h2&gt;

&lt;p&gt;At Build 2022, several Cosmos DB updates were announced, including preview features that allow &lt;strong&gt;direct operations on physical partitions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One of them is &lt;strong&gt;Merge partitions&lt;/strong&gt;, which combines physical partitions.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/merge" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/merge" rel="noopener noreferrer" class="c-link"&gt;
            Merge partitions (preview) - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Reduce the number of physical partitions used for your container with the merge capability in Azure Cosmos DB.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Since RU is evenly distributed across physical partitions, fewer partitions can result in &lt;strong&gt;higher effective throughput per partition&lt;/strong&gt;. Merging excess partitions helps optimize performance.&lt;/p&gt;

&lt;p&gt;Another feature allows &lt;strong&gt;redistributing throughput across physical partitions&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/sql/distribute-throughput-across-partitions" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/sql/distribute-throughput-across-partitions" rel="noopener noreferrer" class="c-link"&gt;
            Redistribute Throughput Across Partitions (Preview) - Azure Cosmos DB | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn how to redistribute throughput across partitions in Azure Cosmos DB to optimize performance. To improve partition performance, follow these step-by-step instructions and best practices.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;No matter how carefully you design your partition key, data skew is inevitable in real-world scenarios. This feature allows you to assign more RU to hot partitions without increasing total RU, improving overall efficiency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By understanding physical partitions, you can push Cosmos DB optimization further in terms of both performance and cost.&lt;/p&gt;

&lt;p&gt;However, this is &lt;strong&gt;not mandatory knowledge&lt;/strong&gt; for most use cases.&lt;/p&gt;

&lt;p&gt;Rather than focusing on physical partitions from the beginning, the most important thing is to &lt;strong&gt;design a good partition key&lt;/strong&gt; during data modeling.&lt;/p&gt;

&lt;p&gt;Most physical partition-related optimizations can be handled later, so invest your effort where it matters most: &lt;strong&gt;your initial data model design&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cosmosdb</category>
    </item>
    <item>
      <title>Migrating Azure Functions Monitoring to OpenTelemetry</title>
      <dc:creator>Tatsuro Shibamura</dc:creator>
      <pubDate>Tue, 31 Mar 2026 14:05:40 +0000</pubDate>
      <link>https://dev.to/polymind/migrating-azure-functions-monitoring-to-opentelemetry-17o</link>
      <guid>https://dev.to/polymind/migrating-azure-functions-monitoring-to-opentelemetry-17o</guid>
      <description>&lt;p&gt;A while ago, version 3.0 of the Application Insights SDK was released, and its internal implementation was migrated to be based on OpenTelemetry. While it is generally recommended to use the OpenTelemetry Distro directly, being able to support OpenTelemetry just by updating the Application Insights SDK is a clear advantage.&lt;/p&gt;

&lt;p&gt;Along with this change, one of the Application Insights SDKs used in Azure Functions .NET Isolated—&lt;code&gt;Microsoft.ApplicationInsights.WorkerService&lt;/code&gt;—has also been updated to version 3.0. If you are managing both Azure Functions and ASP.NET Core projects in a single solution, you may often update them together.&lt;/p&gt;

&lt;p&gt;In a typical .NET Worker application, this update does not cause any issues. However, in Azure Functions, simply updating the package leads to runtime exceptions like the one below. Since the build succeeds without errors, this can be particularly tricky to notice.&lt;/p&gt;

&lt;p&gt;This issue occurs because the &lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt; package—required for Azure Functions' built-in Application Insights integration—has a strong dependency on the v2 SDK. While this would be resolved if the package were updated for v3, it appears from GitHub issues that there are no plans to provide a v3-compatible version. Therefore, in the long term, migrating to an OpenTelemetry-based approach will be necessary.&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/Azure/azure-functions-dotnet-worker/issues/3322#issuecomment-4050282773" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Comment for
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#3322&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/RohitRanjanMS" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F90008725%3Fu%3Dcef322ae933f4fb1cb94f6a94e20ae5f1a996ee9%26v%3D4" alt="RohitRanjanMS avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/RohitRanjanMS" rel="noopener noreferrer"&gt;RohitRanjanMS&lt;/a&gt;
        &lt;/strong&gt; commented on &lt;a href="https://github.com/Azure/azure-functions-dotnet-worker/issues/3322#issuecomment-4050282773" rel="noopener noreferrer"&gt;&lt;time&gt;Mar 12, 2026&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;We do not plan to release a new version of &lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt; that is compatible with the 3.0 release of &lt;code&gt;Microsoft.ApplicationInsights.WorkerService&lt;/code&gt;. Our current package relies heavily on Telemetry Initializers and Telemetry Processors, and the new 3.0 SDK no longer supports these extensibility points. Supporting it would require a full rewrite of the Functions worker package. The same limitation may also affect customers who depend on Initializers or Processors in their applications.
Our recommendation is to move to OpenTelemetry‑based monitoring. This is the direction we are investing in, and we are committed to improving observability by aligning with the OpenTelemetry ecosystem. You can follow Functions OpenTelemetry guidance here:
&lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/opentelemetry-howto?tabs=app-insights%2Cihostapplicationbuilder%2Cmaven&amp;amp;pivots=programming-language-csharp" rel="nofollow noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/azure-functions/opentelemetry-howto?tabs=app-insights%2Cihostapplicationbuilder%2Cmaven&amp;amp;pivots=programming-language-csharp&lt;/a&gt;&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Azure/azure-functions-dotnet-worker/issues/3322#issuecomment-4050282773" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Azure Functions now supports OpenTelemetry, but if you create a project from the default template, it still uses the Application Insights SDK. Therefore, some additional steps are required to switch.&lt;/p&gt;

&lt;p&gt;You can follow the official documentation below for the migration steps, but there are a few pitfalls I encountered during the process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/opentelemetry-howto" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/azure-functions/opentelemetry-howto&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing Application Insights Packages
&lt;/h2&gt;

&lt;p&gt;First, check your existing Azure Functions project. You will likely find the following two Application Insights-related packages installed. Remove them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Microsoft.ApplicationInsights.WorkerService&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These packages depend on the Application Insights SDK and are no longer needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding OpenTelemetry Packages
&lt;/h2&gt;

&lt;p&gt;Instead, install the following OpenTelemetry-related packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Microsoft.Azure.Functions.Worker.OpenTelemetry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Azure.Monitor.OpenTelemetry.Exporter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OpenTelemetry.Extensions.Hosting&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In ASP.NET Core, a single package is often sufficient, but in Azure Functions, you need to install them individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating Program.cs
&lt;/h2&gt;

&lt;p&gt;At this point, your project will no longer build. Remove the existing Application Insights initialization code in &lt;code&gt;Program.cs&lt;/code&gt; and replace it with the following:&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.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="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="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="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 need customization, you can pass options to &lt;code&gt;UseAzureMonitorExporter&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Local Development Pitfall
&lt;/h2&gt;

&lt;p&gt;After this change, the project builds successfully. However, when running locally, you may encounter an error indicating that the Application Insights connection string is missing, causing the application to fail at startup.&lt;/p&gt;

&lt;p&gt;With the Application Insights SDK, telemetry was automatically disabled if no connection string was provided. However, with the OpenTelemetry Distro, it is now required.&lt;/p&gt;

&lt;p&gt;To avoid forcing Application Insights in local development, you could disable it using preprocessor directives. However, I personally found that approach messy.&lt;/p&gt;

&lt;p&gt;Instead, I added a dummy connection string like this:&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;"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=00000000-0000-0000-0000-000000000000"&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;This allows the application to start correctly in a local development environment.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verifying Telemetry
&lt;/h2&gt;

&lt;p&gt;Finally, connect to Application Insights and verify that telemetry is being sent correctly.&lt;/p&gt;

&lt;p&gt;You will notice that property names follow the OpenTelemetry schema. However, telemetry generated by the Azure Functions host remains unchanged.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real-World Example
&lt;/h2&gt;

&lt;p&gt;Here is the Pull Request where I migrated Acmebot to an OpenTelemetry-based setup:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/polymind-inc/acmebot/pull/996" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Add OpenTelemetry support and remove Application Insights initializer
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#996&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/shibayan" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F1356444%3Fv%3D4" alt="shibayan avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/shibayan" rel="noopener noreferrer"&gt;shibayan&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/polymind-inc/acmebot/pull/996" rel="noopener noreferrer"&gt;&lt;time&gt;Mar 20, 2026&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;This pull request updates the application's telemetry and monitoring stack by migrating from Application Insights to OpenTelemetry with Azure Monitor Exporter. This modernizes observability, aligns with current best practices, and simplifies related configuration and code. The most important changes are grouped below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Migration to OpenTelemetry and Azure Monitor Exporter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Removed dependencies on &lt;code&gt;Microsoft.ApplicationInsights.WorkerService&lt;/code&gt; and &lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt;, and added new dependencies for &lt;code&gt;Azure.Monitor.OpenTelemetry.Exporter&lt;/code&gt;, &lt;code&gt;Microsoft.Azure.Functions.Worker.OpenTelemetry&lt;/code&gt;, and &lt;code&gt;OpenTelemetry.Extensions.Hosting&lt;/code&gt; in &lt;code&gt;Acmebot.App.csproj&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Replaced Application Insights telemetry setup in &lt;code&gt;Program.cs&lt;/code&gt; with OpenTelemetry initialization using &lt;code&gt;AddOpenTelemetry()&lt;/code&gt;, &lt;code&gt;UseFunctionsWorkerDefaults()&lt;/code&gt;, and &lt;code&gt;UseAzureMonitorExporter()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Updated &lt;code&gt;host.json&lt;/code&gt; to set &lt;code&gt;"telemetryMode": "OpenTelemetry"&lt;/code&gt; and removed Application Insights-specific logging configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Codebase cleanup:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Removed the custom &lt;code&gt;ApplicationVersionInitializer&lt;/code&gt; class and its registration, as it was specific to Application Insights. [1] [2]
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;General code and dependency updates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Updated &lt;code&gt;using&lt;/code&gt; statements in &lt;code&gt;Program.cs&lt;/code&gt; to remove Application Insights references and add OpenTelemetry/Azure Monitor references as needed.&lt;/li&gt;
&lt;/ul&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/polymind-inc/acmebot/pull/996" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;The changes are relatively simple—for example, removing &lt;code&gt;ITelemetryInitializer&lt;/code&gt;. Equivalent functionality is now built in, so it is no longer necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Migrating applications that rely heavily on &lt;code&gt;ITelemetryProcessor&lt;/code&gt; or other advanced customizations may require more effort. However, it seems feasible to migrate even filtering logic to OpenTelemetry.&lt;/p&gt;

&lt;p&gt;If I get the time, I plan to write another post covering those advanced scenarios.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>dotnet</category>
      <category>opentelemetry</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
