<?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: dotnet</title>
    <description>The latest articles tagged 'dotnet' on DEV Community.</description>
    <link>https://dev.to/t/dotnet</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/dotnet"/>
    <language>en</language>
    <item>
      <title>MCP (Model Context Protocol) for .NET Devs — What It Is and Why You'll Be Using It Soon</title>
      <dc:creator>Vikrant Bagal</dc:creator>
      <pubDate>Fri, 15 May 2026 19:02:52 +0000</pubDate>
      <link>https://dev.to/vikrant_bagal_afae3e25ca7/mcp-model-context-protocol-for-net-devs-what-it-is-and-why-youll-be-using-it-soon-a98</link>
      <guid>https://dev.to/vikrant_bagal_afae3e25ca7/mcp-model-context-protocol-for-net-devs-what-it-is-and-why-youll-be-using-it-soon-a98</guid>
      <description>&lt;p&gt;Every major AI tool is adopting MCP. If you build .NET APIs, this is the integration pattern coming your way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is MCP?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt; is an open-standard protocol designed to standardize integrations between AI applications and external tools and data sources. Think of it as the universal language that allows AI assistants to safely and securely connect to your .NET applications, databases, and APIs.&lt;/p&gt;

&lt;p&gt;Originally developed by Anthropic in partnership with Microsoft, MCP provides a structured way for AI systems to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discover capabilities&lt;/strong&gt; across different systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute actions&lt;/strong&gt; through external tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange structured context&lt;/strong&gt; without custom integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protocol is built on JSON-RPC 2.0, ensuring interoperability across platforms and tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why MCP Matters for .NET Developers
&lt;/h2&gt;

&lt;p&gt;If you're building APIs, microservices, or AI-powered applications with .NET, MCP is becoming the de facto standard for AI-tool communication. Here's why:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Microsoft's Full Backing
&lt;/h3&gt;

&lt;p&gt;Microsoft is actively supporting MCP across its ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copilot Studio&lt;/strong&gt; now supports MCP integrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Studio Code&lt;/strong&gt; with GitHub Copilot agent mode uses MCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent Framework&lt;/strong&gt; integrates with MCP servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Functions&lt;/strong&gt; supports remote MCP servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means your .NET applications can now communicate with AI agents in a standardized way.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Standardized AI Communication
&lt;/h3&gt;

&lt;p&gt;Before MCP, every AI tool required custom integration code. MCP eliminates this by providing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unified protocol&lt;/strong&gt; for all AI tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security-first design&lt;/strong&gt; with explicit user consent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable architecture&lt;/strong&gt; allowing multiple servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive enhancement&lt;/strong&gt; for adding new features&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Production-Ready .NET SDK
&lt;/h3&gt;

&lt;p&gt;The official MCP C# SDK is available through NuGet and maintained by Microsoft in collaboration with Anthropic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package ModelContextProtocol &lt;span class="nt"&gt;--prerelease&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you everything you need to create MCP clients and servers in .NET.&lt;/p&gt;




&lt;h2&gt;
  
  
  The MCP Architecture
&lt;/h2&gt;

&lt;p&gt;MCP follows a client-host-server architecture that's familiar to .NET developers:&lt;/p&gt;

&lt;h3&gt;
  
  
  Host
&lt;/h3&gt;

&lt;p&gt;The host is your AI application (like GitHub Copilot in VS Code). It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates and manages client instances&lt;/li&gt;
&lt;li&gt;Handles security policies and user consent&lt;/li&gt;
&lt;li&gt;Coordinates AI integration and sampling&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Client
&lt;/h3&gt;

&lt;p&gt;The client connects to MCP servers. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Establishes stateful sessions per server&lt;/li&gt;
&lt;li&gt;Handles protocol negotiation&lt;/li&gt;
&lt;li&gt;Routes messages between host and server&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Server
&lt;/h3&gt;

&lt;p&gt;The server exposes your .NET application's capabilities. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provides tools, resources, and prompts&lt;/li&gt;
&lt;li&gt;Operates independently with focused responsibilities&lt;/li&gt;
&lt;li&gt;Can be local processes or remote services&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Building Your First MCP Server in .NET
&lt;/h2&gt;

&lt;p&gt;Creating an MCP server is surprisingly straightforward. Here's a basic stdio-based server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ModelContextProtocol.Server&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;System.ComponentModel&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;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateApplicationBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddConsole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consoleLogOptions&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;consoleLogOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogToStandardErrorThreshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcpServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithStdioServerTransport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithToolsFromAssembly&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;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;RunAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&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;class&lt;/span&gt; &lt;span class="nc"&gt;EchoTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Echoes the message back to the client."&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="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"Hello from C#: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What's happening here?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Host.CreateApplicationBuilder&lt;/strong&gt; - Sets up dependency injection and hosting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AddMcpServer()&lt;/strong&gt; - Registers the MCP server with DI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WithStdioServerTransport()&lt;/strong&gt; - Configures stdio communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WithToolsFromAssembly()&lt;/strong&gt; - Auto-discovers tools marked with attributes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;McpServerToolType&lt;/strong&gt; - Marks a class as containing MCP tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;McpServerTool&lt;/strong&gt; - Marks a method as an MCP-accessible tool&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;Description&lt;/code&gt; attribute becomes part of the tool's metadata, helping AI models understand what each tool does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exposing Your .NET APIs with MCP
&lt;/h2&gt;

&lt;p&gt;Let's say you have a Monkey API (because everyone needs one 🐒). Here's how to expose it via MCP:&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;MonkeyService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;MonkeyService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Monkey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetMonkeys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com/monkeys"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ... process and return monkeys&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Monkey&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetMonkey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;monkeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetMonkeys&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;monkeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&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;class&lt;/span&gt; &lt;span class="nc"&gt;MonkeyTools&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Get a list of all monkeys."&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;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;GetMonkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MonkeyService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;monkeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMonkeys&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;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monkeys&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Get details for a specific monkey."&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;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;GetMonkey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MonkeyService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The name of the monkey"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;monkey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMonkey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monkey&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;Now AI assistants can query your monkey database through a standardized interface!&lt;/p&gt;




&lt;h2&gt;
  
  
  HTTP Transport for ASP.NET Core
&lt;/h2&gt;

&lt;p&gt;For web APIs, you can use HTTP transport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;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="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;AddMcpServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHttpTransport&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="n"&gt;Stateless&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithToolsFromAssembly&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;MapMcp&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;span class="s"&gt;"http://localhost:3001"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is perfect for exposing your existing ASP.NET Core APIs to AI tools without major refactoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Integrations
&lt;/h2&gt;

&lt;p&gt;The MCP ecosystem already includes servers for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt; - Version control operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; - Repository management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright&lt;/strong&gt; - Browser automation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem&lt;/strong&gt; - Local file operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases&lt;/strong&gt; - SQL/NoSQL queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Services&lt;/strong&gt; - Storage, Cosmos DB, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also build custom MCP servers for your specific APIs, databases, or internal systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;p&gt;MCP is designed with security first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit User Consent&lt;/strong&gt; - Every tool invocation requires user approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Privacy&lt;/strong&gt; - Resources only share what's explicitly permitted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool Safety&lt;/strong&gt; - Arbitrary code execution with proper safeguards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling Controls&lt;/strong&gt; - Users control LLM interaction parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a .NET developer, you should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Always implement proper authorization&lt;/li&gt;
&lt;li&gt;Use least-privilege access for resources&lt;/li&gt;
&lt;li&gt;Log and audit all tool invocations&lt;/li&gt;
&lt;li&gt;Follow security best practices for your specific domain&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Deployment Options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Local Development
&lt;/h3&gt;

&lt;p&gt;Use stdio transport for local processes or ASP.NET Core HTTP servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Deployment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Container Apps&lt;/strong&gt; - Managed container hosting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Functions&lt;/strong&gt; - Serverless MCP servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; - Multi-arch container images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Publishing a containerized MCP server:&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;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;EnableSdkContainerSupport&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/EnableSdkContainerSupport&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ContainerRepository&amp;gt;&lt;/span&gt;myorg/mcp-server&lt;span class="nt"&gt;&amp;lt;/ContainerRepository&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ContainerFamily&amp;gt;&lt;/span&gt;alpine&lt;span class="nt"&gt;&amp;lt;/ContainerFamily&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RuntimeIdentifiers&amp;gt;&lt;/span&gt;linux-x64;linux-arm64&lt;span class="nt"&gt;&amp;lt;/RuntimeIdentifiers&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then deploy with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet publish /t:PublishContainer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Future Is Here
&lt;/h2&gt;

&lt;p&gt;MCP isn't just another API standard—it's becoming the &lt;strong&gt;universal protocol for AI-tool communication&lt;/strong&gt;. Every major AI platform is adopting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Copilot&lt;/li&gt;
&lt;li&gt;Claude&lt;/li&gt;
&lt;li&gt;Cursor&lt;/li&gt;
&lt;li&gt;Windsurf&lt;/li&gt;
&lt;li&gt;And many more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a .NET developer, learning MCP now positions you perfectly for the AI-integrated future. Whether you're building APIs, microservices, or AI-powered applications, MCP provides the standard interface that makes your .NET code accessible to AI agents.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install the SDK&lt;/strong&gt;: &lt;code&gt;dotnet add package ModelContextProtocol --prerelease&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the docs&lt;/strong&gt;: &lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/get-started-mcp" rel="noopener noreferrer"&gt;Microsoft Learn - Get started with .NET AI and MCP&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore examples&lt;/strong&gt;: &lt;a href="https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples" rel="noopener noreferrer"&gt;MCP C# SDK samples&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build your first server&lt;/strong&gt;: Start with a simple echo tool and expand from there&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The MCP revolution is happening now. Are you ready to join it?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Connect with me on LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/vikrant-bagal" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/vikrant-bagal&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>ai</category>
      <category>csharp</category>
      <category>api</category>
    </item>
    <item>
      <title>Stop putting business logic in your integration layer — here's the pattern we built</title>
      <dc:creator>Thiago Paz</dc:creator>
      <pubDate>Fri, 15 May 2026 18:24:51 +0000</pubDate>
      <link>https://dev.to/thiagopazs/stop-putting-business-logic-in-your-integration-layer-heres-the-pattern-we-built-35mk</link>
      <guid>https://dev.to/thiagopazs/stop-putting-business-logic-in-your-integration-layer-heres-the-pattern-we-built-35mk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — We had business logic scattered across an integration service. Debugging was painful, onboarding was slow, and every new flow was a gamble. We fixed it by moving all orchestration into our platform via a standardized job pattern. This post walks through the architecture, the full end-to-end flow, and a copy-paste recipe for creating new jobs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem nobody wants to admit
&lt;/h2&gt;

&lt;p&gt;You know the feeling. You open a service called &lt;code&gt;ExternalApi&lt;/code&gt; expecting to find... API calls. Instead you find business rules, conditional flows, client-specific logic, and three years of "temporary" fixes.&lt;/p&gt;

&lt;p&gt;That was us.&lt;/p&gt;

&lt;p&gt;Every integration with our external system was its own snowflake. Different conventions, different error handling, different logging strategies — or no logging at all. Onboarding a new developer meant a two-hour walkthrough just to explain why a single job worked the way it did.&lt;/p&gt;

&lt;p&gt;We needed to fix this. Not with a rewrite — with a &lt;strong&gt;pattern&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The principle: your platform owns the rules
&lt;/h2&gt;

&lt;p&gt;The core decision was simple but had big consequences:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The integration service handles transport. Our platform owns the business logic.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The external service calls an endpoint and gets back a result&lt;/li&gt;
&lt;li&gt;All orchestration, validation, rule application, and logging lives in our platform&lt;/li&gt;
&lt;li&gt;The integration job becomes a dumb but reliable pipe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this shift, every new integration follows the same skeleton. The domain knowledge stays where it belongs — inside the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;Here's how the layers map out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scheduler / Orchestrator
        │
        ▼
JobsController              ← route + auth (query key)
        │
        ▼
ClientUpdateApp             ← orchestration + business rules
        │
        ├── JobConfiguracao      ← job config per client (JSON)
        │
        ├── ExternalSystemApi    ← external data fetch + ack
        │
        └── Domain (Workflow, Scheduling, Notifications)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer has a single responsibility. The controller authenticates and delegates. The app orchestrates and applies rules. The infra layer talks to the outside world.&lt;/p&gt;




&lt;h2&gt;
  
  
  The full flow: &lt;code&gt;JobSyncCompletedRecords&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let's walk through a real job. This one syncs completed records from the external system back into our platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Endpoint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/Jobs/JobSyncCompletedRecords?key=&amp;lt;job-key&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;200 OK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job completed successfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Invalid or missing key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;500 Internal Server Error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unhandled exception (logged automatically)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step by step
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Scheduler fires the route&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An external scheduler (cron, Azure Timer, whatever fits your stack) calls the endpoint. No payload needed — configuration lives in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Controller validates and delegates&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JobSyncCompletedRecords"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;JobSyncCompletedRecords&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;key&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;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteJobWithKeyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_clientUpdateApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;JobSyncCompletedRecordsAsync&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s"&gt;"Records synced successfully"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExecuteJobWithKeyAsync&lt;/code&gt; is a shared helper that handles key validation, wraps execution in try/catch, and returns the appropriate HTTP response. Every job uses it — zero boilerplate per route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. App loads configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;JobSyncCompletedRecordsAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteJobPerClientAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;JobTypeEnum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SyncCompletedRecords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ProcessCompletedRecordsAsync&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExecuteJobPerClientAsync&lt;/code&gt; fetches the job config from &lt;code&gt;APP_JOB_CONFIG&lt;/code&gt; by job type, deserializes the client list from the &lt;code&gt;Configuration&lt;/code&gt; JSON field, and calls the action for each client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Per-client processing (parallel, bounded)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessCompletedRecordsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClientJobConfigDTO&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Fetch completed records from external system&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;records&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;_externalApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCompletedRecordsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Script&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Process in parallel (max 6 concurrent)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForEachAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&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;ParallelOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;MaxDegreeOfParallelism&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;UpdateRecordStatusAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;
                &lt;span class="nc"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;RegisterWorkflowTriggerAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="nc"&gt;COMPLETED&lt;/span&gt;&lt;span class="s"&gt;");
&lt;/span&gt;                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ClearPendingItemsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;
            &lt;span class="err"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Acknowledge back to external system&lt;/span&gt;
        &lt;span class="nc"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_externalApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AcknowledgeRecordsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. Persist execution log&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_requestLogApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Item-level error: log and continue — never abort the batch&lt;/span&gt;
        &lt;span class="n"&gt;_appLogService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth highlighting here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bounded parallelism&lt;/strong&gt;: &lt;code&gt;MaxDegreeOfParallelism = 6&lt;/code&gt; prevents thread explosion on large client batches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Item-level resilience&lt;/strong&gt;: a failure on one record is logged but never stops the rest of the batch&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The configuration contract
&lt;/h2&gt;

&lt;p&gt;Every job reads its client list from a single table:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table: &lt;code&gt;APP_JOB_CONFIG&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JobType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enum value identifying the job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Route&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Endpoint path for external system calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Configuration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON array of client configs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CreatedByUserId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Technical user for execution context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Configuration&lt;/code&gt; JSON structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ClientId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"GroupId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/records"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Script"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET_COMPLETED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SendFrequency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means enabling a new client is a database insert — no code change, no deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The recipe: creating a new job in 8 steps
&lt;/h2&gt;

&lt;p&gt;Follow this and you'll have a production-ready job in under two hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Add the enum value
&lt;/h3&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;enum&lt;/span&gt; &lt;span class="n"&gt;JobTypeEnum&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SyncCompletedRecords&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;YourNewJob&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;  &lt;span class="c1"&gt;// 👈 add here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Expose the route
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"YourNewJob"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;YourNewJob&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;key&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;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteJobWithKeyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_clientUpdateApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;YourNewJobAsync&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s"&gt;"Job executed successfully"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Create the entry method
&lt;/h3&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;YourNewJobAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteJobPerClientAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;JobTypeEnum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YourNewJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ProcessYourNewJobAsync&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4 — Implement the per-client action
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessYourNewJobAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClientJobConfigDTO&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Fetch data from external system&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&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;_externalApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetYourDataAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Apply business rules&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ApplyBusinessRuleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;
        &lt;span class="err"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Acknowledge back to external system&lt;/span&gt;
        &lt;span class="nc"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_externalApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AcknowledgeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. Log the execution&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_requestLogApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_appLogService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5 — Add external system integration methods
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;ExternalSystemApi.cs&lt;/code&gt;, add the two methods: one for fetching (&lt;code&gt;Get...&lt;/code&gt;) and one for acknowledging (&lt;code&gt;Acknowledge...&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Insert the DB config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;APP_JOB_CONFIG&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreatedByUserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'/api/Jobs/YourNewJob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'[{"ClientId": 1, "GroupId": 10}]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;999&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7 — Observability is not optional
&lt;/h3&gt;

&lt;p&gt;Your job must emit at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entry log&lt;/strong&gt;: job started, which type, timestamp&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-item error log&lt;/strong&gt;: client id, group id, item id, exception&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completion log&lt;/strong&gt;: total received, total processed, total errors, duration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this, you're flying blind in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8 — Rollout by config
&lt;/h3&gt;

&lt;p&gt;Enable one pilot client. Monitor two full cycles. Validate with the functional team. Then expand. Never enable all clients at once on a first deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The observability checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Records received per job/client&lt;/td&gt;
&lt;td&gt;Detects external system issues early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Records processed successfully&lt;/td&gt;
&lt;td&gt;Your primary success signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Records with errors&lt;/td&gt;
&lt;td&gt;Triggers alert if above threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total job duration&lt;/td&gt;
&lt;td&gt;Catches performance regressions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average time per item&lt;/td&gt;
&lt;td&gt;Diagnoses bottlenecks at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Alert rules we recommend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 3+ consecutive failures for the same client → page the team&lt;/li&gt;
&lt;li&gt;🟡 Processed/received ratio below 95% → investigate before next cycle&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The checklist before you ship
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Technical
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;JobTypeEnum&lt;/code&gt; updated&lt;/li&gt;
&lt;li&gt;[ ] Route created in &lt;code&gt;JobsController&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Interface + implementation in &lt;code&gt;ClientUpdateApp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Per-item exception handling in place&lt;/li&gt;
&lt;li&gt;[ ] External system fetch + ack methods added&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;APP_JOB_CONFIG&lt;/code&gt; record inserted&lt;/li&gt;
&lt;li&gt;[ ] Request log and exception log covering success and error paths&lt;/li&gt;
&lt;li&gt;[ ] Idempotency verified — no reprocessing on retry&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Invalid key returns &lt;code&gt;401&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Missing config doesn't crash execution&lt;/li&gt;
&lt;li&gt;[ ] Empty record list completes with proper log&lt;/li&gt;
&lt;li&gt;[ ] Single item error doesn't abort the batch&lt;/li&gt;
&lt;li&gt;[ ] External system ack only fires for successfully processed items&lt;/li&gt;
&lt;li&gt;[ ] Workflow trigger fires as expected&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What we gained
&lt;/h2&gt;

&lt;p&gt;After rolling this out across our first jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new developer can understand any job end-to-end in &lt;strong&gt;under 15 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Adding a new client to an existing job is &lt;strong&gt;a single database insert&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Debugging a production issue means checking one log table — not tracing across two services&lt;/li&gt;
&lt;li&gt;Every new job starts from the same skeleton — &lt;strong&gt;zero architectural decisions at job creation time&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is underrated. Decision fatigue is real. When the pattern is clear, developers ship faster and make fewer mistakes.&lt;/p&gt;




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

&lt;p&gt;We're looking at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moving the job key from a query param to a proper secret vault&lt;/li&gt;
&lt;li&gt;Adding token-based service auth for the medium term&lt;/li&gt;
&lt;li&gt;Building a lightweight dashboard to visualize job health across all clients in real time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've dealt with similar integration patterns — in health tech or elsewhere — I'd love to hear how you approached it. Drop a comment below.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>architecture</category>
      <category>healthtech</category>
      <category>backend</category>
    </item>
    <item>
      <title>I spent a year building Apache Camel for .NET. Here's the honest state of it.</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Fri, 15 May 2026 16:23:44 +0000</pubDate>
      <link>https://dev.to/rinat_kozin_d0a2ef43e7824/i-spent-a-year-building-apache-camel-for-net-heres-the-honest-state-of-it-150e</link>
      <guid>https://dev.to/rinat_kozin_d0a2ef43e7824/i-spent-a-year-building-apache-camel-for-net-heres-the-honest-state-of-it-150e</guid>
      <description>&lt;p&gt;I've spent the last year building an integration ecosystem for .NET that I think fills a real gap. Three repos just went public. This post is the honest account — what it does, what it doesn't do yet, and three questions I genuinely don't know the answer to.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;If you need Apache Camel in .NET, you don't have it.&lt;/p&gt;

&lt;p&gt;MassTransit, Wolverine, NServiceBus are message buses — great at what they do, but they give you 4–7 transports and a Saga pattern. Apache Camel has 300+ components and 80+ EIP patterns as first-class DSL. But it's JVM.&lt;/p&gt;

&lt;p&gt;There's no .NET project that gives you the full EIP catalogue —&lt;br&gt;
Splitter, Aggregator, Content-Based Router, Recipient List, Dynamic Router,&lt;br&gt;
Wiretap, Dead Letter Channel — as a fluent C# DSL with 20+ transports.&lt;/p&gt;

&lt;p&gt;So I wrote one.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;redb.Route&lt;/strong&gt; — Camel-style fluent C# DSL. Apache 2.0.&lt;br&gt;
&lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;22 transports: Kafka, RabbitMQ, Redis, SQL polling, HTTP, gRPC, SFTP, MQTT,&lt;br&gt;
S3, IBM MQ, AMQP 1.0, Azure Service Bus, Elasticsearch, LDAP, Mail, TCP,&lt;br&gt;
WebSocket, SignalR, FTP, Quartz, File — plus 5 built-in (direct, seda, timer, log, mock).&lt;/p&gt;

&lt;p&gt;30+ EIP processors as first-class DSL steps. Compiled expression engine:&lt;br&gt;
&lt;code&gt;${header.price} * 1.2&lt;/code&gt; compiles to &lt;code&gt;Func&amp;lt;IExchange, decimal&amp;gt;&lt;/code&gt; via&lt;br&gt;
&lt;code&gt;System.Linq.Expressions&lt;/code&gt; at route-build time. No interpreter at runtime.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;redb.Tsak&lt;/strong&gt; — runtime container for redb.Route modules. Apache 2.0.&lt;br&gt;
&lt;a href="https://github.com/redbase-app/redb-tsak" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-tsak&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop a &lt;code&gt;.dll&lt;/code&gt; or &lt;code&gt;.tpkg&lt;/code&gt; (ZIP + manifest) into a folder. Tsak picks it up,&lt;br&gt;
wires the DI container, starts the routes. Hot-reload — replace the file while&lt;br&gt;
running, zero downtime. REST API + CLI + Blazor dashboard.&lt;/p&gt;

&lt;p&gt;Cluster: leader election + auto-rebalance, no ZooKeeper/etcd/Consul.&lt;br&gt;
Coordination lives entirely in EAV (row locks + SELECT FOR UPDATE CAS + epoch fencing).&lt;/p&gt;

&lt;p&gt;Closest analogues are Apache Karaf and Camel K. Both JVM-only until now.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;redb.Core&lt;/strong&gt; — the storage layer underneath. Apache 2.0.&lt;br&gt;
&lt;a href="https://github.com/redbase-app/redb" rel="noopener noreferrer"&gt;github.com/redbase-app/redb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;EAV-style typed object store over Postgres / MSSQL with full LINQ. Schema is a&lt;br&gt;
C# class with &lt;code&gt;[RedbScheme]&lt;/code&gt;. Values live in typed columns with FK constraints —&lt;br&gt;
not JSON blobs. &lt;code&gt;SyncSchemeAsync&amp;lt;T&amp;gt;()&lt;/code&gt; at startup, no migration files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RedbScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProps&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="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;set&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;""&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;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&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;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Inside a route pipeline:&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pg-main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&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;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedbObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderProps&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;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"Order-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;HHmmss&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;Props&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="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="s"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;299.99m&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"order-id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rabbitmq://processed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Production state
&lt;/h2&gt;

&lt;p&gt;Running at &lt;a href="https://ews.ru" rel="noopener noreferrer"&gt;East-West / EWS&lt;/a&gt; — a 30-year-old national HoReCa&lt;br&gt;
food distributor in Russia.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~150k orders/month, ~20k B2B customers, 600+ cities&lt;/li&gt;
&lt;li&gt;3-node cluster (4 cores / 8 GB / 50 GB SSD per node)&lt;/li&gt;
&lt;li&gt;~550 daily users, ~3 months stable, 10–15% CPU under full load&lt;/li&gt;
&lt;li&gt;Routes integrate: SAP, Kafka, RabbitMQ, GPS feeds, Mercury / EGAIS /
Chestny Znak / FGIS Grain (Russian regulatory systems)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;43 NuGet packages, ~8.4k total downloads. Real but small.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Bus factor of 1.&lt;/strong&gt; I'm the solo author of the framework code. Publishing&lt;br&gt;
to find early users and get harsh feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tier exists.&lt;/strong&gt; Commercial license covers: compiled expression-tree-to-SQL,&lt;br&gt;
parallel materialization, change-tracking diff, schema migrations.&lt;br&gt;
Everything above — Route, Tsak, Core CRUD/LINQ/trees/security/export,&lt;br&gt;
22 transports, hot-reload, cluster — is Apache 2.0 and stays free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No third-party benchmarks yet.&lt;/strong&gt; Internal Free-vs-Pro numbers at&lt;br&gt;
&lt;a href="https://redbase.app/results" rel="noopener noreferrer"&gt;redbase.app/results&lt;/a&gt; — treat as&lt;br&gt;
"how editions compare on the same machine", not universal claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture writeup:&lt;/strong&gt; &lt;a href="https://redbase.app/architecture" rel="noopener noreferrer"&gt;redbase.app/architecture&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;redb.Identity&lt;/strong&gt; (near-complete, not yet public) — OAuth 2.0 / OIDC server&lt;br&gt;
built on redb.Core. Authorization Code + PKCE, Client Credentials, Device Code,&lt;br&gt;
Refresh Token, PAR, DPoP, backchannel logout, private_key_jwt. Apache 2.0.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three questions I don't have the answer to
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Is "another integration framework" the wrong framing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gap I described feels real to me — I lived with it for years before building&lt;br&gt;
this. But maybe .NET teams that need Camel just use Camel via NativeAOT interop,&lt;br&gt;
or have given up on the EIP model entirely. Curious what you've actually done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Does "EAV" as a term kill the pitch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;redb.Core uses EAV — and that word triggers people (slow, untyped, joins&lt;br&gt;
everywhere). The design is typed columns + FK constraints + 40+ indexes, not&lt;br&gt;
JSON blobs. But maybe the term itself is a non-starter regardless of the&lt;br&gt;
implementation. Has anyone successfully reframed EAV to a skeptical audience?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What makes a Pro/Free split feel fair vs bait-and-switch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The free tier covers everything you need to build and run production pipelines.&lt;br&gt;
Pro adds performance features (compiled SQL, parallel materialization) and ops&lt;br&gt;
features (change tracking, migrations). Is that split legible from the outside,&lt;br&gt;
or does "compiled queries are Pro" read as "the free tier is intentionally crippled"?&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;redb.Route&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redb.Tsak&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-tsak" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-tsak&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redb.Core&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb" rel="noopener noreferrer"&gt;github.com/redbase-app/redb&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;&lt;a href="https://redbase.app/architecture" rel="noopener noreferrer"&gt;redbase.app/architecture&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discussions&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/discussions" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route/discussions&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Happy to answer technical questions in the comments.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
      <category>discuss</category>
    </item>
    <item>
      <title>PDFmyURL vs IronPDF: an unbiased look for .NET teams</title>
      <dc:creator>IronSoftware</dc:creator>
      <pubDate>Fri, 15 May 2026 16:14:00 +0000</pubDate>
      <link>https://dev.to/ironsoftware/pdfmyurl-vs-ironpdf-an-unbiased-look-for-net-teams-4gc9</link>
      <guid>https://dev.to/ironsoftware/pdfmyurl-vs-ironpdf-an-unbiased-look-for-net-teams-4gc9</guid>
      <description>&lt;p&gt;Picture a familiar architectural fork: a .NET service that generates compliance PDFs containing customer account data, transaction histories, and PII. A hosted HTML-to-PDF API like PDFmyURL is tempting — no install, one HTTP call, a PDF back. But the same call ships the document body across the public internet to a third-party processor, which immediately raises questions a security review will ask: where does the data land, who can audit it, and is there a DPA/BAA in place that the regulators will accept?&lt;/p&gt;

&lt;p&gt;That fork shows up across industries with data residency or compliance constraints — healthcare (HIPAA), finance (SOX, PCI-DSS), government (FedRAMP), and EU GDPR scope generally. The question this article tries to answer is narrow: when does a cloud HTML-to-PDF API like PDFmyURL fit the use case, and when do you want a local-rendering library like IronPDF running inside your own process? Both are legitimate tools; the choice is about where the bytes are allowed to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding IronPDF
&lt;/h2&gt;

&lt;p&gt;IronPDF processes all PDF generation locally within your application's process space. HTML content, data, and generated PDFs never leave your infrastructure. The library embeds a Chromium rendering engine that executes entirely on your servers—whether on-premises Windows/Linux boxes, private cloud VMs, or containerized deployments. Network calls occur only when rendering URLs that your application explicitly requests, not as part of the library's operation.&lt;/p&gt;

&lt;p&gt;This on-premises architecture addresses compliance requirements by default: data never transits to third parties, processing happens within your security boundary, and service availability depends only on your infrastructure. For applications with data governance requirements, this architectural difference often determines eligibility before any feature comparison begins. See the &lt;a href="https://ironpdf.com/tutorials/html-to-pdf/" rel="noopener noreferrer"&gt;HTML to PDF tutorial&lt;/a&gt; for implementation patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Limitations of PDFmyURL
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Product Status
&lt;/h3&gt;

&lt;p&gt;PDFmyURL has operated as a cloud conversion service since 2008, providing a stable REST API for HTML-to-PDF conversion. The service offers tiered subscription plans based on monthly conversion volume. API stability is good; breaking changes are rare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing Capabilities
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Data Sovereignty Compliance&lt;/strong&gt;: Every HTML document you convert is transmitted to PDFmyURL's servers for processing. This creates compliance issues for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GDPR (data transfer outside EU)&lt;/li&gt;
&lt;li&gt;HIPAA (PHI transmission to third parties)&lt;/li&gt;
&lt;li&gt;PCI-DSS (cardholder data handling)&lt;/li&gt;
&lt;li&gt;SOX (financial data controls)&lt;/li&gt;
&lt;li&gt;FedRAMP (government data restrictions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Offline Operation&lt;/strong&gt;: The service requires internet connectivity for every conversion. Air-gapped environments, private networks without external access, and scenarios requiring offline functionality cannot use cloud APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct PDF Manipulation&lt;/strong&gt;: PDFmyURL converts URLs/HTML to PDFs. It doesn't provide APIs for merging existing PDFs, extracting text, adding watermarks to pre-generated documents, or modifying PDFs programmatically. Each operation requires a separate tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Issues
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Network Dependency&lt;/strong&gt;: Every conversion is an HTTP request to external servers. This introduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network latency (roundtrip times)&lt;/li&gt;
&lt;li&gt;Potential network failures (timeouts, connectivity issues)&lt;/li&gt;
&lt;li&gt;Rate limiting (API quotas)&lt;/li&gt;
&lt;li&gt;Service availability dependency (uptime outside your control)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Subscription Cost Model&lt;/strong&gt;: Cloud services charge per conversion or monthly volume tiers. High-volume applications may encounter usage costs that scale with business success, creating unpredictable operational expenses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Transfer Overhead&lt;/strong&gt;: Large HTML documents with embedded images or complex layouts require uploading all content to remote servers. This consumes bandwidth and adds latency proportional to payload size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Support Status
&lt;/h3&gt;

&lt;p&gt;PDFmyURL provides email support and an API reference at &lt;a href="https://pdfmyurl.com/html-to-pdf-api" rel="noopener noreferrer"&gt;https://pdfmyurl.com/html-to-pdf-api&lt;/a&gt;. Response time and SLA tiers vary by plan; check the current pricing/SLA page on the vendor site for specifics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Problems
&lt;/h3&gt;

&lt;p&gt;The cloud API model creates an architectural coupling where your application's PDF generation capability depends on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;External service availability&lt;/li&gt;
&lt;li&gt;Network connectivity&lt;/li&gt;
&lt;li&gt;API quota limits&lt;/li&gt;
&lt;li&gt;Third-party infrastructure performance&lt;/li&gt;
&lt;li&gt;Data transmission latency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For applications where PDF generation is a critical path operation (invoicing, reporting, document delivery), these dependencies introduce failure modes beyond your infrastructure control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Comparison Overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;PDFmyURL&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Current Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Active (cloud SaaS)&lt;/td&gt;
&lt;td&gt;Active (on-premises library)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML/CSS Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-side rendering engine&lt;/td&gt;
&lt;td&gt;Chromium-based engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rendering Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-rendered HTML to PDF&lt;/td&gt;
&lt;td&gt;Matches Chrome print preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Installation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTP calls against pdfmyurl.com/api&lt;/td&gt;
&lt;td&gt;NuGet package + first-run Chromium download (~150 MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Email support, tier-dependent&lt;/td&gt;
&lt;td&gt;Commercial support per IronPDF license&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depends on service availability&lt;/td&gt;
&lt;td&gt;Runs inside your process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Code Comparison - Checklist Style
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Task: Convert HTML String to PDF
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PDFmyURL Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&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;System.Collections.Generic&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;System.Net.Http&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;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfMyUrlConverter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_license&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PdfMyUrlConverter&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;license&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_license&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="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;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConvertHtmlStringAsync&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;htmlContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Form-encoded POST against the single REST endpoint;&lt;/span&gt;
        &lt;span class="c1"&gt;// response body is the PDF binary.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;form&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;FormUrlEncodedContent&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_license&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;htmlContent&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"page_size"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"A4"&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"orientation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"portrait"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;try&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;_httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://pdfmyurl.com/api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&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="nf"&gt;EnsureSuccessStatusCode&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;await&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;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsByteArrayAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// HTTP-level failure: invalid license, rate limit, service unavailable, network error&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"PDFmyURL conversion failed - inspect status code / network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PDF conversion exceeded HttpClient timeout"&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;&lt;strong&gt;Checklist - Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML payload is sent to PDFmyURL's servers for rendering — confirm acceptable under your data handling policy&lt;/li&gt;
&lt;li&gt;Requires outbound HTTPS to pdfmyurl.com on every conversion&lt;/li&gt;
&lt;li&gt;Per-call latency includes network round-trip plus server-side render time&lt;/li&gt;
&lt;li&gt;Per-plan rate limits and monthly quotas apply; verify against the current tier on the vendor pricing page&lt;/li&gt;
&lt;li&gt;HTTP-level errors surface as status codes and exception messages rather than typed render exceptions&lt;/li&gt;
&lt;li&gt;Default &lt;code&gt;HttpClient&lt;/code&gt; timeouts may need tuning for large payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IronPDF Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;IronPdf&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;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocalPdfGenerator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ChromePdfRenderer&lt;/span&gt; &lt;span class="n"&gt;_renderer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LocalPdfGenerator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_renderer&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;ChromePdfRenderer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConvertHtmlStringAsync&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;htmlContent&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;pdf&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;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdfAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;htmlContent&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;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checklist - Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Data Privacy&lt;/strong&gt;: All processing local, data never leaves infrastructure&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No Network Dependency&lt;/strong&gt;: Works offline, air-gapped, private networks&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Low Latency&lt;/strong&gt;: No network roundtrip overhead&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No Rate Limiting&lt;/strong&gt;: Limited only by hardware resources&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Rich Error Handling&lt;/strong&gt;: Detailed exceptions with stack traces&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Predictable Performance&lt;/strong&gt;: Depends only on server hardware&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Task: Convert URL to PDF
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PDFmyURL Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;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;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConvertUrlAsync&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;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// PDFmyURL margin parameters: top/bottom/left/right + unit (mm/in/cm/pt/px)&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;form&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;FormUrlEncodedContent&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_license&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"page_size"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Letter"&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"10"&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"bottom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"10"&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mm"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://pdfmyurl.com/api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&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="nf"&gt;EnsureSuccessStatusCode&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;await&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;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsByteArrayAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checklist - Considerations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The fetch goes your server → PDFmyURL → target URL, so the target URL must be reachable from PDFmyURL's network&lt;/li&gt;
&lt;li&gt;Authenticated URLs typically need cookies or headers passed as API parameters — verify what the PDFmyURL parameter set supports for your tier&lt;/li&gt;
&lt;li&gt;Intranet, localhost, and VPN-only URLs are not reachable from a public cloud renderer&lt;/li&gt;
&lt;li&gt;JavaScript wait timing is controlled by the &lt;code&gt;javascript_time&lt;/code&gt; parameter rather than by event hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IronPDF Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;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;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConvertUrlAsync&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;url&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;pdf&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;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderUrlAsPdfAsync&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConvertAuthenticatedUrlAsync&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;url&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;cookieHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RenderingOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomCssUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://yourcdn.com/print.css"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RenderingOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomHttpHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cookie"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookieHeader&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;pdf&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;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderUrlAsPdfAsync&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checklist - Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Single Network Hop&lt;/strong&gt;: Direct fetch from your server to target URL&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Full Authentication Control&lt;/strong&gt;: Pass cookies, headers, certificates programmatically&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Private URLs&lt;/strong&gt;: Can convert localhost, intranet, VPN-protected URLs&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;JavaScript Control&lt;/strong&gt;: Configurable JS execution, wait conditions&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Task: Batch Convert Multiple HTMLs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PDFmyURL Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;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;byte&lt;/span&gt;&lt;span class="p"&gt;[][&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BatchConvertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;htmlContents&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;tasks&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;htmlContents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Each conversion is a separate API call&lt;/span&gt;
        &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ConvertHtmlStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// All requests go to external service&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checklist - Batch Considerations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each call counts against the plan's monthly quota and per-tier concurrency limits&lt;/li&gt;
&lt;li&gt;Multiple large HTML payloads consume outbound bandwidth on every batch&lt;/li&gt;
&lt;li&gt;Cost scales linearly with batch size on metered tiers&lt;/li&gt;
&lt;li&gt;A single slow render does not block siblings, but overall throughput is bounded by the service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IronPDF Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;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;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BatchConvertAndMergeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;htmlContents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Process all in parallel on local hardware&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;htmlContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
        &lt;span class="n"&gt;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdfAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&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;pdfs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Merge into single PDF&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfs&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;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checklist - Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;No Rate Limits&lt;/strong&gt;: Parallelism limited only by CPU/memory&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No Network Overhead&lt;/strong&gt;: All processing local&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No Cost Scaling&lt;/strong&gt;: Process millions without per-unit charges&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Built-in Merge&lt;/strong&gt;: Combine results without additional tools&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Task: Add Watermarks to Generated PDFs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PDFmyURL Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;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;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConvertWithWatermarkAsync&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;htmlContent&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;watermarkText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Watermark behavior is controlled via API parameters on the same /api endpoint.&lt;/span&gt;
    &lt;span class="c1"&gt;// Check the live API reference at https://pdfmyurl.com/html-to-pdf-api for the&lt;/span&gt;
    &lt;span class="c1"&gt;// current parameter set and any tier-related availability.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;form&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;FormUrlEncodedContent&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_license&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;htmlContent&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"watermark_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;watermarkText&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;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"watermark_style"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"diagonal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://pdfmyurl.com/api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&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="nf"&gt;EnsureSuccessStatusCode&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;await&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;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsByteArrayAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checklist - Watermark Considerations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm watermark parameter support and tier availability against the current API reference&lt;/li&gt;
&lt;li&gt;Text, position, and opacity are controlled via parameters; check the documented ranges&lt;/li&gt;
&lt;li&gt;Image-watermark support depends on the parameter set exposed by the service&lt;/li&gt;
&lt;li&gt;The /api endpoint converts source HTML/URL to PDF; modifying an existing PDF is not part of the conversion endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IronPDF Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;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;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConvertWithWatermarkAsync&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;htmlContent&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;watermarkText&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;pdf&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;_renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdfAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;htmlContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add text watermark to all pages&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;h1&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;watermarkText&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;verticalAlignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Editing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VerticalAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Middle&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;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;AddImageWatermarkAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;existingPdfBytes&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;imagePath&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;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromBinaryData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingPdfBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Image watermark&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyImageWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imagePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;verticalOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checklist - Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Full Customization&lt;/strong&gt;: Text, images, HTML watermarks&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Post-Processing&lt;/strong&gt;: Add watermarks to any existing PDF&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Precise Control&lt;/strong&gt;: Position, opacity, rotation, per-page options&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No API Tiers&lt;/strong&gt;: All features available in library&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Task: Handle Sensitive Financial Data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cloud API model (PDFmyURL) — questions to answer with your compliance team:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the conversion involve PII / PHI / cardholder data leaving your security boundary?&lt;/li&gt;
&lt;li&gt;What processing jurisdiction is documented in the vendor's DPA?&lt;/li&gt;
&lt;li&gt;Are data residency guarantees offered at the tier you would purchase?&lt;/li&gt;
&lt;li&gt;Is a BAA or equivalent contractual instrument available for your regulator?&lt;/li&gt;
&lt;li&gt;Where is the audit trail stored, and how long is it retained?&lt;/li&gt;
&lt;li&gt;What is the documented data deletion / retention SLA?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those questions does not have an acceptable answer for your data class, the cloud-API path is likely off the table regardless of feature parity — that is an organizational call, not a technical one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-premises library model (IronPDF) — what changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Processing happens inside your application's process; HTML and generated PDFs stay on your infrastructure&lt;/li&gt;
&lt;li&gt;Processing jurisdiction equals wherever you deploy the host&lt;/li&gt;
&lt;li&gt;Data residency is whatever your hosting choice is&lt;/li&gt;
&lt;li&gt;No third-party data sharing introduced by the PDF step itself&lt;/li&gt;
&lt;li&gt;Audit trail and retention are under your control&lt;/li&gt;
&lt;li&gt;Data deletion is governed by your own storage policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For detailed HTML rendering and security options, see the &lt;a href="https://ironpdf.com/how-to/html-string-to-pdf/" rel="noopener noreferrer"&gt;HTML string to PDF guide&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  API Mapping Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PDFmyURL Concept&lt;/th&gt;
&lt;th&gt;IronPDF Equivalent&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP POST to /api&lt;/td&gt;
&lt;td&gt;ChromePdfRenderer methods&lt;/td&gt;
&lt;td&gt;REST API vs. library calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;license parameter&lt;/td&gt;
&lt;td&gt;IronPdf.License.LicenseKey&lt;/td&gt;
&lt;td&gt;One-time setup vs. per-request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;html parameter&lt;/td&gt;
&lt;td&gt;RenderHtmlAsPdf(string)&lt;/td&gt;
&lt;td&gt;String content directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;url parameter&lt;/td&gt;
&lt;td&gt;RenderUrlAsPdf(string)&lt;/td&gt;
&lt;td&gt;Same concept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page_size parameter&lt;/td&gt;
&lt;td&gt;RenderingOptions.PaperSize&lt;/td&gt;
&lt;td&gt;Enum vs. string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;orientation parameter&lt;/td&gt;
&lt;td&gt;RenderingOptions.PaperOrientation&lt;/td&gt;
&lt;td&gt;Typed property&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;top/bottom/left/right + unit&lt;/td&gt;
&lt;td&gt;RenderingOptions.MarginTop etc. (mm)&lt;/td&gt;
&lt;td&gt;Margins are typed numeric properties in IronPDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP response body&lt;/td&gt;
&lt;td&gt;PdfDocument.BinaryData&lt;/td&gt;
&lt;td&gt;Bytes returned directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API quota limits&lt;/td&gt;
&lt;td&gt;Hardware limitations&lt;/td&gt;
&lt;td&gt;No artificial throttling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription tiers&lt;/td&gt;
&lt;td&gt;License types&lt;/td&gt;
&lt;td&gt;Per-deployment vs. per-use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network timeout&lt;/td&gt;
&lt;td&gt;Local processing time&lt;/td&gt;
&lt;td&gt;No network overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service uptime&lt;/td&gt;
&lt;td&gt;Application uptime&lt;/td&gt;
&lt;td&gt;Same availability tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Comprehensive Feature Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;PDFmyURL&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing location&lt;/td&gt;
&lt;td&gt;Cloud (third-party servers)&lt;/td&gt;
&lt;td&gt;On-premises (your infrastructure)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data transmission&lt;/td&gt;
&lt;td&gt;All content to external service&lt;/td&gt;
&lt;td&gt;Local only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network requirement&lt;/td&gt;
&lt;td&gt;Required for every conversion&lt;/td&gt;
&lt;td&gt;Optional (for URL fetching)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline operation&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compliance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data sovereignty&lt;/td&gt;
&lt;td&gt;Data leaves boundary&lt;/td&gt;
&lt;td&gt;Data stays local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR compliance&lt;/td&gt;
&lt;td&gt;Complex (data transfer)&lt;/td&gt;
&lt;td&gt;Straightforward (no transfer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIPAA compliance&lt;/td&gt;
&lt;td&gt;Requires BAA (if available)&lt;/td&gt;
&lt;td&gt;Native (on-premises)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Air-gapped deployment&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Fully supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML Conversion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML string to PDF&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL to PDF&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML file to PDF&lt;/td&gt;
&lt;td&gt;Via upload&lt;/td&gt;
&lt;td&gt;Yes (direct file access)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modern CSS support&lt;/td&gt;
&lt;td&gt;Server-side rendering (W3C compliant per vendor docs)&lt;/td&gt;
&lt;td&gt;Chromium-based engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript execution&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;javascript_time&lt;/code&gt; wait param)&lt;/td&gt;
&lt;td&gt;Yes (configurable render delay)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF Operations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge PDFs&lt;/td&gt;
&lt;td&gt;Not part of the conversion endpoint&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;PdfDocument.Merge&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split PDFs&lt;/td&gt;
&lt;td&gt;Not part of the conversion endpoint&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract text&lt;/td&gt;
&lt;td&gt;Not part of the conversion endpoint&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watermarks&lt;/td&gt;
&lt;td&gt;Via API parameters; verify tier availability&lt;/td&gt;
&lt;td&gt;Built-in (&lt;code&gt;ApplyWatermark&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital signatures&lt;/td&gt;
&lt;td&gt;Not part of the conversion endpoint&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;PdfSignature&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Network RT + processing&lt;/td&gt;
&lt;td&gt;Processing only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch processing&lt;/td&gt;
&lt;td&gt;API rate limits&lt;/td&gt;
&lt;td&gt;Hardware limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent operations&lt;/td&gt;
&lt;td&gt;Depends on tier/quota&lt;/td&gt;
&lt;td&gt;Depends on resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching&lt;/td&gt;
&lt;td&gt;Vendor-side caching not documented as a guarantee&lt;/td&gt;
&lt;td&gt;Application-controlled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET integration&lt;/td&gt;
&lt;td&gt;HttpClient/WebClient against &lt;code&gt;/api&lt;/code&gt;; optional &lt;code&gt;PDFmyURL.NET.dll&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Native NuGet package (&lt;code&gt;IronPdf&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async support&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HttpClient.PostAsync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sync and async (&lt;code&gt;RenderHtmlAsPdfAsync&lt;/code&gt; etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;td&gt;HTTP status codes / &lt;code&gt;WebException&lt;/code&gt; / &lt;code&gt;HttpRequestException&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Typed exceptions (&lt;code&gt;IronPdfRenderingException&lt;/code&gt;, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;Stringly-typed form parameters&lt;/td&gt;
&lt;td&gt;Strongly typed &lt;code&gt;RenderingOptions&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost model&lt;/td&gt;
&lt;td&gt;Monthly subscription tiers&lt;/td&gt;
&lt;td&gt;Per-license (perpetual option available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling&lt;/td&gt;
&lt;td&gt;Bounded by plan tier and vendor capacity&lt;/td&gt;
&lt;td&gt;Bounded by your own hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service dependency&lt;/td&gt;
&lt;td&gt;External SaaS on the critical path&lt;/td&gt;
&lt;td&gt;Runs in your process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support&lt;/td&gt;
&lt;td&gt;Email, tier-dependent&lt;/td&gt;
&lt;td&gt;Commercial support per IronPDF license&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Installation Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PDFmyURL Setup:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Option 1: Use HttpClient directly&lt;/span&gt;
dotnet add package System.Net.Http

&lt;span class="c"&gt;# Option 2: Use official wrapper (if available)&lt;/span&gt;
&lt;span class="c"&gt;# Download PDFmyURL.NET.dll from vendor site&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Net.Http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Make REST API calls with API key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IronPDF Setup:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package IronPdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&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;renderer&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;ChromePdfRenderer&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;pdf&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;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdfAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deployment Checklist Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PDFmyURL Deployment Checklist:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Verify API key is configured securely&lt;/li&gt;
&lt;li&gt;[ ] Ensure outbound HTTPS connectivity to pdfmyurl.com&lt;/li&gt;
&lt;li&gt;[ ] Configure HTTP client timeouts appropriately&lt;/li&gt;
&lt;li&gt;[ ] Implement retry logic for network failures&lt;/li&gt;
&lt;li&gt;[ ] Monitor API quota usage&lt;/li&gt;
&lt;li&gt;[ ] Review data transmission policies with legal/compliance&lt;/li&gt;
&lt;li&gt;[ ] Test network latency from production environment&lt;/li&gt;
&lt;li&gt;[ ] Plan for service outages (fallback strategy?)&lt;/li&gt;
&lt;li&gt;[ ] Evaluate costs at expected volume&lt;/li&gt;
&lt;li&gt;[ ] Confirm acceptable use policy compliance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  IronPDF Deployment Checklist:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Install NuGet package&lt;/li&gt;
&lt;li&gt;[ ] Set license key in application startup&lt;/li&gt;
&lt;li&gt;[ ] Ensure sufficient disk space (~200MB for library)&lt;/li&gt;
&lt;li&gt;[ ] Verify .NET version compatibility&lt;/li&gt;
&lt;li&gt;[ ] Configure memory limits for high-volume scenarios&lt;/li&gt;
&lt;li&gt;[ ] Test on target OS (Windows/Linux/macOS)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.toOptional"&gt; &lt;/a&gt; Configure custom Chrome executable path&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.toOptional"&gt; &lt;/a&gt; Set up load balancing if needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to Stay with PDFmyURL / When IronPDF is Better
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Consider staying with PDFmyURL if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have zero infrastructure and want someone else to manage PDF generation&lt;/li&gt;
&lt;li&gt;Your HTML content contains no sensitive or regulated data&lt;/li&gt;
&lt;li&gt;Conversion volume is low and occasional (not business-critical)&lt;/li&gt;
&lt;li&gt;Offline operation is never required&lt;/li&gt;
&lt;li&gt;Data sovereignty is not a concern&lt;/li&gt;
&lt;li&gt;Network latency is acceptable for your use case&lt;/li&gt;
&lt;li&gt;You prefer operational costs over capital expenses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IronPDF becomes necessary when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data contains PII, PHI, financial data, or regulated information&lt;/li&gt;
&lt;li&gt;Compliance requires on-premises processing (GDPR, HIPAA, SOX, FedRAMP)&lt;/li&gt;
&lt;li&gt;Air-gapped or private network deployment&lt;/li&gt;
&lt;li&gt;PDF generation is business-critical (cannot depend on external service)&lt;/li&gt;
&lt;li&gt;High-volume conversion where per-use costs become prohibitive&lt;/li&gt;
&lt;li&gt;Low-latency requirements (milliseconds matter)&lt;/li&gt;
&lt;li&gt;Offline or occasionally-connected scenarios&lt;/li&gt;
&lt;li&gt;Need full PDF manipulation (merge, split, extract, modify existing PDFs)&lt;/li&gt;
&lt;li&gt;Want architectural control over PDF generation infrastructure&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;PDFmyURL is a straightforward cloud API for HTML-to-PDF conversion that removes infrastructure management for teams comfortable sending document content to an external processor. For public content, marketing materials, or non-sensitive document generation, the model is a real deployment shortcut: no libraries to install, no servers to maintain, just HTTP calls that return PDFs. It typically works well in low-volume scenarios where data sensitivity is low and outbound network dependency is acceptable.&lt;/p&gt;

&lt;p&gt;The architectural fit changes when the application handles regulated data or needs to be operationally independent. A cloud API by definition transmits the source document to a third party, which can be incompatible with HIPAA, PCI-DSS, or GDPR scope unless the right DPA/BAA is in place. Even outside regulated workloads, the external dependency means your PDF step shares an availability budget with another company's service.&lt;/p&gt;

&lt;p&gt;IronPDF takes the opposite trade-off: rendering happens inside your own process, so the source HTML and the generated PDF stay within your security boundary. The library bundles a Chromium engine, runs locally on Windows/Linux/macOS, and removes the network from the conversion path itself. For applications where PDF generation is on the critical path rather than an occasional feature, local rendering keeps that step under your own SLAs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For teams evaluating cloud vs. on-premises:&lt;/strong&gt; Have data privacy regulations or service dependency concerns already eliminated cloud API options from consideration, or does your architecture permit external processing with appropriate contractual safeguards?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ironpdf.com/tutorials/html-to-pdf/" rel="noopener noreferrer"&gt;HTML to PDF Tutorial&lt;/a&gt; - Complete guide to on-premises HTML rendering&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ironpdf.com/object-reference/api/IronPdf.ChromePdfRenderer.html" rel="noopener noreferrer"&gt;ChromePdfRenderer API Documentation&lt;/a&gt; - Detailed method reference for local processing&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 6 - Wishlist or… “Dream Coins True”?</title>
      <dc:creator>Alena</dc:creator>
      <pubDate>Fri, 15 May 2026 15:07:18 +0000</pubDate>
      <link>https://dev.to/alenadevsoft/a-net-dinosaur-in-web3-day-6-wishlist-or-dream-coins-true-1kb9</link>
      <guid>https://dev.to/alenadevsoft/a-net-dinosaur-in-web3-day-6-wishlist-or-dream-coins-true-1kb9</guid>
      <description>&lt;p&gt;There's something that happened between Day 5 and Day 6 during brainstorming about project ideas. Day 6 wasn't just a deploy session — it was the first day of building something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Brainstorming started as a strategy question — I needed a practice project. Something to learn the full modern Web3 stack and something I could build in public.&lt;/p&gt;

&lt;p&gt;I already have another idea that I've started building, but until the MVP is ready — I decided to keep it under wraps.&lt;/p&gt;

&lt;p&gt;The idea was simple: pick something "common", but complex enough to cover real technical ground.&lt;/p&gt;

&lt;p&gt;Well… I'm a dreamer. So the wishlist idea just felt right.&lt;/p&gt;

&lt;p&gt;I started thinking — is it possible to build something interesting around it? Maybe the idea isn't new. It doesn't really matter… I just like it.&lt;/p&gt;

&lt;p&gt;Take a wishlist, let people make each other's dreams come true and… maybe build something on-chain around it.&lt;/p&gt;

&lt;p&gt;The base Wishlist contract was already written. It was already there — a working smart contract with a React frontend deployed on Sepolia.&lt;/p&gt;

&lt;p&gt;So why not just build something on top of it?&lt;/p&gt;

&lt;p&gt;Multi-user goals, ETH donations for specific goals, a Telegram bot for notifications. A proper monorepo with contract, Supabase, Drizzle, Next.js, and more.&lt;/p&gt;

&lt;p&gt;And then the idea shifted.&lt;/p&gt;

&lt;p&gt;What if every donation increases the "strength" of something?&lt;/p&gt;

&lt;p&gt;A global counter. An on-chain kind of energy. A number that grows every time someone believes in someone else's dream enough to send ETH.&lt;/p&gt;

&lt;p&gt;The working name became WishList Chain (WSHL) — not a token, at least not yet. An on-chain power score. &lt;code&gt;totalDreamPower&lt;/code&gt; in the contract. Every donation adds to it. Every goal has its own dream power.&lt;/p&gt;

&lt;p&gt;(DreamCoin, the name I originally wanted, is apparently already taken — so I had to rethink pretty quickly.)&lt;/p&gt;

&lt;p&gt;Is it a real product? Possibly. Is it a learning project? Definitely. Is the idea somewhat ridiculous? Yes, and that's exactly why it might work in crypto.&lt;/p&gt;

&lt;p&gt;The project is at &lt;a href="https://github.com/alena-dev-soft/wish-list-chain" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/wish-list-chain&lt;/a&gt; — open source.&lt;/p&gt;

&lt;p&gt;Day 6 was about laying the foundation for all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;Write the core contract for WishList Chain — version 3 of the original Wishlist.sol.&lt;/p&gt;

&lt;p&gt;Multi-user goals, ETH donations, &lt;code&gt;dreamPower&lt;/code&gt; accumulation per goal, &lt;code&gt;totalDreamPower&lt;/code&gt; globally, and a &lt;code&gt;DreamPowerIncreased&lt;/code&gt; event for future Alchemy webhooks. Deploy with Hardhat. Verify on Etherscan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contract
&lt;/h2&gt;

&lt;p&gt;WishlistV3 is a different architecture from V2. V2 was one owner, one wishlist. V3 is a shared contract where every wallet has its own goals.&lt;/p&gt;

&lt;p&gt;The .NET analogy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// V2&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WishItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;wishes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// V3&lt;/span&gt;
&lt;span class="nc"&gt;Dictionary&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Goal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;goals&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Solidity: &lt;code&gt;mapping(address =&amp;gt; Goal[]) public goals&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;struct Goal&lt;/code&gt; gained financial fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Goal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&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="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;targetAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;currentAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;dreamPower&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFulfilled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;donate()&lt;/code&gt; is the core function. Two things worth noting:&lt;/p&gt;

&lt;p&gt;It must be &lt;code&gt;payable&lt;/code&gt; — without that modifier, Solidity won't accept ETH. The keyword is required, not optional.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Goal storage goal = goals[_owner][_goalIndex]&lt;/code&gt; — &lt;code&gt;storage&lt;/code&gt; means you're working with the actual on-chain data, not a copy. Change it, and the state changes. Use &lt;code&gt;memory&lt;/code&gt; instead, and you're editing a local copy that disappears after the function returns. This distinction doesn't exist in .NET — it's specific to the EVM execution model.&lt;/p&gt;

&lt;p&gt;The event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;DreamPowerIncreased&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt; &lt;span class="n"&gt;goalIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;addedPower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;newTotalPower&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;indexed&lt;/code&gt; parameters are searchable in logs. They become filter keys — Alchemy webhooks can listen for specific owners or goal indices without scanning every event. The non-indexed parameters are just data payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardhat
&lt;/h2&gt;

&lt;p&gt;Moving from Remix to Hardhat is the next step in my Web3 journey. Remix is a great tool for exploration. Hardhat provides everything needed out of the box, including a proper build pipeline.&lt;/p&gt;

&lt;p&gt;The setup that actually works with Hardhat 3 + viem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; hardhat@latest @nomicfoundation/hardhat-toolbox-viem@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I really like this part — deploy and verify in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hardhat ignition deploy ignition/modules/WishlistV3.ts &lt;span class="nt"&gt;--network&lt;/span&gt; sepolia &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fair warning: getting there involved the usual package dependency hell — wrong versions, renamed config keys, cached artifacts. Nothing mysterious, just the standard tax you pay any time you touch a stack that lives and dies by npm. Read the error codes, fix one thing at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;WishlistV3 is not just a learning exercise. It's the core contract for something that's being built. The &lt;code&gt;donate()&lt;/code&gt; function, the &lt;code&gt;DreamPowerIncreased&lt;/code&gt; event, the multi-user architecture — all of it is designed for a real use case that will be revealed when the MVP is ready.&lt;/p&gt;

&lt;p&gt;Two-week sprint. This was the foundation.&lt;/p&gt;

&lt;p&gt;Contract address: &lt;code&gt;0x90de4a1934d0B062423adAEeDEe37Bb6fD12D0Ca&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Verified: &lt;a href="https://sepolia.etherscan.io/address/0x90de4a1934d0B062423adAEeDEe37Bb6fD12D0Ca" rel="noopener noreferrer"&gt;sepolia.etherscan.io/address/0x90de4a1934d0B062423adAEeDEe37Bb6fD12D0Ca&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — dependency hell survived. Foundation is live.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>dotnet</category>
      <category>beginners</category>
      <category>ethereum</category>
    </item>
    <item>
      <title>Gnostice PDFOne .NET vs IronPDF: side by side for .NET</title>
      <dc:creator>IronSoftware</dc:creator>
      <pubDate>Fri, 15 May 2026 13:13:00 +0000</pubDate>
      <link>https://dev.to/ironsoftware/gnostice-pdfone-net-vs-ironpdf-side-by-side-for-net-4k4j</link>
      <guid>https://dev.to/ironsoftware/gnostice-pdfone-net-vs-ironpdf-side-by-side-for-net-4k4j</guid>
      <description>&lt;p&gt;The legacy &lt;code&gt;PDFOne.NET&lt;/code&gt; NuGet package is now marked deprecated (last release 24.1.60, July 1, 2024), and Gnostice now directs new work to the &lt;code&gt;Gnostice.DocumentStudio.*&lt;/code&gt; package family. For teams currently on PDFOne .NET, this is a natural inflection point — a Gnostice-to-Gnostice move to Document Studio is itself a migration, so it is worth re-evaluating the broader PDF library landscape at the same time. PDFOne .NET served as a coordinate-based PDF creation and manipulation library — teams built documents by specifying exact positions, drawing text blocks, and manually calculating layouts.&lt;/p&gt;

&lt;p&gt;IronPDF takes a fundamentally different approach: HTML and CSS define document structure, with a Chrome rendering engine handling layout calculations automatically. This architectural distinction affects development velocity, maintenance overhead, and the types of documents each library handles efficiently. The comparison below focuses on measurable characteristics—API surface area, code volume, and workflow patterns—that impact project timelines and long-term maintainability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding IronPDF
&lt;/h2&gt;

&lt;p&gt;IronPDF centers on HTML-to-PDF conversion using an integrated Chromium rendering engine. Development teams leverage existing HTML, CSS, and JavaScript skills rather than learning PDF coordinate systems. The library supports modern web standards: CSS Grid, Flexbox, custom fonts, responsive layouts, and JavaScript execution before rendering.&lt;/p&gt;

&lt;p&gt;Beyond HTML rendering, IronPDF provides standard PDF operations through a streamlined API: merging documents, splitting files, extracting text/images, form handling, digital signatures, and encryption. The library targets .NET Framework 4.6.2+, .NET Core 3.1+, and .NET 6-10 with consistent cross-platform behavior. For comprehensive rendering options, see &lt;a href="https://ironsoftware.com/suite/blog/using-ironsuite/html-to-pdf-ironpdf-tutorial/" rel="noopener noreferrer"&gt;HTML to PDF Conversion Tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural Constraints of Gnostice PDFOne .NET
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Product Status
&lt;/h3&gt;

&lt;p&gt;PDFOne .NET is now in legacy maintenance — the &lt;code&gt;PDFOne.NET&lt;/code&gt; NuGet package's last release is 24.1.60 (July 1, 2024), and the vendor recommends &lt;code&gt;Gnostice.DocumentStudio.*&lt;/code&gt; packages for new work. Existing PDFOne .NET licenses remain functional, but teams may want to verify what feature updates and .NET version coverage are planned against the vendor's current roadmap before starting new projects on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing Capabilities
&lt;/h3&gt;

&lt;p&gt;PDFOne .NET does not ship native HTML-to-PDF conversion. Document generation requires manual coordinate calculations for every text block, image, and shape. Teams build custom layout helpers or accept rigid, code-heavy document structures. Dynamic content from databases or user input means recalculating positions programmatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Trade-offs
&lt;/h3&gt;

&lt;p&gt;Coordinate-based APIs make layout changes propagate. Adding a single line of text to a header typically means adjusting Y-coordinates for every subsequent element. Multi-column layouts, tables with dynamic row counts, and page breaks across long content are application-level concerns. Text wrapping, font metrics, and overflow handling sit with the developer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Support Status
&lt;/h3&gt;

&lt;p&gt;With PDFOne .NET in legacy maintenance, Gnostice support traffic increasingly points new customers toward Document Studio .NET. Teams maintaining PDFOne .NET applications should plan for an eventual Gnostice-to-Document-Studio or Gnostice-to-alternative move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;The library's design reflects pre-HTML5 document generation patterns. PDF coordinates originate from bottom-left corners (PostScript convention), which can create cognitive overhead for developers more familiar with top-left origin systems. There is no CSS styling, no responsive breakpoint model, and no separation of content from presentation — every document change is a code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Comparison Overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Gnostice PDFOne .NET&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Current Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Legacy (PDFOne.NET deprecated; last release 24.1.60, Jul 2024)&lt;/td&gt;
&lt;td&gt;Active with continuous updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None natively&lt;/td&gt;
&lt;td&gt;Chromium-based rendering engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layout Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual coordinate drawing&lt;/td&gt;
&lt;td&gt;CSS-driven layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Installation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PDFOne.NET&lt;/code&gt; NuGet (deprecated)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IronPdf&lt;/code&gt; NuGet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vendor redirects new work to Document Studio .NET&lt;/td&gt;
&lt;td&gt;Live chat, email, tickets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Successor Path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Gnostice.DocumentStudio.*&lt;/code&gt; packages&lt;/td&gt;
&lt;td&gt;Continues as &lt;code&gt;IronPdf&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Code Comparison: Efficiency Metrics
&lt;/h2&gt;

&lt;p&gt;The following sections compare document generation workflows, measuring code verbosity, maintenance overhead, and adaptability to requirement changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gnostice PDFOne .NET — Creating a Simple Invoice
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NuGet: Install-Package PDFOne.NET (deprecated)&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Gnostice.PDFOne&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;Gnostice.PDFOne.Graphics&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;System&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;System.Drawing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfOneInvoiceGenerator&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;void&lt;/span&gt; &lt;span class="nf"&gt;CreateInvoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Initialize PDF document&lt;/span&gt;
            &lt;span class="n"&gt;PDFDocument&lt;/span&gt; &lt;span class="n"&gt;document&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;PDFPage&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Font objects per text style&lt;/span&gt;
            &lt;span class="n"&gt;PDFFont&lt;/span&gt; &lt;span class="n"&gt;titleFont&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;PDFFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PDFStandardFont&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Helvetica&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;PDFFont&lt;/span&gt; &lt;span class="n"&gt;bodyFont&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;PDFFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PDFStandardFont&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Helvetica&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;PDFFont&lt;/span&gt; &lt;span class="n"&gt;boldFont&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;PDFFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PDFStandardFont&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Helvetica&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Manual coordinate calculations (origin: bottom-left)&lt;/span&gt;
            &lt;span class="c1"&gt;// Page height is ~792 points (11 inches at 72 DPI)&lt;/span&gt;
            &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;750&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Start near top&lt;/span&gt;

            &lt;span class="c1"&gt;// Helper: build and draw a text element at (x, y)&lt;/span&gt;
            &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Draw&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;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PDFFont&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;y&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;element&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;PDFTextElement&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;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Font&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Black&lt;/span&gt;
                &lt;span class="p"&gt;};&lt;/span&gt;
                &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invoice #12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;titleFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Date: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;yyyy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;MM&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dd&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;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Customer: Acme Corporation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Address: 123 Business St"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// Table header (manual column tracking)&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;450&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// Line items — each requires manual positioning&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Professional Services"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"40"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$150.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$6,000.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;450&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Consulting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$200.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$2,000.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bodyFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;450&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;yPosition&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// Total&lt;/span&gt;
            &lt;span class="nf"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Total: $8,000.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;boldFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Save document&lt;/span&gt;
            &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invoice_pdfone.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&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;"Invoice created: invoice_pdfone.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&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;$"Error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&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;&lt;strong&gt;Performance &amp;amp; Maintainability Metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lines of code: 64 lines for basic invoice&lt;/li&gt;
&lt;li&gt;Manual Y-coordinate calculations: 12 explicit position updates&lt;/li&gt;
&lt;li&gt;Font objects created: 4 instances requiring disposal&lt;/li&gt;
&lt;li&gt;Table column alignment: Manual calculation for each column&lt;/li&gt;
&lt;li&gt;Adding one new line item: Adjust 8+ subsequent Y-coordinates&lt;/li&gt;
&lt;li&gt;Responsive layout support: None (fixed positions)&lt;/li&gt;
&lt;li&gt;Change request impact: High (coordinate cascade effect)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Technical limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No text wrapping—long strings overflow page boundaries&lt;/li&gt;
&lt;li&gt;No automatic page breaks for multi-page documents&lt;/li&gt;
&lt;li&gt;Font size changes require recalculating all subsequent positions&lt;/li&gt;
&lt;li&gt;Dynamic table row counts need complex loop logic with position tracking&lt;/li&gt;
&lt;li&gt;RTL languages (Arabic, Hebrew) require custom text rendering&lt;/li&gt;
&lt;li&gt;No CSS styling or separation of content from presentation&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  IronPDF — Creating a Simple Invoice
&lt;/h3&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;IronPdf&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;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IronPdfInvoiceGenerator&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;void&lt;/span&gt; &lt;span class="nf"&gt;CreateInvoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&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;renderer&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;ChromePdfRenderer&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;htmlContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$@"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;        &amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;        &amp;lt;html&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;        &amp;lt;head&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;style&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                body &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Arial&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                h1 &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                .info &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="n"&gt;px&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;font&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                table &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;%;&lt;/span&gt; &lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;collapse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;collapse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                th &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;f0f0f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt; &lt;span class="n"&gt;solid&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="m"&gt;000&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;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                td &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt; &lt;span class="n"&gt;solid&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;ddd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                .total &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="n"&gt;px&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;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;/style&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;        &amp;lt;/head&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;        &amp;lt;body&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;h1&amp;gt;Invoice #12345&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;div class='info'&amp;gt;Date: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;yyyy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;MM&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;div class='info'&amp;gt;Customer: Acme Corporation&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;div class='info'&amp;gt;Address: 123 Business St&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;table&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                &amp;lt;thead&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                    &amp;lt;tr&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;th&amp;gt;Item&amp;lt;/th&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;th&amp;gt;Quantity&amp;lt;/th&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;th&amp;gt;Price&amp;lt;/th&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;th&amp;gt;Total&amp;lt;/th&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                    &amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                &amp;lt;/thead&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                &amp;lt;tbody&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                    &amp;lt;tr&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;Professional Services&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;40&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;$150.00&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;$6,000.00&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                    &amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                    &amp;lt;tr&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;Consulting&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;10&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;$200.00&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                        &amp;lt;td&amp;gt;$2,000.00&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                    &amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;                &amp;lt;/tbody&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="s"&gt;            &amp;lt;div class='total'&amp;gt;Total: $8,000.00&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;        &amp;lt;/body&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;        &amp;lt;/html&amp;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;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;htmlContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invoice_ironpdf.pdf"&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;"Invoice created: invoice_ironpdf.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance &amp;amp; Maintainability Metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lines of code: 42 lines for same invoice (34% less code)&lt;/li&gt;
&lt;li&gt;Manual coordinate calculations: 0 (CSS handles layout)&lt;/li&gt;
&lt;li&gt;Font management: Automatic via CSS&lt;/li&gt;
&lt;li&gt;Table rendering: Native HTML &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; with automatic alignment&lt;/li&gt;
&lt;li&gt;Adding one new line item: Insert single &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; element (no position recalculation)&lt;/li&gt;
&lt;li&gt;Responsive layout: Built-in CSS media queries and flexible layouts&lt;/li&gt;
&lt;li&gt;Change request impact: Low (modify HTML/CSS independently of logic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production templates with Razor views or external CSS files, see &lt;a href="https://ironsoftware.com/suite/blog/using-ironsuite/cshtml-to-pdf-tutorial/" rel="noopener noreferrer"&gt;CSHTML to PDF Tutorial&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Gnostice PDFOne .NET — Merging PDFs with Form Preservation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NuGet: Install-Package PDFOne.NET (deprecated)&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Gnostice.PDFOne&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;Gnostice.PDFOne.Document&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;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfOneMerger&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;void&lt;/span&gt; &lt;span class="nf"&gt;MergePdfsWithForms&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Load source documents&lt;/span&gt;
            &lt;span class="n"&gt;PDFDocument&lt;/span&gt; &lt;span class="n"&gt;doc1&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;doc1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"form_part1.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;PDFDocument&lt;/span&gt; &lt;span class="n"&gt;doc2&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;doc2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"form_part2.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Create destination and append each source&lt;/span&gt;
            &lt;span class="n"&gt;PDFDocument&lt;/span&gt; &lt;span class="n"&gt;mergedDoc&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;mergedDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;mergedDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;mergedDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;mergedDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"merged_forms.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Explicit cleanup of each document&lt;/span&gt;
            &lt;span class="n"&gt;doc1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;doc2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;mergedDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&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;"Merge completed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&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;$"Merge error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&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;&lt;strong&gt;Workflow notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each source document is opened, appended, and closed explicitly&lt;/li&gt;
&lt;li&gt;Form-field name collisions across the appended documents are the developer's responsibility to detect and rename — confirm against your AcroForm field set&lt;/li&gt;
&lt;li&gt;Three &lt;code&gt;PDFDocument&lt;/code&gt; instances need explicit &lt;code&gt;Close()&lt;/code&gt; to release resources&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  IronPDF — Merging PDFs with Form Preservation
&lt;/h3&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;IronPdf&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;System&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;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IronPdfMerger&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;void&lt;/span&gt; &lt;span class="nf"&gt;MergePdfsWithForms&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&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;docs&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PdfDocument&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;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"form_part1.pdf"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"form_part2.pdf"&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;merged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"merged_forms.pdf"&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;"Merge completed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance &amp;amp; Maintainability Metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic form field conflict resolution&lt;/li&gt;
&lt;li&gt;Single method call handles all merge logic&lt;/li&gt;
&lt;li&gt;Automatic resource management&lt;/li&gt;
&lt;li&gt;Preserves bookmarks, annotations, and metadata&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Gnostice PDFOne .NET — Extracting Text from Multi-Page Document
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NuGet: Install-Package PDFOne.NET (deprecated)&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Gnostice.PDFOne&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;Gnostice.PDFOne.Document&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;System&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;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfOneTextExtractor&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;void&lt;/span&gt; &lt;span class="nf"&gt;ExtractAllText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PDFDocument&lt;/span&gt; &lt;span class="n"&gt;document&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"report.pdf"&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;extractedText&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;StringBuilder&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;pageCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// PDFOne page indices are typically 1-based&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;pageIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;pageIndex&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;pageIndex&lt;/span&gt;&lt;span class="p"&gt;++)&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;pageText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPageText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="n"&gt;extractedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"--- Page &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pageIndex&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;extractedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;extractedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&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;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"extracted.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extractedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&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;$"Extracted text from &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; pages"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&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;$"Extraction error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&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;&lt;strong&gt;Workflow notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual per-page iteration required to assemble the document text&lt;/li&gt;
&lt;li&gt;Page indices are 1-based (vs. 0-based in IronPDF) — easy off-by-one when porting code&lt;/li&gt;
&lt;li&gt;Column layouts and table structure are not preserved in the linear text output&lt;/li&gt;
&lt;li&gt;Non-standard or embedded fonts can affect text decoding fidelity&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  IronPDF — Extracting Text from Multi-Page Document
&lt;/h3&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;IronPdf&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;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IronPdfTextExtractor&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;void&lt;/span&gt; &lt;span class="nf"&gt;ExtractAllText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&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;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"report.pdf"&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;allText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExtractAllText&lt;/span&gt;&lt;span class="p"&gt;();&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;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"extracted.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allText&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;"Text extracted successfully"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single method handles all pages&lt;/li&gt;
&lt;li&gt;Optimized memory usage for large documents&lt;/li&gt;
&lt;li&gt;Maintains reading order across complex layouts&lt;/li&gt;
&lt;li&gt;Built-in encoding normalization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more details on PDF manipulation operations, see &lt;a href="https://ironsoftware.com/suite/blog/using-ironsuite/csharp-create-pdf-ironpdf-tutorial/" rel="noopener noreferrer"&gt;IronPDF PDF Creation Guide&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  API Mapping Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gnostice PDFOne Operation&lt;/th&gt;
&lt;th&gt;IronPDF Equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new PDFDocument(); doc.Load(path)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PdfDocument.FromFile(path)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;document.Pages.Add()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTML rendering creates pages automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PDFTextElement.Draw(page, x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTML elements and CSS styling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual coordinate calculations&lt;/td&gt;
&lt;td&gt;CSS layout engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doc.Append(other)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PdfDocument.Merge(pdf1, pdf2)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;doc.GetPageText(i)&lt;/code&gt; (1-based)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pdf.ExtractAllText()&lt;/code&gt; / &lt;code&gt;pdf.ExtractTextFromPage(i)&lt;/code&gt; (0-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual form field creation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CreatePdfFormsFromHtml&lt;/code&gt; option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doc.Save(path); doc.Close()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf.SaveAs(path)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChromePdfRenderer.RenderHtmlAsPdf()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChromePdfRenderer.RenderUrlAsPdf()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;JavaScript execution before PDF generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;Responsive CSS rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doc.SetEncryption(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf.SecuritySettings.UserPassword = "..."&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Comprehensive Feature Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Status &amp;amp; Support
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Gnostice PDFOne .NET&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Current Development Status&lt;/td&gt;
&lt;td&gt;Legacy maintenance (last NuGet release Jul 2024)&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successor Product&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Gnostice.DocumentStudio.*&lt;/code&gt; packages&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 6+ Support&lt;/td&gt;
&lt;td&gt;Verify against vendor matrix&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET Core Support&lt;/td&gt;
&lt;td&gt;Yes (legacy)&lt;/td&gt;
&lt;td&gt;Yes (.NET Core 3.1+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET Framework Support&lt;/td&gt;
&lt;td&gt;4.6.2+&lt;/td&gt;
&lt;td&gt;4.6.2+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official Documentation&lt;/td&gt;
&lt;td&gt;Available; PDFOne section in maintenance&lt;/td&gt;
&lt;td&gt;Comprehensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support Channels&lt;/td&gt;
&lt;td&gt;Vendor redirects new work to Document Studio&lt;/td&gt;
&lt;td&gt;Live chat, email, tickets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Future Updates&lt;/td&gt;
&lt;td&gt;Direction is Document Studio .NET&lt;/td&gt;
&lt;td&gt;Continuous delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Content Creation
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Gnostice PDFOne .NET&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML to PDF&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Native Chrome engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coordinate-Based Drawing&lt;/td&gt;
&lt;td&gt;Primary method&lt;/td&gt;
&lt;td&gt;Alternative API available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL to PDF&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RenderUrlAsPdf()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template-Based Generation&lt;/td&gt;
&lt;td&gt;Manual code&lt;/td&gt;
&lt;td&gt;HTML templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic Content Layouts&lt;/td&gt;
&lt;td&gt;Requires custom logic&lt;/td&gt;
&lt;td&gt;CSS Flexbox, Grid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text Wrapping&lt;/td&gt;
&lt;td&gt;Manual calculation&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Column Layouts&lt;/td&gt;
&lt;td&gt;Custom implementation&lt;/td&gt;
&lt;td&gt;CSS columns or Grid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Responsive Design&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;CSS media queries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  PDF Operations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Gnostice PDFOne .NET&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merge PDFs&lt;/td&gt;
&lt;td&gt;Yes (manual iteration)&lt;/td&gt;
&lt;td&gt;Yes (single method)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split PDFs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract Text&lt;/td&gt;
&lt;td&gt;Yes (page-by-page)&lt;/td&gt;
&lt;td&gt;Yes (optimized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract Images&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form Field Creation&lt;/td&gt;
&lt;td&gt;Yes (manual)&lt;/td&gt;
&lt;td&gt;From HTML forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form Data Filling&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annotations&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital Signatures&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page Manipulation&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Code Complexity Metrics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Gnostice PDFOne .NET&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic Invoice (LOC)&lt;/td&gt;
&lt;td&gt;64 lines&lt;/td&gt;
&lt;td&gt;42 lines (34% reduction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coordinate Calculations&lt;/td&gt;
&lt;td&gt;Manual (frequent)&lt;/td&gt;
&lt;td&gt;None (CSS-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Font Management&lt;/td&gt;
&lt;td&gt;Explicit creation&lt;/td&gt;
&lt;td&gt;CSS declarations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table Generation&lt;/td&gt;
&lt;td&gt;Manual drawing&lt;/td&gt;
&lt;td&gt;HTML &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic Content&lt;/td&gt;
&lt;td&gt;Complex (position tracking)&lt;/td&gt;
&lt;td&gt;Simple (data binding)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance Overhead&lt;/td&gt;
&lt;td&gt;High (coordinate dependencies)&lt;/td&gt;
&lt;td&gt;Low (declarative layouts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Known Trade-offs
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Product-Specific Considerations
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Gnostice PDFOne .NET:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PDFOne.NET&lt;/code&gt; package is in legacy maintenance; new work is directed to &lt;code&gt;Gnostice.DocumentStudio.*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Moving forward on Gnostice means migrating to Document Studio (a different API surface)&lt;/li&gt;
&lt;li&gt;Coordinate-based API; layout changes propagate through subsequent positions&lt;/li&gt;
&lt;li&gt;No native HTML rendering — text wrapping, overflow, and page breaks are application-level concerns&lt;/li&gt;
&lt;li&gt;Font-metric handling sits with the developer for precise layouts&lt;/li&gt;
&lt;li&gt;AcroForm field-name collisions during multi-document merges are the developer's responsibility to resolve&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IronPDF:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial Chromium engine startup adds a one-time warm-up cost (amortized in long-running processes)&lt;/li&gt;
&lt;li&gt;Linux deployments may require &lt;code&gt;libgdiplus&lt;/code&gt; for certain image operations&lt;/li&gt;
&lt;li&gt;Docker containers should be sized to accommodate the bundled Chromium&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Installation Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Gnostice PDFOne .NET Installation (Legacy)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PDFOne.NET is deprecated on NuGet (last release 24.1.60, Jul 2024)&lt;/span&gt;
Install-Package PDFOne.NET

&lt;span class="c"&gt;# Current Gnostice direction for new work:&lt;/span&gt;
&lt;span class="c"&gt;# dotnet add package Gnostice.DocumentStudio.WinForms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Gnostice.PDFOne&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// License key for PDFOne (set via the static PDFOne class)&lt;/span&gt;
&lt;span class="n"&gt;Gnostice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PDFOne&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PDFOne&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-GNOSTICE-LICENSE"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Migration Notice:&lt;/strong&gt;&lt;br&gt;
Teams on PDFOne .NET should evaluate Document Studio .NET (Gnostice's current product line) or alternative PDF libraries for future projects.&lt;/p&gt;
&lt;h3&gt;
  
  
  IronPDF Installation
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# NuGet Package Manager&lt;/span&gt;
Install-Package IronPdf

&lt;span class="c"&gt;# .NET CLI&lt;/span&gt;
dotnet add package IronPdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Optional license configuration&lt;/span&gt;
&lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Benchmark Summary
&lt;/h2&gt;

&lt;p&gt;Based on common document generation scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;PDFOne .NET (Coordinate-Based)&lt;/th&gt;
&lt;th&gt;IronPDF (HTML-Based)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple invoice (3 items)&lt;/td&gt;
&lt;td&gt;~64 LOC&lt;/td&gt;
&lt;td&gt;~42 LOC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex report (50 items, tables)&lt;/td&gt;
&lt;td&gt;~400+ LOC&lt;/td&gt;
&lt;td&gt;~150 LOC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development time (initial)&lt;/td&gt;
&lt;td&gt;~8 hours&lt;/td&gt;
&lt;td&gt;~3 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change request (add field)&lt;/td&gt;
&lt;td&gt;Adjust 10-20 coordinates&lt;/td&gt;
&lt;td&gt;Modify HTML template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-page document handling&lt;/td&gt;
&lt;td&gt;Custom pagination logic&lt;/td&gt;
&lt;td&gt;Automatic page breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance burden&lt;/td&gt;
&lt;td&gt;High (coordinate cascade)&lt;/td&gt;
&lt;td&gt;Low (CSS separation)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: Metrics based on typical development workflows. Actual performance varies by team experience and document complexity.&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;Gnostice PDFOne .NET served as a traditional PDF generation library, emphasizing precise control over document structure through coordinate-based APIs. For teams with specific requirements around low-level PDF manipulation or applications already on PDFOne, the library has provided reliable functionality. The &lt;code&gt;PDFOne.NET&lt;/code&gt; package is now in legacy maintenance on NuGet, and Gnostice directs new development to the &lt;code&gt;Gnostice.DocumentStudio.*&lt;/code&gt; product line.&lt;/p&gt;

&lt;p&gt;The coordinate-based approach, while offering fine-grained control, comes with maintenance overhead. Layout changes typically require recalculating positions for subsequent elements. Complex tables, multi-column layouts, and responsive content demand custom logic that HTML/CSS engines handle automatically. Development time often goes into font metrics, text wrapping, and page-break logic — infrastructure concerns that modern rendering engines abstract away.&lt;/p&gt;

&lt;p&gt;IronPDF's HTML-based approach can reduce code volume substantially for typical document generation tasks (the invoice example above is about a third shorter). Maintenance overhead drops further because HTML templates separate content from presentation. Adding a header, resizing a table, or changing fonts is a CSS change, not coordinate recalculations across the codebase. For teams building .NET applications where PDF generation is a feature rather than the core business, that architectural difference often translates to faster iteration and lower long-term maintenance costs.&lt;/p&gt;

&lt;p&gt;With PDFOne .NET in legacy maintenance and Document Studio .NET representing a different API surface, teams already face a migration decision. IronPDF's HTML-first design aligns with the web skills most .NET teams already have, which can shorten the learning curve when evaluating alternatives.&lt;/p&gt;

&lt;p&gt;What percentage of your PDF generation code involves manual positioning versus content definition? Share your experience with coordinate-based vs. template-based document workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ironsoftware.com/suite/blog/using-ironsuite/html-to-pdf-ironpdf-tutorial/" rel="noopener noreferrer"&gt;HTML to PDF Conversion with IronPDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ironsoftware.com/object-reference/api/IronPdf.IPdfRenderOptions.html" rel="noopener noreferrer"&gt;IronPDF API Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>PdfiumViewer vs IronPDF: a technical breakdown for 2026</title>
      <dc:creator>IronSoftware</dc:creator>
      <pubDate>Fri, 15 May 2026 12:12:00 +0000</pubDate>
      <link>https://dev.to/ironsoftware/pdfiumviewer-vs-ironpdf-a-technical-breakdown-for-2026-1cnm</link>
      <guid>https://dev.to/ironsoftware/pdfiumviewer-vs-ironpdf-a-technical-breakdown-for-2026-1cnm</guid>
      <description>&lt;p&gt;Viewer control or headless generation? It is one of the first architectural forks a .NET team hits when PDF requirements start expanding. PdfiumViewer is built for the first answer — a WinForms/WPF control that wraps Google's PDFium and renders pages to the screen, no browser dependency, native binaries shipped via separate NuGet packages. The moment the roadmap adds "generate invoices from data" or "produce reports server-side," that architecture stops carrying the workload, and a second library has to join the project — or replace it.&lt;/p&gt;

&lt;p&gt;PdfiumViewer is a WinForms and WPF control for displaying PDF documents in desktop applications, built on Google's PDFium library (the same engine Chrome uses for PDFs). The original &lt;a href="https://github.com/pvginkel/PdfiumViewer" rel="noopener noreferrer"&gt;pvginkel/PdfiumViewer&lt;/a&gt; repository was archived on 2019-08-02 and the last NuGet release (2.13.0) shipped on 2017-11-06, so the library still works for the cases it was designed for but is not receiving updates from the original maintainer. Community forks such as bezzad/PdfiumViewer and PdfiumViewer.Updated continue some development for newer .NET targets — APIs and features can vary between forks. The library exposes a &lt;code&gt;PdfRenderer&lt;/code&gt; control for low-level page rendering, a &lt;code&gt;PdfViewer&lt;/code&gt; control with built-in toolbar, zoom, and navigation, plus page-level text extraction via &lt;code&gt;PdfDocument.GetPdfText(int page)&lt;/code&gt;. It does not generate PDFs from HTML, URLs, or templates — that is outside its scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding IronPDF
&lt;/h2&gt;

&lt;p&gt;IronPDF is a PDF generation and manipulation library, not a viewer control. The core workflow is &lt;a href="https://ironpdf.com/tutorials/html-to-pdf/" rel="noopener noreferrer"&gt;creating PDFs from HTML&lt;/a&gt; using a Chromium rendering engine, so the output tracks what Chrome would print. IronPDF covers the document lifecycle: generate from HTML, URLs, or files; manipulate (merge, split, forms, signatures, encryption); extract content (text, images, metadata); and print to physical printers. It does not ship a WinForms/WPF viewer UI control — that is intentionally outside its scope. IronPDF runs on .NET Framework 4.6.2+, .NET Core 3.1+, and modern .NET releases, with cross-platform support for Windows, Linux, macOS, Docker, and common cloud environments. Installation is a single NuGet package with embedded native dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Limitations of PdfiumViewer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Product Status
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Upstream archived: &lt;code&gt;pvginkel/PdfiumViewer&lt;/code&gt; was marked read-only on 2019-08-02; last NuGet release (2.13.0) is from 2017-11-06&lt;/li&gt;
&lt;li&gt;Community forks: multiple versions exist (bezzad/PdfiumViewer, PdfiumViewer.Updated, others) with different features and APIs&lt;/li&gt;
&lt;li&gt;Windows-focused: original docs reference Windows XP through 8; modern OS coverage varies by fork&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Missing Capabilities
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No PDF generation from HTML, URLs, or templates&lt;/li&gt;
&lt;li&gt;No server-side or headless document creation as a primary use case (the library is designed around UI controls)&lt;/li&gt;
&lt;li&gt;No PDF manipulation beyond viewing (no merge, split, edit, watermarks)&lt;/li&gt;
&lt;li&gt;No programmatic form filling or extraction&lt;/li&gt;
&lt;li&gt;No digital signatures or encryption APIs&lt;/li&gt;
&lt;li&gt;Text extraction is page-level only: &lt;code&gt;PdfDocument.GetPdfText(int page)&lt;/code&gt; returns raw page text with no per-word coordinates, no layout reconstruction, and no OCR for image-only pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Technical Considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Synchronous load and render: &lt;code&gt;PdfDocument.Load&lt;/code&gt; and &lt;code&gt;Render&lt;/code&gt; run on the calling thread, which can freeze WinForms/WPF UI for large documents unless dispatched off the UI thread&lt;/li&gt;
&lt;li&gt;Native DLL dependency: requires platform-specific &lt;code&gt;pdfium.dll&lt;/code&gt; deployment via &lt;code&gt;PdfiumViewer.Native.*&lt;/code&gt; packages&lt;/li&gt;
&lt;li&gt;Memory profile: documents are loaded for viewer display rather than streamed for backend processing&lt;/li&gt;
&lt;li&gt;Limited async surface: most operations are synchronous&lt;/li&gt;
&lt;li&gt;Fork compatibility: code written for the original may need adjustment on community forks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Desktop-oriented: tightly coupled to WinForms/WPF and &lt;code&gt;System.Drawing&lt;/code&gt;, which is Windows-only on .NET 6+&lt;/li&gt;
&lt;li&gt;Viewer-first design: no headless generation pipeline&lt;/li&gt;
&lt;li&gt;Native binary management: cross-platform deployment requires platform-specific PDFium binaries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feature Comparison Overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;PdfiumViewer&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Upstream status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Archived (community forks exist)&lt;/td&gt;
&lt;td&gt;Active, commercial product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (viewer/renderer only)&lt;/td&gt;
&lt;td&gt;HTML5/CSS3/JS via Chromium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Page display and rasterization&lt;/td&gt;
&lt;td&gt;PDF generation and manipulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Installation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-package with native DLLs&lt;/td&gt;
&lt;td&gt;Single NuGet, embedded native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Support model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Community-driven via fork repos&lt;/td&gt;
&lt;td&gt;Commercial vendor support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target runtimes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;.NET Framework / fork-dependent&lt;/td&gt;
&lt;td&gt;.NET Framework 4.6.2+ through modern .NET, cross-platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Code Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PdfiumViewer — Load and Display PDF in WinForms
&lt;/h3&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;PdfiumViewer&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;System&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;System.Windows.Forms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;DesktopPdfViewer&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;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainForm&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Form&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PdfRenderer&lt;/span&gt; &lt;span class="n"&gt;pdfRenderer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;MainForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;InitializeComponent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Step 1: Add PdfRenderer control to form&lt;/span&gt;
            &lt;span class="c1"&gt;// Typically done in designer, shown here programmatically&lt;/span&gt;
            &lt;span class="n"&gt;pdfRenderer&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;PdfRenderer&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Dock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DockStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fill&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfRenderer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LoadPdfButton_Click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Step 2: Load PDF from file (synchronous call on the caller thread)&lt;/span&gt;
                &lt;span class="k"&gt;using&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;pdfDocument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"C:\Documents\report.pdf"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Step 3: Assign the document to the renderer control for display&lt;/span&gt;
                    &lt;span class="n"&gt;pdfRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfDocument&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;$"Loaded &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PageCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; pages."&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;MessageBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Failed to load PDF: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Error"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;protected&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;OnFormClosing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FormClosingEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Step 4: Cleanup&lt;/span&gt;
            &lt;span class="n"&gt;pdfRenderer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnFormClosing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Technical notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synchronous load: &lt;code&gt;PdfDocument.Load()&lt;/code&gt; runs on the calling thread; large files can block the UI unless dispatched off the UI thread&lt;/li&gt;
&lt;li&gt;Document loaded for display: not optimized for streamed backend processing&lt;/li&gt;
&lt;li&gt;Desktop-oriented: built around WinForms/WPF controls and &lt;code&gt;System.Drawing&lt;/code&gt;, not designed for ASP.NET Core or headless services&lt;/li&gt;
&lt;li&gt;No generation capability: cannot create PDFs from data, templates, or HTML — that is outside the library's scope&lt;/li&gt;
&lt;li&gt;Fork API differences: method signatures may differ between community forks; check the docs for the fork you choose&lt;/li&gt;
&lt;li&gt;Native DLL required: &lt;code&gt;pdfium.dll&lt;/code&gt; must be deployed via the &lt;code&gt;PdfiumViewer.Native.*&lt;/code&gt; packages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  IronPDF — Generate PDF from HTML String
&lt;/h3&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;IronPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&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;htmlContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;@"
&amp;lt;html&amp;gt;
&amp;lt;body style='font-family: Arial; margin: 40px;'&amp;gt;
    &amp;lt;h1&amp;gt;Quarterly Report&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;Revenue: $500,000&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Generated: {DateTime.Now:yyyy-MM-dd}&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;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;renderer&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;ChromePdfRenderer&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;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;htmlContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"quarterly-report.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance notes:&lt;/strong&gt;&lt;br&gt;
Chromium engine renders HTML in ~200-500ms for typical single-page documents. For batch scenarios (100+ PDFs), IronPDF supports async/await and parallel rendering. The &lt;a href="https://ironpdf.com/how-to/html-string-to-pdf/" rel="noopener noreferrer"&gt;HTML string to PDF guide&lt;/a&gt; covers optimization techniques like caching, image embedding, and resource management.&lt;/p&gt;


&lt;h3&gt;
  
  
  PdfiumViewer — Extract Page as Bitmap
&lt;/h3&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;PdfiumViewer&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;System.Drawing&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;System.Drawing.Imaging&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;System.IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;PdfThumbnailGenerator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;using&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;pdfDocument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"contract.pdf"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Render the first page to a bitmap at 96 DPI.&lt;/span&gt;
                    &lt;span class="c1"&gt;// Signature: Render(pageIndex, dpiX, dpiY, forPrinting)&lt;/span&gt;
                    &lt;span class="c1"&gt;// Some community forks expose alternative overloads; check&lt;/span&gt;
                    &lt;span class="c1"&gt;// the documentation for the fork you depend on.&lt;/span&gt;
                    &lt;span class="k"&gt;using&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;bitmap&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Render&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="m"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;96&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;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"thumbnail.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ImageFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Png&lt;/span&gt;&lt;span class="p"&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;"Thumbnail created."&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&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;$"Error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Technical notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synchronous rendering: each &lt;code&gt;Render&lt;/code&gt; call runs on the calling thread&lt;/li&gt;
&lt;li&gt;Per-page calls: rendering N pages typically means N individual &lt;code&gt;Render&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;Bitmap memory: high-DPI renders consume significant memory (Letter size at 300 DPI is roughly a 25 MB uncompressed bitmap)&lt;/li&gt;
&lt;li&gt;Output format: bitmap only — no vector output or re-embedding into a PDF&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;System.Drawing.Common&lt;/code&gt; dependency: Windows-only on .NET 6+, which constrains where the same code can run&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  IronPDF — Generate and Manipulate Multi-Page PDF
&lt;/h3&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;IronPdf&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;System.Linq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&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;pageHtmlTemplates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Enumerable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Select&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;gt;&lt;/span&gt;
    &lt;span class="s"&gt;$"&amp;lt;h1&amp;gt;Page &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="s"&gt;&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Content for page &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="s"&gt;&amp;lt;/p&amp;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;renderer&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;ChromePdfRenderer&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;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;div style='page-break-after: always;'&amp;gt;&amp;lt;/div&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageHtmlTemplates&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Add watermark to all pages&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;div style='opacity: 0.3;'&amp;gt;DRAFT&amp;lt;/div&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Extract specific pages&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;subset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CopyPages&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="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// First 5 pages&lt;/span&gt;
&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"contract-summary.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Performance notes:&lt;/strong&gt;&lt;br&gt;
Bulk HTML rendering is faster than per-page approaches—IronPDF's Chromium engine processes page-break directives efficiently. Typical 10-page document with simple content renders in ~1 second. &lt;a href="https://ironpdf.com/tutorials/html-to-pdf/" rel="noopener noreferrer"&gt;HTML to PDF tutorial&lt;/a&gt; demonstrates advanced layout techniques (CSS Grid, Flexbox, print media queries).&lt;/p&gt;


&lt;h3&gt;
  
  
  PdfiumViewer — Search Text via Page-Level Extraction
&lt;/h3&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;PdfiumViewer&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;System&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;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pdfDocument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"legal-document.pdf"&lt;/span&gt;&lt;span class="p"&gt;))&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;searchTerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"indemnification"&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;totalMatches&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sb&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;StringBuilder&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="n"&gt;pdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PageCount&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="c1"&gt;// GetPdfText returns the raw text of a single page —&lt;/span&gt;
                &lt;span class="c1"&gt;// no per-word coordinates, no layout reconstruction,&lt;/span&gt;
                &lt;span class="c1"&gt;// and no OCR for image-only pages.&lt;/span&gt;
                &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;pageText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPdfText&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="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"--- Page &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="m"&gt;1&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;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageText&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;idx&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="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pageText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;totalMatches&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
                    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&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;$"Found &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;totalMatches&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; match(es) for '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Technical notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GetPdfText(int page)&lt;/code&gt; returns raw page text — no per-word coordinates, no layout reconstruction, and no OCR&lt;/li&gt;
&lt;li&gt;Image-only / scanned pages return empty text and need an external OCR engine (Tesseract, etc.) to be searchable&lt;/li&gt;
&lt;li&gt;The library is Windows-oriented and depends on WinForms / &lt;code&gt;System.Drawing&lt;/code&gt; assemblies, which limits where the same code runs headlessly on .NET 6+&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  IronPDF — Extract and Search Text
&lt;/h3&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;IronPdf&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;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&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;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"legal-document.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Extract all text from every page&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fullText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExtractAllText&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Search for term&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;occurrences&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"indemnification"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;StringSplitOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&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;$"Found '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;occurrences&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;' occurrences of 'indemnification'."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Extract from a specific page (0-indexed)&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;page3Text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExtractTextFromPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Performance notes:&lt;/strong&gt;&lt;br&gt;
Text extraction uses the underlying PDF text layer, not OCR, so it is fast on text-based PDFs but returns nothing for purely scanned image pages. Scanned-PDF workflows pair IronPDF with an OCR step (Tesseract or similar) as a separate package. The extraction API runs the same way in a console app, ASP.NET Core, or Docker — no UI dependency.&lt;/p&gt;


&lt;h3&gt;
  
  
  PdfiumViewer — Performance Benchmark Scenario: Load 500-Page PDF
&lt;/h3&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;PdfiumViewer&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;System&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;System.Diagnostics&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;System.Windows.Forms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;ViewerPerformanceTest&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;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BenchmarkForm&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Form&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;PdfRenderer&lt;/span&gt; &lt;span class="n"&gt;pdfRenderer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;BenchmarkForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;InitializeComponent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;pdfRenderer&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;PdfRenderer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Dock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DockStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fill&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfRenderer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LoadLargePdf_Click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stopwatch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// PdfDocument.Load is synchronous; dispatch off the UI thread&lt;/span&gt;
                &lt;span class="c1"&gt;// (Task.Run, BackgroundWorker) if you want to keep the form&lt;/span&gt;
                &lt;span class="c1"&gt;// responsive while a large file loads.&lt;/span&gt;
                &lt;span class="k"&gt;using&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;pdfDocument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"large-manual-500-pages.pdf"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="n"&gt;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;$"Load time: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                    &lt;span class="n"&gt;pdfRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfDocument&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&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;$"Load failed after &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Observations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load is synchronous: large files block the calling thread; benchmark on your own hardware&lt;/li&gt;
&lt;li&gt;Document is loaded for display: large documents can have a noticeable memory footprint&lt;/li&gt;
&lt;li&gt;No built-in cancellation token for &lt;code&gt;Load&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Very large documents (1,000+ pages) can be slow to load and view in the renderer control&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  IronPDF — Performance Benchmark: Generate 500-Page PDF
&lt;/h3&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;IronPdf&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;System&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;System.Diagnostics&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;System.Linq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicenseKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-LICENSE-KEY"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;htmlPages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Enumerable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Select&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;gt;&lt;/span&gt;
    &lt;span class="s"&gt;$"&amp;lt;h1&amp;gt;Chapter &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="s"&gt;&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Content for chapter &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="s"&gt;...&amp;lt;/p&amp;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;renderer&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;ChromePdfRenderer&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;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RenderHtmlAsPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;div style='page-break-after: always;'&amp;gt;&amp;lt;/div&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;htmlPages&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;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;$"Generation time: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"generated-500-pages.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Observations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generation cost scales with HTML complexity (CSS, images, tables) more than raw page count&lt;/li&gt;
&lt;li&gt;IronPDF exposes async APIs so generation can run off the UI thread in desktop apps&lt;/li&gt;
&lt;li&gt;Optimization patterns: cache external assets, pre-compile templates, parallelize batches with care for engine memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Architectural comparison:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;PdfiumViewer&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Load and view an existing PDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in viewer/renderer controls&lt;/td&gt;
&lt;td&gt;Not the target use case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generate a PDF from HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Core feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render a page to bitmap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (via &lt;code&gt;Render&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Available via &lt;code&gt;ToBitmap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extract text (programmatic)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Page-level via &lt;code&gt;GetPdfText&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ExtractAllText&lt;/code&gt; / &lt;code&gt;ExtractTextFromPage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Headless / server execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Designed around UI controls&lt;/td&gt;
&lt;td&gt;Designed for headless use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Async APIs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Available across the generation pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  API Mapping Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PdfiumViewer Concept&lt;/th&gt;
&lt;th&gt;IronPDF Equivalent&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PdfDocument.Load(path)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PdfDocument.FromFile(path)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both load existing PDFs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pdfRenderer.Load(doc)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N/A (no viewer control)&lt;/td&gt;
&lt;td&gt;IronPDF is backend-focused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doc.Render(page, dpiX, dpiY, forPrinting)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pdf.RasterizeToImageFiles(path, dpi)&lt;/code&gt; / &lt;code&gt;pdf.ToBitmap()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Both rasterize, different shapes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doc.PageCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf.PageCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same property&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doc.GetPdfText(page)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pdf.ExtractTextFromPage(page)&lt;/code&gt; / &lt;code&gt;pdf.ExtractAllText()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Page-level vs. whole-document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doc.Dispose()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf.Dispose()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both implement &lt;code&gt;IDisposable&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;(not available)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RenderHtmlAsPdf()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IronPDF core feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;(not available)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PdfDocument.Merge()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Combine PDFs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;(not available)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf.Form&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Form read / write / flatten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;(not available)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf.SecuritySettings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Password protection and permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PdfViewer&lt;/code&gt; control&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(not available)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;IronPDF has no WinForms/WPF UI control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Comprehensive Feature Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;PdfiumViewer&lt;/th&gt;
&lt;th&gt;IronPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstream project&lt;/td&gt;
&lt;td&gt;Archived 2019-08-02&lt;/td&gt;
&lt;td&gt;Actively developed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Community forks&lt;/td&gt;
&lt;td&gt;Yes (bezzad, PdfiumViewer.Updated, others)&lt;/td&gt;
&lt;td&gt;N/A (single product)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API stability across forks&lt;/td&gt;
&lt;td&gt;May vary&lt;/td&gt;
&lt;td&gt;Documented per release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Support model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;Fork-dependent&lt;/td&gt;
&lt;td&gt;Vendor docs and API reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issue channels&lt;/td&gt;
&lt;td&gt;GitHub issues per fork&lt;/td&gt;
&lt;td&gt;Commercial vendor channels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commercial support&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML to PDF&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (core feature)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL to PDF&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate from scratch&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge PDFs&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split PDFs&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF operations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display in WinForms/WPF&lt;/td&gt;
&lt;td&gt;Yes (core feature)&lt;/td&gt;
&lt;td&gt;No UI control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render page to bitmap&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;Render&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;ToBitmap&lt;/code&gt;, &lt;code&gt;RasterizeToImageFiles&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract text&lt;/td&gt;
&lt;td&gt;Page-level via &lt;code&gt;GetPdfText&lt;/code&gt; (no per-word coords, no OCR)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ExtractAllText&lt;/code&gt; / &lt;code&gt;ExtractTextFromPage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form handling&lt;/td&gt;
&lt;td&gt;View only&lt;/td&gt;
&lt;td&gt;Read, write, flatten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital signatures&lt;/td&gt;
&lt;td&gt;No API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;No API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watermarks&lt;/td&gt;
&lt;td&gt;No API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password-protected PDFs&lt;/td&gt;
&lt;td&gt;Can open with password&lt;/td&gt;
&lt;td&gt;Open and manipulate, can set passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate signatures&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redaction&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime characteristics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thread model&lt;/td&gt;
&lt;td&gt;Synchronous; UI thread must dispatch large work&lt;/td&gt;
&lt;td&gt;Async APIs available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native binaries&lt;/td&gt;
&lt;td&gt;Separate &lt;code&gt;PdfiumViewer.Native.*&lt;/code&gt; packages&lt;/td&gt;
&lt;td&gt;Embedded with the IronPdf package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform&lt;/td&gt;
&lt;td&gt;Windows-oriented; &lt;code&gt;System.Drawing&lt;/code&gt; is Windows-only on .NET 6+&lt;/td&gt;
&lt;td&gt;Windows, Linux, macOS, Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server / cloud usage&lt;/td&gt;
&lt;td&gt;Not the target use case&lt;/td&gt;
&lt;td&gt;Designed for it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async/await&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headless operation&lt;/td&gt;
&lt;td&gt;Built around UI controls&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Desktop apps&lt;/td&gt;
&lt;td&gt;NuGet, Docker, cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Installation Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PdfiumViewer (community fork example):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Install-Package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PdfiumViewer.Updated&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# .NET 6+&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Install-Package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PdfiumViewer.Native.x86_64.v8-xfa&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# Native binaries&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;PdfiumViewer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Requires WinForms or WPF application&lt;/span&gt;
&lt;span class="c1"&gt;// Add PdfRenderer control to form&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IronPDF:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Install-Package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IronPdf&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# Single package, all platforms&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;IronPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Works in console, ASP.NET, Azure Functions, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;PdfiumViewer is good at the job it was designed for: embedding a PDF viewer control in WinForms/WPF desktop applications, with page-level rendering and &lt;code&gt;GetPdfText&lt;/code&gt; for raw text extraction. For teams whose product is a Windows desktop UI that already has PDFs to display, community forks like PdfiumViewer.Updated or bezzad/PdfiumViewer can still serve that role. The upstream archive in 2019, multi-fork ecosystem, and tight coupling to WinForms/&lt;code&gt;System.Drawing&lt;/code&gt; are the points to weigh against that.&lt;/p&gt;

&lt;p&gt;Migration starts to make sense when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-side or headless PDF generation is on the requirements list (web apps, APIs, background workers)&lt;/li&gt;
&lt;li&gt;Document creation from templates, HTML, or data is needed&lt;/li&gt;
&lt;li&gt;Cross-platform deployment is on the roadmap (Linux, Docker, cloud)&lt;/li&gt;
&lt;li&gt;PDF manipulation beyond viewing is required (merge, split, forms, signatures, watermarks)&lt;/li&gt;
&lt;li&gt;Architecture is shifting from desktop to web or microservices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;IronPDF replaces the generation side of that equation with PDF rendering from &lt;a href="https://ironpdf.com/how-to/html-string-to-pdf/" rel="noopener noreferrer"&gt;HTML strings&lt;/a&gt;, URLs, and files via a Chromium engine. It does not ship a WinForms/WPF viewer control, but it does handle the document lifecycle: create from modern HTML/CSS/JS, manipulate (merge, split, forms, encryption, signatures), extract (text, images, metadata), and print to physical printers. Installation is a single NuGet package with embedded native dependencies, and IronPDF runs in console apps, ASP.NET Core, Azure Functions, AWS Lambda, and Docker containers across Windows, Linux, and macOS on .NET Framework 4.6.2+ through modern .NET.&lt;/p&gt;

&lt;p&gt;The two libraries optimize for different things — PdfiumViewer for immediate on-screen display, IronPDF for headless document creation — so the right answer often depends on which side of that line the product roadmap is sitting on.&lt;/p&gt;

&lt;p&gt;What's your experience with desktop PDF viewer controls versus headless generation libraries? Share your performance benchmarks in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ironpdf.com/tutorials/html-to-pdf/" rel="noopener noreferrer"&gt;IronPDF HTML to PDF Performance Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ironpdf.com/how-to/rendering-options/" rel="noopener noreferrer"&gt;Rendering Options Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>When Azure Functions Fight Back: Signs You've Outgrown Them</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 15 May 2026 09:41:53 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/when-azure-functions-fight-back-signs-youve-outgrown-them-1o5c</link>
      <guid>https://dev.to/martin_oehlert/when-azure-functions-fight-back-signs-youve-outgrown-them-1o5c</guid>
      <description>&lt;p&gt;Your queue handler hit the 10-minute Consumption ceiling last week. You restructured it to checkpoint, and the next month-end batch still creeps over. The question now is not how to wring one more workaround out of Functions. It is when the next workaround stops being cheaper than moving the job onto a different host. Four signals push the answer past "still cheaper": performance walls, complexity sprawl, coupling patterns the platform makes worse, and a cost crossover that arrives sooner than most teams plan for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance walls you will hit
&lt;/h2&gt;

&lt;p&gt;Four limits decide how far Functions can carry the workload: how long any one invocation can run, how much memory it can use, how many sockets it can hold open, and what its file system actually persists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution timeout per plan
&lt;/h3&gt;

&lt;p&gt;The numbers from the &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-scale#function-app-timeout-duration" rel="noopener noreferrer"&gt;hosting plan timeout reference&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%2Fm95oz9cjygoa31tc39p4.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%2Fm95oz9cjygoa31tc39p4.png" alt="Execution timeout per plan" width="606" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cross-cutting cap: HTTP triggers that do not respond within &lt;strong&gt;230 seconds&lt;/strong&gt; are cut off by the Azure Load Balancer with HTTP 502 regardless of &lt;code&gt;functionTimeout&lt;/code&gt;. The function keeps running but cannot return a response. Sources: &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-http-webhook-trigger#limits" rel="noopener noreferrer"&gt;HTTP trigger limits&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/troubleshoot/azure/app-service/web-request-times-out-app-service" rel="noopener noreferrer"&gt;Web request times out in App Service&lt;/a&gt;. For longer work, Microsoft points at the &lt;a href="https://learn.microsoft.com/azure/azure-functions/durable-functions/durable-functions-http-features#async-operation-tracking" rel="noopener noreferrer"&gt;Durable Functions async HTTP pattern&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-host-json#functiontimeout" rel="noopener noreferrer"&gt;host.json: &lt;code&gt;functionTimeout&lt;/code&gt;&lt;/a&gt; describes what happens at the cap: "When an execution exceeds this duration, a timeout error occurs and the language worker process restarts." The worker is killed, and in-flight invocations on it are lost. What the trigger does next is per binding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service Bus&lt;/strong&gt;: PeekLock with &lt;code&gt;autoComplete = true&lt;/code&gt;. On host crash the lock expires, and on next visibility the message reappears with &lt;code&gt;DeliveryCount&lt;/code&gt; incremented. After &lt;code&gt;MaxDeliveryCount&lt;/code&gt; (default 10) it lands in &lt;code&gt;&amp;lt;queue&amp;gt;/$deadletterqueue&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/azure/service-bus-messaging/service-bus-dead-letter-queues#maximum-delivery-count" rel="noopener noreferrer"&gt;Service Bus dead-letter queues&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage Queue&lt;/strong&gt;: visibility timeout per message. On host crash the storage-default 10-minute timeout takes over. After 5 failed attempts the message moves to &lt;code&gt;&amp;lt;queue&amp;gt;-poison&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-storage-queue-trigger#peek-lock" rel="noopener noreferrer"&gt;Storage queue trigger: Peek lock&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blob trigger&lt;/strong&gt;: same five-attempt default, with failed blobs landing in &lt;code&gt;webjobs-blobtrigger-poison&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-storage-blob-trigger#poison-blobs" rel="noopener noreferrer"&gt;Poison blobs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP&lt;/strong&gt;: caller already saw the 502 at 230 s. No automatic retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timer&lt;/strong&gt;: per &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-timer#usage" rel="noopener noreferrer"&gt;Timer trigger: Retry behavior&lt;/a&gt;, "the timer trigger doesn't retry after a function fails."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A subtle one to flag: only &lt;strong&gt;Cosmos DB, Event Hubs, Kafka, and Timer&lt;/strong&gt; support host-level retry policies (&lt;code&gt;[FixedDelayRetry]&lt;/code&gt;, &lt;code&gt;[ExponentialBackoffRetry]&lt;/code&gt;). For Service Bus and Storage Queue, the binding's native retry semantics are the only mechanism (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-error-pages#retry-policies" rel="noopener noreferrer"&gt;Azure Functions error handling and retries&lt;/a&gt;). Decorating a Service Bus trigger with &lt;code&gt;[ExponentialBackoffRetry]&lt;/code&gt; does nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix that lets you stay (when it can)
&lt;/h3&gt;

&lt;p&gt;Most "we hit the timeout" stories are workloads that can be split into chunks with a checkpoint between them. A queue-triggered batch that processes 50,000 items in 50 ms each runs 42 minutes. The same handler that processes 500 at a time and writes a cursor blob between chunks survives any number of restarts. From the &lt;a href="https://github.com/MO2k4/azure-functions-samples/tree/main/BatchCheckpointDemo" rel="noopener noreferrer"&gt;companion sample&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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;DrainBatchFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"batches"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;BatchCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cursors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;checkpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBlobClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"batch-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.cursor"&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;lastCommitted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ReadCursorAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChunksAfterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                       &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastCommitted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_chunkSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Commit the cursor *before* the next chunk; if the worker is killed&lt;/span&gt;
        &lt;span class="c1"&gt;// immediately after this upload, the next attempt resumes here.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UploadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastItemId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&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 shape is what matters: chunk, process, commit cursor, repeat. Each chunk must be small enough that the worst-case batch (500 items at 50 ms = 25 s) finishes well inside the timeout. The cursor write is the moment durability shifts. On retry, &lt;code&gt;ReadCursorAsync&lt;/code&gt; resumes at the last committed item instead of restarting at item 1.&lt;/p&gt;

&lt;p&gt;This pattern keeps you on Functions. When the &lt;em&gt;single chunk&lt;/em&gt; itself runs longer than the timeout (a 12-minute database query, a multi-gigabyte file copy, a 30-minute model inference), the workload has outgrown the platform. No chunk size helps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory ceilings
&lt;/h3&gt;

&lt;p&gt;Per-instance memory from the &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-scale#service-limits" rel="noopener noreferrer"&gt;service limits table&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%2Fu3e7n9rvsmae5i2mpakv.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%2Fu3e7n9rvsmae5i2mpakv.png" alt="Memory per instance per plan" width="568" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two surprises in that list. Flex Consumption has a &lt;strong&gt;512 MB&lt;/strong&gt; SKU. Most teams reading the marketing page assume Flex starts where Premium does. And from the trigger's perspective, &lt;strong&gt;OOM and timeout look the same&lt;/strong&gt;: the OS terminates the worker, the host restarts, and the per-binding retry semantics from the previous section decide whether the input is replayed or dropped. The closest official reference is the &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-host-json#healthmonitor" rel="noopener noreferrer"&gt;host health monitor&lt;/a&gt;, which can recycle the host preemptively when performance counters stay above 80% within the health-check window.&lt;/p&gt;

&lt;p&gt;The blob trigger has a documented memory amplifier worth knowing. Microsoft's &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-storage-blob-trigger#memory-usage-and-concurrency" rel="noopener noreferrer"&gt;Blob trigger: Memory usage and concurrency&lt;/a&gt; warns that "the runtime must load the entire blob into memory more than one time during processing" if you bind to a non-streaming type, and concurrency multiplies the effect. Bind to &lt;code&gt;Stream&lt;/code&gt; for anything past a few MB.&lt;/p&gt;

&lt;h3&gt;
  
  
  SNAT ports and connection exhaustion
&lt;/h3&gt;

&lt;p&gt;Two distinct outbound limits, easy to confuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SNAT port budget per instance.&lt;/strong&gt; From &lt;a href="https://learn.microsoft.com/azure/app-service/troubleshoot-intermittent-outbound-connection-errors#cause" rel="noopener noreferrer"&gt;Troubleshoot intermittent outbound connection errors&lt;/a&gt;: "Each instance on Azure App service is initially given a preallocated number of &lt;em&gt;128&lt;/em&gt; SNAT ports." Ports apply to the same destination tuple (address + port) and are reclaimed by the load balancer &lt;strong&gt;four minutes&lt;/strong&gt; after the connection closes. Microsoft's recommendation is to keep usage under &lt;strong&gt;100 outbound connections per unique remote endpoint per instance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total outbound TCP connections per instance.&lt;/strong&gt; Consumption is capped at &lt;strong&gt;600 active (1,200 total) per instance&lt;/strong&gt;, and the runtime logs &lt;code&gt;Host thresholds exceeded: Connections&lt;/code&gt; at the limit. Flex, Premium, and Dedicated are listed as "unbounded" (Dedicated still subject to App Service worker-size caps).&lt;/p&gt;

&lt;p&gt;The fastest way to walk into both limits is the canonical Functions anti-pattern: &lt;code&gt;new HttpClient()&lt;/code&gt; inside a function body. Each invocation creates a new socket pool, and sockets sit in &lt;code&gt;TIME_WAIT&lt;/code&gt; after disposal, compounded by the four-minute SNAT reclaim. At any reasonable RPS, the 128-port budget for a destination host is exhausted, and the function sees intermittent connect failures or &lt;code&gt;SocketException&lt;/code&gt;. The &lt;a href="https://learn.microsoft.com/azure/azure-functions/errors-diagnostics/sdk-rules/azf0002" rel="noopener noreferrer"&gt;AZF0002 analyzer&lt;/a&gt; flags the call site at build time.&lt;/p&gt;

&lt;p&gt;The recommended fix is &lt;code&gt;IHttpClientFactory&lt;/code&gt;, registered once in &lt;code&gt;Program.cs&lt;/code&gt;. From the &lt;a href="https://github.com/MO2k4/azure-functions-samples/tree/main/HttpClientFactoryDemo" rel="noopener noreferrer"&gt;companion sample&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaymentsOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&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="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentsOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SectionName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDataAnnotations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IPaymentsApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PaymentsApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaymentsOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStandardResilienceHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The factory caches &lt;code&gt;HttpMessageHandler&lt;/code&gt; instances (default lifetime 2 min per &lt;a href="https://learn.microsoft.com/dotnet/core/extensions/httpclient-factory#httpclient-lifetime-management" rel="noopener noreferrer"&gt;HttpClient lifetime management&lt;/a&gt;) and rotates them so DNS changes are picked up. The &lt;code&gt;AddStandardResilienceHandler&lt;/code&gt; call layers in retry, circuit breaker, and timeout policies from &lt;code&gt;Microsoft.Extensions.Http.Resilience&lt;/code&gt; without an extra DI dance. The function takes &lt;code&gt;IPaymentsApi&lt;/code&gt; on its primary constructor and never calls &lt;code&gt;new HttpClient()&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;One caveat from &lt;a href="https://learn.microsoft.com/dotnet/fundamentals/networking/http/httpclient-guidelines#recommended-use" rel="noopener noreferrer"&gt;HttpClient guidelines: Recommended use&lt;/a&gt;: the factory shares a &lt;code&gt;CookieContainer&lt;/code&gt; across pooled handlers. If your client depends on cookies, prefer a singleton with an explicit &lt;code&gt;SocketsHttpHandler&lt;/code&gt; that sets &lt;code&gt;PooledConnectionLifetime&lt;/code&gt; instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database connections do not get the same fix
&lt;/h3&gt;

&lt;p&gt;ADO.NET pools per process, and Functions runs one process per instance. Microsoft's &lt;a href="https://learn.microsoft.com/azure/azure-functions/manage-connections#sqlclient-connections" rel="noopener noreferrer"&gt;SqlClient guidance&lt;/a&gt; is direct: "ADO.NET implements connection pooling by default. But because you can still run out of connections, you should optimize connections to the database."&lt;/p&gt;

&lt;p&gt;The math is the trap. Each Consumption instance carries its own pool (default &lt;code&gt;Max Pool Size = 100&lt;/code&gt;), and the platform can spin up to &lt;strong&gt;200&lt;/strong&gt; Consumption instances (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-scale#service-limits" rel="noopener noreferrer"&gt;service limits&lt;/a&gt;). At full scale-out: &lt;code&gt;200 instances * 100 connections = 20,000 potential connections&lt;/code&gt; against the database. Premium is capped at 100 instances, Flex at 1,000. Most managed databases fall over well below that.&lt;/p&gt;

&lt;p&gt;The fix lives at the database, not in Functions. Lower the pool size per instance, cap the application's max scale-out, or front the database with a connection pooler (PgBouncer, RDS Proxy equivalents on Azure Database for PostgreSQL Flexible Server).&lt;/p&gt;

&lt;h3&gt;
  
  
  Local file system
&lt;/h3&gt;

&lt;p&gt;Three behaviours worth distinguishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-instance ephemeral temp.&lt;/strong&gt; From &lt;a href="https://learn.microsoft.com/azure/app-service/operating-system-functionality#file-access" rel="noopener noreferrer"&gt;Operating system functionality in App Service&lt;/a&gt;: &lt;code&gt;%SystemDrive%\local&lt;/code&gt; is reserved for temporary local storage, "not persistent across app restarts." Plan-by-plan capacity ranges from 0.5 GB (Consumption) to 21-140 GB (Premium / Dedicated). Scale-out does not just reset temp. It makes the data invisible: instance A writes &lt;code&gt;/tmp/cache.json&lt;/code&gt;, and instance B never sees it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persisted shares.&lt;/strong&gt; Premium and Dedicated can mount Azure Files (SMB or NFS). &lt;strong&gt;Flex Consumption supports SMB and read-only Blobs but not NFS&lt;/strong&gt; (&lt;a href="https://learn.microsoft.com/azure/azure-functions/concept-file-access-options" rel="noopener noreferrer"&gt;Choose a file access strategy&lt;/a&gt;). SMB cold-start latency is documented at 200-500 ms on first execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read-only file systems.&lt;/strong&gt; When &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt; is set, "the &lt;code&gt;wwwroot&lt;/code&gt; folder is read-only and you receive an error if you write files to this directory" (&lt;a href="https://learn.microsoft.com/azure/azure-functions/run-functions-from-deployment-package#general-considerations" rel="noopener noreferrer"&gt;Run from package: General considerations&lt;/a&gt;). The same page is explicit: "Don't add the &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt; app setting to apps on the Flex Consumption plan." Flex's deployment model treats the package as read-only by design. Container Apps follows container-image semantics: image layers are read-only.&lt;/p&gt;

&lt;p&gt;The Durable Functions provider doc gives the blunt warning: "Storing payloads to local disks is &lt;em&gt;not&lt;/em&gt; recommended, since on-disk state isn't guaranteed to be available" (&lt;a href="https://learn.microsoft.com/azure/azure-functions/durable-functions/durable-functions-azure-storage-provider#azure-storage-representation-in-a-task-hub" rel="noopener noreferrer"&gt;Azure Storage representation in a task hub&lt;/a&gt;). Anything you want to read on a different instance, or after a restart, belongs in Blob, Cosmos, or another external store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complexity signals that indicate a problem
&lt;/h2&gt;

&lt;p&gt;Performance walls fail the workload outright. Complexity signals fail it slowly: the function count keeps climbing, the orchestration diagram keeps growing legends, and at some point you cannot describe the system without a whiteboard. None of the limits below are timeouts. They are the shape of the architecture telling you it has outgrown the deployment unit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durable Functions sub-orchestration sprawl
&lt;/h3&gt;

&lt;p&gt;Microsoft lists three legitimate uses for &lt;code&gt;CallSubOrchestratorAsync&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-sub-orchestrations#when-to-use-sub-orchestrations" rel="noopener noreferrer"&gt;Sub-orchestrations: When to use&lt;/a&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compose reusable workflow building blocks shared across parents.&lt;/li&gt;
&lt;li&gt;Fan out parallel instances of the same orchestrator and wait for all.&lt;/li&gt;
&lt;li&gt;Organise a large orchestration into named, testable pieces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a sub-orchestration is not doing one of those three jobs, it is decoration, and the cost compounds. The same docs add a hard constraint: "Sub-orchestrations must be defined in the same app as the parent orchestration." The cross-app workaround is the &lt;a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-features" rel="noopener noreferrer"&gt;HTTP 202 polling pattern&lt;/a&gt;, a different programming model with no parent/child semantics, no shared retry policy, and no automatic exception propagation. The reason is the &lt;a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-task-hubs#use-multiple-apps-with-separate-task-hubs" rel="noopener noreferrer"&gt;task hub model&lt;/a&gt;: "If multiple apps use the same task hub, they compete for messages, which can result in undefined behavior, including orchestrations getting unexpectedly stuck."&lt;/p&gt;

&lt;p&gt;The decision worth surfacing in your design review: &lt;strong&gt;task hub partition count is immutable after creation&lt;/strong&gt;. Default 4, max 16. From &lt;a href="https://learn.microsoft.com/azure/azure-functions/durable-functions/durable-functions-perf-and-scale#partition-count" rel="noopener noreferrer"&gt;Performance and scale: Partition count&lt;/a&gt;: "You can't change the partition count after you create a task hub. Set it high enough to meet expected scale-out requirements." Most teams discover this when their orchestrator throughput plateaus and they reach for the dial that does not exist.&lt;/p&gt;

&lt;p&gt;Sprawl signals inside one app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parent depth of 3+ levels where the grandchild does almost no work.&lt;/li&gt;
&lt;li&gt;Sub-orchestrators that wrap a single activity call.&lt;/li&gt;
&lt;li&gt;Parents fanning out to sub-orchestrators that themselves fan out: one instance ID can spawn dozens of children, each consuming control queue capacity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Orchestrator code constraints
&lt;/h3&gt;

&lt;p&gt;Orchestrators replay from history every time a new event arrives. The runtime requires deterministic code: the same input must produce the same call sequence on every replay. From &lt;a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-code-constraints" rel="noopener noreferrer"&gt;Orchestrator function code constraints&lt;/a&gt;, the following must not appear inside an orchestrator body:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DateTime.Now&lt;/code&gt; / &lt;code&gt;DateTime.UtcNow&lt;/code&gt; (use &lt;code&gt;context.CurrentUtcDateTime&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Guid.NewGuid()&lt;/code&gt; (use &lt;code&gt;context.NewGuid()&lt;/code&gt;, which returns Type 5 UUIDs derived from instance ID).&lt;/li&gt;
&lt;li&gt;Bindings, including the orchestration client and entity client bindings. I/O lives in activities.&lt;/li&gt;
&lt;li&gt;Static variables and environment variables.&lt;/li&gt;
&lt;li&gt;Direct network or HTTP calls.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Task.Run&lt;/code&gt;, &lt;code&gt;Task.Delay&lt;/code&gt;, &lt;code&gt;Thread.Sleep&lt;/code&gt;, &lt;code&gt;HttpClient.SendAsync&lt;/code&gt;. Use &lt;code&gt;context.CreateTimer&lt;/code&gt; for delays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clean rule is one sentence: &lt;strong&gt;orchestrators schedule, activities act&lt;/strong&gt;. Anything that reads time, calls the network, or generates a random value belongs in an activity. The framework throws &lt;code&gt;NonDeterministicOrchestrationException&lt;/code&gt; sometimes, but the docs are explicit: "this detection behavior won't catch all violations, and you shouldn't depend on it." Violations ship and break weeks later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared external state as bottleneck
&lt;/h3&gt;

&lt;p&gt;Account-level ceilings from &lt;a href="https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account" rel="noopener noreferrer"&gt;Standard storage account scalability targets&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20,000 RPS&lt;/strong&gt; per general-purpose v2 account in most regions, and &lt;strong&gt;40,000 RPS&lt;/strong&gt; in higher-tier regions.&lt;/li&gt;
&lt;li&gt;Hitting any of these returns &lt;strong&gt;HTTP 503 Server Busy&lt;/strong&gt; or &lt;strong&gt;HTTP 500 Operation Timeout&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per-service ceilings (the ones that bite first):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage Queue&lt;/strong&gt;: account 20,000 msg/s, &lt;strong&gt;single queue tops out at 2,000 msg/s&lt;/strong&gt; (&lt;a href="https://learn.microsoft.com/azure/storage/queues/scalability-targets" rel="noopener noreferrer"&gt;Queue Storage scalability&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/azure/architecture/best-practices/data-partitioning-strategies#partitioning-azure-storage-queues" rel="noopener noreferrer"&gt;Data partitioning strategies&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage Table&lt;/strong&gt;: 20,000 entities/s account-wide, &lt;strong&gt;2,000 entities/s per partition&lt;/strong&gt; (&lt;a href="https://learn.microsoft.com/azure/storage/tables/scalability-targets" rel="noopener noreferrer"&gt;Table Storage scalability&lt;/a&gt;). Throttling shows as &lt;code&gt;PercentThrottlingError&lt;/code&gt;. Date-as-partition-key is the canonical anti-pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cosmos DB hot partition&lt;/strong&gt;: hard ceiling of &lt;strong&gt;10,000 RU/s per logical partition&lt;/strong&gt; (&lt;a href="https://learn.microsoft.com/azure/cosmos-db/partitioning#physical-partitions" rel="noopener noreferrer"&gt;Partitioning and horizontal scaling&lt;/a&gt;), regardless of total provisioned throughput. Splitting does not help when the key is genuinely hot (&lt;a href="https://learn.microsoft.com/azure/cosmos-db/how-to-redistribute-throughput-across-partitions" rel="noopener noreferrer"&gt;Redistribute throughput&lt;/a&gt;). The fix is a different partition key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Durable Functions sits on top of those numbers. Every task hub creates two Azure Tables (&lt;code&gt;&amp;lt;hub&amp;gt;History&lt;/code&gt;, &lt;code&gt;&amp;lt;hub&amp;gt;Instances&lt;/code&gt;), one work-item queue, one control queue per partition, and blob containers for leases and large messages (&lt;a href="https://learn.microsoft.com/azure/azure-functions/durable-functions/durable-functions-azure-storage-provider#azure-storage-representation-in-a-task-hub" rel="noopener noreferrer"&gt;Azure Storage representation in a task hub&lt;/a&gt;). When several apps point at the same storage account, every one fights for the same per-partition 2,000 entities/s on the History and Instances tables. Microsoft's reliability guidance is direct: "use a separate storage account for each function app. This aspect is especially true with Durable Functions and Event Hubs triggered functions" (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-best-practices#configure-storage-correctly" rel="noopener noreferrer"&gt;Best practices for reliable Azure Functions&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Bus session locks
&lt;/h3&gt;

&lt;p&gt;The architectural smell is two or more functions in the same function app triggering on the same session-enabled queue. Sessions hold an exclusive lock per session ID (&lt;a href="https://learn.microsoft.com/azure/service-bus-messaging/message-sessions" rel="noopener noreferrer"&gt;Message sessions&lt;/a&gt;): only one receiver at a time, per-session FIFO, default &lt;code&gt;MaxDeliveryCount = 10&lt;/code&gt;. The Functions session host defaults are aggressive (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-service-bus#hostjson-settings" rel="noopener noreferrer"&gt;Service Bus host.json settings&lt;/a&gt;): &lt;code&gt;maxConcurrentSessions = 2000&lt;/code&gt;, &lt;code&gt;maxConcurrentCalls = 16&lt;/code&gt; per session. Thread starvation at high &lt;code&gt;MaxConcurrentSessions&lt;/code&gt; is documented in the &lt;a href="https://learn.microsoft.com/azure/service-bus-messaging/service-bus-troubleshooting-guide#troubleshoot-processor-issues" rel="noopener noreferrer"&gt;troubleshooting guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two functions in the same app are not parallelising work over those sessions. They are competing for session locks. &lt;code&gt;SessionLockLost&lt;/code&gt; shows up when the lock expires before renewal, the partition rebalances, or the AMQP link is idle for 10 minutes (&lt;a href="https://learn.microsoft.com/azure/service-bus-messaging/service-bus-messaging-exceptions-latest#reason-sessionlocklost" rel="noopener noreferrer"&gt;Service Bus messaging exceptions: SessionLockLost&lt;/a&gt;). The fix is one consumer per session-enabled queue per app, period.&lt;/p&gt;

&lt;h3&gt;
  
  
  Function count vs feature count
&lt;/h3&gt;

&lt;p&gt;Microsoft does not publish a number for "too many functions in one app", but the &lt;a href="https://learn.microsoft.com/azure/azure-functions/performance-reliability#function-organization-best-practices" rel="noopener noreferrer"&gt;Function organization best practices&lt;/a&gt; describe the failure mode plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each function that you create has a memory footprint. While this footprint is usually small, having too many functions within a function app can lead to slower startup of your app on new instances.&lt;/p&gt;

&lt;p&gt;Connection strings and other credentials stored in application settings gives all of the functions in the function app the same set of permissions in the associated resource. Consider minimizing the number of functions with access to specific credentials by moving functions that don't use those credentials to a separate function app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A self-test you can run in 10 minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can you name the feature each function delivers without opening the code?&lt;/li&gt;
&lt;li&gt;Does explaining one feature require drawing a diagram of 5+ function boundaries?&lt;/li&gt;
&lt;li&gt;Is your function-to-feature ratio above 3:1? (50 functions for 8 features is 6:1.)&lt;/li&gt;
&lt;li&gt;Do all functions share the same connection strings whether they use them or not?&lt;/li&gt;
&lt;li&gt;Do load profiles inside the app diverge? (Chatty queue trigger next to memory-heavy report function.)&lt;/li&gt;
&lt;li&gt;Does shipping one function redeploy 49 others?&lt;/li&gt;
&lt;li&gt;Does cold start time grow with every release?&lt;/li&gt;
&lt;li&gt;Does the same Durable task hub serve more than one feature area?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three or more is the smell. Six or more is "this should have been split a quarter ago." The W19 split-apps approach handles the first wave of this. If splitting still leaves you fighting the platform, the article you are reading is the second wave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coupling patterns that fight the serverless model
&lt;/h2&gt;

&lt;p&gt;Some of the worst Functions deployments do not look bad on any one screen. The handlers are clean, every function is short, the metrics are healthy. The damage is in the topology: the way the functions wire to each other multiplies cost, slows change, or quietly burns money in a loop. Four shapes show up over and over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sequential chains (the service with three methods)
&lt;/h3&gt;

&lt;p&gt;Function A writes a message. Function B is triggered by it. Function C is triggered by B's message. Every input traverses the same three hops, with no branching and no fan-out. It is a workflow, not a serverless decomposition.&lt;/p&gt;

&lt;p&gt;Microsoft's &lt;a href="https://learn.microsoft.com/azure/architecture/patterns/pipes-and-filters" rel="noopener noreferrer"&gt;Pipes and Filters pattern&lt;/a&gt; is explicit on when &lt;strong&gt;not&lt;/strong&gt; to use it: "the processing steps performed by an application aren't independent, or they have to be performed together as part of a single transaction." A three-step lockstep chain meets that condition. The same page points at &lt;a href="https://learn.microsoft.com/azure/architecture/patterns/compute-resource-consolidation" rel="noopener noreferrer"&gt;Compute Resource Consolidation&lt;/a&gt; for the consolidation: "You can group filters that should scale together in the same process."&lt;/p&gt;

&lt;p&gt;Cost per hop is five charges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source-function invocation (per-execution + GB-second).&lt;/li&gt;
&lt;li&gt;Queue write (one transaction).&lt;/li&gt;
&lt;li&gt;Queue read by the next function (another transaction).&lt;/li&gt;
&lt;li&gt;Serialize + deserialize (CPU on both sides).&lt;/li&gt;
&lt;li&gt;New consumer invocation (per-execution + GB-second again).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A three-function chain triples that. The corrected shapes are documented: collapse to one Functions invocation that calls the three operations as private methods (single trigger, single execution, no inter-function queues), or move to &lt;a href="https://learn.microsoft.com/azure/durable-task/common/durable-task-sequence" rel="noopener noreferrer"&gt;Durable Functions function chaining&lt;/a&gt; if the steps are genuinely separate but coordinated. The orchestrator keeps durable state and tracks the choreography explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared database schemas across Function Apps
&lt;/h3&gt;

&lt;p&gt;Microsoft's &lt;a href="https://learn.microsoft.com/azure/architecture/microservices/design/data-considerations" rel="noopener noreferrer"&gt;Data considerations for microservices&lt;/a&gt; is unambiguous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Two services shouldn't share a data store. Each service manages its own private data store, and other services can't access it directly.&lt;/p&gt;

&lt;p&gt;Services can safely share the same physical database server. Problems occur when services share the same schema, or they read and write to the same set of database tables.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two Function Apps on the same Azure SQL server with separate schemas is fine. Two Function Apps writing the same &lt;code&gt;Orders&lt;/code&gt; table is the antipattern.&lt;/p&gt;

&lt;p&gt;The cost surfaces at migration time. Additive changes (new column, new table) work. Destructive changes (rename, drop, type change, NOT NULL on a populated column) require all N apps to agree on a release order: forward-compat code shipped in advance to every app, or a coordinated cutover that removes the ability of any one app to deploy independently. Two-phase migrations (expand-and-contract) become the default. The blue/green compatibility window is the &lt;strong&gt;intersection&lt;/strong&gt; of every app's deploy windows. Each schema version must be readable and writable by every prior version of every consumer that might still be running (&lt;a href="https://learn.microsoft.com/azure/architecture/guide/multitenant/approaches/storage-data#antipatterns-to-avoid" rel="noopener noreferrer"&gt;multitenant antipatterns&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If three apps share an &lt;code&gt;Orders&lt;/code&gt; table and one ships weekly, one biweekly, one monthly, the slowest cadence sets the floor. A hosted service that owns the schema collapses N consumers to 1, and migrations stop being a coordination problem. The trade is what you give up: per-function scaling and trigger-binding ergonomics. The &lt;a href="https://learn.microsoft.com/azure/architecture/patterns/saga#context-and-problem" rel="noopener noreferrer"&gt;Saga pattern: Context and problem&lt;/a&gt; acknowledges the trade explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Circular queue dependencies and poison loops
&lt;/h3&gt;

&lt;p&gt;Two defaults to cite side by side, because readers conflate them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage Queue &lt;code&gt;maxDequeueCount&lt;/code&gt; = 5&lt;/strong&gt;. After 5 failures the message goes to &lt;code&gt;&amp;lt;originalqueue&amp;gt;-poison&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-storage-queue#hostjson-settings" rel="noopener noreferrer"&gt;Storage queue host.json settings&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Bus &lt;code&gt;MaxDeliveryCount&lt;/code&gt; = 10&lt;/strong&gt;. After 10 attempts the message goes to the DLQ at &lt;code&gt;&amp;lt;queue path&amp;gt;/$deadletterqueue&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/azure/service-bus-messaging/service-bus-dead-letter-queues#maximum-delivery-count" rel="noopener noreferrer"&gt;Service Bus dead-letter queues&lt;/a&gt;). "There's no automatic cleanup of the DLQ. Messages remain in the DLQ until you explicitly retrieve them."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Functions handles the settlement automatically: "By default, the runtime calls &lt;code&gt;Complete&lt;/code&gt; on the message if the function finishes successfully, or calls &lt;code&gt;Abandon&lt;/code&gt; if the function fails" (&lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-bindings-service-bus-trigger#peeklock-behavior" rel="noopener noreferrer"&gt;Service Bus trigger: PeekLock&lt;/a&gt;). The two antipatterns that subvert this safety net:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retry queue that re-enqueues to the input queue.&lt;/strong&gt; Handler catches the exception, writes the message &lt;em&gt;back&lt;/em&gt; to the input queue with a transient-failure tag, returns success. The runtime never sees a failure, &lt;code&gt;dequeueCount&lt;/code&gt; resets each round trip, and the message lives forever. The &lt;code&gt;MessageId&lt;/code&gt; changes because the application is publishing a new message each time, so log correlation by ID misses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead-letter handler that re-triggers the original function.&lt;/strong&gt; A second function with a trigger on the poison/DLQ subqueue picks up failed messages and calls back into the original function's logic (or worse, writes back to the original input queue). Result: input -&amp;gt; processing -&amp;gt; poison -&amp;gt; handler -&amp;gt; input ad infinitum. Service Bus eventually surfaces &lt;code&gt;QuotaExceeded&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/azure/service-bus-messaging/service-bus-messaging-exceptions-latest#reason-quotaexceeded" rel="noopener noreferrer"&gt;messaging exceptions&lt;/a&gt;). Storage Queues do not fail nearly as loudly. The loop just costs money until somebody notices the bill.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Microsoft names the failure shape directly. The &lt;a href="https://learn.microsoft.com/azure/architecture/patterns/choreography#solution" rel="noopener noreferrer"&gt;Choreography pattern&lt;/a&gt; carries the warning: "There's a risk of cyclic dependency between saga participants because they have to consume each other's commands."&lt;/p&gt;

&lt;h3&gt;
  
  
  Detecting poison loops in Application Insights
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://learn.microsoft.com/azure/azure-monitor/app/failures-performance-transactions#transaction-diagnostics-experience" rel="noopener noreferrer"&gt;End-to-end transaction details&lt;/a&gt; view shows a Gantt chart of every server-side telemetry event for a correlated &lt;code&gt;operation_Id&lt;/code&gt; across all instrumented components. For a Functions chain, each invocation shows up as a request span with the queue-dependency calls between them, all under the same operation. &lt;strong&gt;N+1 invocations of the same function name under the same &lt;code&gt;operation_Id&lt;/code&gt; is the loop signature.&lt;/strong&gt; The &lt;a href="https://learn.microsoft.com/azure/azure-monitor/app/app-map" rel="noopener noreferrer"&gt;Application Map&lt;/a&gt; makes the cycle visible at the topology level: a node with a self-edge or a tight A-&amp;gt;B-&amp;gt;A cycle.&lt;/p&gt;

&lt;p&gt;The detection workflow is two clicks: &lt;strong&gt;Failures&lt;/strong&gt; view, drill into a sample exception, the End-to-end transaction view opens with the trace tree expanded. If you cannot tell at a glance whether a transaction is one logical request or three loops of the same one, instrument before you refactor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration drift
&lt;/h3&gt;

&lt;p&gt;Splitting one Function App into N copies the configuration N times. Each app has its own connection strings, API keys, storage keys in App Settings. Rotating a secret means N updates. Missing one leaves a stale credential in production until the next deploy.&lt;/p&gt;

&lt;p&gt;Key Vault references move the storage out of App Settings and into a vault. The syntax (&lt;a href="https://learn.microsoft.com/azure/app-service/app-service-key-vault-references#understand-source-app-settings-from-key-vault" rel="noopener noreferrer"&gt;Use Key Vault references as app settings&lt;/a&gt;) takes one of two forms:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The failure mode the same page documents: "If a reference isn't resolved properly, the reference string is used instead." Real production failure pattern: the function tries to authenticate with the literal string &lt;code&gt;@Microsoft.KeyVault(...)&lt;/code&gt;, gets a 401, and the operator stares at App Settings that look correct in the portal. The &lt;code&gt;WEBSITE_KEYVAULT_REFERENCES&lt;/code&gt; env var holds resolution status for every reference, and the portal exposes a Key Vault Application Settings Diagnostics detector. Both are worth knowing before the first incident.&lt;/p&gt;

&lt;p&gt;The "partially adopted" antipattern is the worst version: some apps reference &lt;code&gt;@Microsoft.KeyVault(...)&lt;/code&gt;, others have raw values because the migration was incomplete. Rotating the secret in the vault updates the references and leaves the raw-value apps stale. Configuration looks correct in the portal until the next failure surface, usually a 401 from a downstream service hours after rotation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/azure/azure-app-configuration/overview" rel="noopener noreferrer"&gt;Azure App Configuration&lt;/a&gt; collapses N App Settings stores into one. "Spreading configuration settings across these components can lead to hard-to-troubleshoot errors during an application deployment. Use App Configuration to store all the settings for your application and secure their accesses in one place." App Configuration handles non-secret config and holds Key Vault references for secret values. Key Vault stays the secret store. The trade is one runtime dependency, N role assignments, and refresh semantics that are opt-in (without dynamic refresh, settings are read once at startup). For two-app workloads it might not pay. For ten-app workloads it almost always does (&lt;a href="https://learn.microsoft.com/azure/azure-app-configuration/howto-best-practices#building-applications-with-high-resiliency" rel="noopener noreferrer"&gt;App Configuration: high resiliency&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost crossover point
&lt;/h2&gt;

&lt;p&gt;"Functions is too expensive at scale" and "Functions is the cheap option" are both true. They describe different points on the same curve. The crossover happens earlier than most teams plan for, and the worked example below makes it concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  A worked example: 10 RPS at 200 ms / 256 MB
&lt;/h3&gt;

&lt;p&gt;Assumptions: 10 RPS sustained for 30 days, 200 ms execution, 256 MB memory. East US 2. Single subscription with the per-month free grants applied. All numbers verified against the Azure Retail Prices API on 2026-05-08.&lt;/p&gt;

&lt;p&gt;Volumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Executions: &lt;code&gt;10 * 3,600 * 24 * 30&lt;/code&gt; = 25,920,000&lt;/li&gt;
&lt;li&gt;GB-seconds: &lt;code&gt;0.25 * 0.2 * 25,920,000&lt;/code&gt; = 1,296,000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsuq20hcisiu9c9o6o7sn.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%2Fsuq20hcisiu9c9o6o7sn.png" alt="Cost per host at 10 RPS / 200 ms / 256 MB" width="738" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Flex number is the trap. Flex bills a &lt;strong&gt;1-second minimum&lt;/strong&gt; per execution then rounds to 100 ms above that (&lt;a href="https://learn.microsoft.com/azure/azure-functions/flex-consumption-plan#billing" rel="noopener noreferrer"&gt;Flex billing&lt;/a&gt;). At 200 ms / 256 MB, every invocation bills as if it ran 1 second, which is 5x the GB-seconds and pushes the bill 9x above legacy Consumption. Teams switching from Consumption to Flex "for the per-function scaling" walk into this and call the platform expensive. Verify on real workload before committing.&lt;/p&gt;

&lt;p&gt;The crossover with EP1 is the other number worth keeping in your head. Setting &lt;code&gt;Consumption_total = EP1_floor&lt;/code&gt; and solving for RPS at 200 ms / 256 MB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cost ≈ 2.592 * R - 6.60 = 145.93
R ≈ 58.8 RPS sustained
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the 200 ms / 256 MB shape, &lt;strong&gt;Consumption ties EP1 around 60 RPS sustained&lt;/strong&gt;. Below that, Consumption wins on cost. Above that, EP1 starts winning and the Premium-only reasons (always-on, VNet integration, longer timeouts) compound the case. The crossover shifts with execution length: at 1 s / 256 MB it falls to about 12 RPS, and at 50 ms it pushes past 240 RPS. Pick the shape that matches your workload before you quote a number.&lt;/p&gt;

&lt;p&gt;AKS gets one sentence: if you need Kubernetes primitives, you are no longer comparing against Functions, and the comparison belongs in a different article.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hidden bill in App Insights
&lt;/h3&gt;

&lt;p&gt;App Insights bills through Log Analytics (&lt;a href="https://learn.microsoft.com/azure/azure-monitor/logs/cost-logs#application-insights-billing" rel="noopener noreferrer"&gt;App Insights billing&lt;/a&gt;) at &lt;strong&gt;$2.76 per GB&lt;/strong&gt; ingested above the 5 GB free grant per workspace. For most apps the default adaptive sampling at 5 events/second per host keeps ingestion under the grant (&lt;a href="https://learn.microsoft.com/azure/azure-monitor/app/sampling-classic-api" rel="noopener noreferrer"&gt;Sampling in Application Insights&lt;/a&gt;). The failure mode is operational: an engineer disables sampling to chase a bug, forgets to re-enable, and the next month's bill arrives. At 100 RPS unsampled with 10 KB telemetry events, ingestion is 86 GB/day, which is 2.6 TB/month, which is roughly &lt;strong&gt;$7,180/month&lt;/strong&gt; at the PAYG rate.&lt;/p&gt;

&lt;p&gt;The mitigation is a daily cap, set per workspace. The portal default for a workspace-based App Insights resource is 100 GB/day, but resources created via Visual Studio default to 32.3 MB/day (&lt;a href="https://learn.microsoft.com/azure/azure-monitor/logs/daily-cap" rel="noopener noreferrer"&gt;Daily cap&lt;/a&gt;). Whichever number you pick, set it before someone disables sampling.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other hidden bill: the storage account
&lt;/h3&gt;

&lt;p&gt;Every Function App requires a general-purpose storage account (&lt;a href="https://learn.microsoft.com/azure/azure-functions/storage-considerations#storage-account-requirements" rel="noopener noreferrer"&gt;Storage considerations&lt;/a&gt;). With one or two apps, storage is rounding error. With thirty apps after a W19-style split, it becomes a line item:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queue triggers&lt;/strong&gt;: every poll is a Class 2 op, roughly one per second when idle. Idle alone is &lt;code&gt;86,400 polls/day * 30 / 10,000 * $0.004 ≈ $1/month per queue&lt;/code&gt;. Real processing adds put + get + delete = 3 ops per message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable task hubs&lt;/strong&gt;: orchestration and history tables grow into millions of rows with replays, plus control queues, work-item queues, and instance tables. A busy task hub easily reaches $20-50/month before the orchestrator's compute cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal runtime traffic&lt;/strong&gt;: lease blobs for the scale controller, host locks. Negligible per app, multiplied by N apps it shows up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft's own guidance is to put each app on its own storage account, especially for Durable Functions and Event Hubs triggers. That doubles or triples your storage line item, and it is still the right call.&lt;/p&gt;

&lt;h3&gt;
  
  
  People-time
&lt;/h3&gt;

&lt;p&gt;The cost crossover argument rarely decides the migration. People-time almost always does.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging across N function apps requires correlated query plumbing in App Insights plus a mental model of which app owns which trigger.&lt;/li&gt;
&lt;li&gt;Each function app is its own deployment unit, so coordinated releases need a release pipeline that understands ordering and rollback.&lt;/li&gt;
&lt;li&gt;An on-call alert that fires "queue X is backing up" requires the operator to know which app owns queue X, which trigger, and which version is deployed.&lt;/li&gt;
&lt;li&gt;Configuration drift compounds with app count, as the previous section already showed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cognitive load scales super-linearly with app count. It shows up as slower MTTR, not as a Functions bill line item, and that is exactly why it stays invisible until the team is exhausted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the decision: stay, refactor, or migrate
&lt;/h2&gt;

&lt;p&gt;Most "outgrowing Functions" complaints are organisational, not technical. Apply the W19 split-apps approach first, then revisit. The four concrete signals that say you can stay: no timeout pressure (longest job under half the plan limit), no memory pressure (peak under 60% of the instance ceiling), SNAT and connection issues fixed by &lt;code&gt;IHttpClientFactory&lt;/code&gt;, function-to-feature ratio under 3:1. If all four hold, the platform is not the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refactor within Functions
&lt;/h3&gt;

&lt;p&gt;This is W19 territory: split into multiple apps, extract a shared library, isolate triggers by scaling profile. If you are on Consumption, the next step before Premium is Flex Consumption (per-function scaling, longer timeouts, larger SKUs). The caveat from the cost section bears repeating: Flex's 1-second billing floor punishes sub-second functions. Verify on real workload before committing.&lt;/p&gt;

&lt;p&gt;For workloads that bump the timeout but can be split, the checkpoint pattern from the Performance walls section keeps you on Functions: chunk, process, commit cursor, repeat. That keeps the trigger ergonomics, the deployment unit, and the scale controller. The cost is one cursor blob per active batch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract specific functions
&lt;/h3&gt;

&lt;p&gt;The middle ground. The function app stays, and one or two functions move out, usually because they hit a wall the rest of the app does not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Long-running jobs.&lt;/strong&gt; Hosted service in App Service, or a Container Apps job. Same business logic, no per-invocation cap. The &lt;a href="https://github.com/MO2k4/azure-functions-samples/tree/main/MigrationDemo" rel="noopener noreferrer"&gt;companion sample&lt;/a&gt; shows the same payment-settlement workload as a &lt;code&gt;BackgroundService&lt;/code&gt; next to its Function App original. The diff is the hosting wrapper, not the algorithm:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&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;SettlementWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;QueueClient&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IPaymentSettler&lt;/span&gt; &lt;span class="n"&gt;settler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QueueOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;queueOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SettlementWorkerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SettlementWorker&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReceiveMessagesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;maxMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxBatchMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VisibilityTimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&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;response&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="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdlePollingDelayMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&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;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;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Same &lt;code&gt;IPaymentSettler&lt;/code&gt; from the shared library, same queue, no per-invocation timeout. The trade is paying for an always-on worker (App Service) or accepting cold-start on first replica scale-out (Container Apps with KEDA).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stateful workflows hitting Durable limits.&lt;/strong&gt; Cross-app sub-orchestration is impossible, and shared task hub contention is real. Logic Apps Standard or a hosted workflow engine (Temporal, Elsa, Conductor) trades the binding ergonomics for a workflow surface that scales the way Durable does not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CPU- or memory-bound work above EP3.&lt;/strong&gt; Container Apps with the Dedicated workload profile, or AKS if Kubernetes is already a platform decision in your org.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Full migration
&lt;/h3&gt;

&lt;p&gt;Rare. Criteria: timeout + memory + connection + cost crossover all present, all blocking, and the W19 organisational fixes have been applied without relief. The migration path is contract-first: extract HTTP triggers into thin wrappers, push business logic into testable libraries (a &lt;code&gt;*.Core&lt;/code&gt; project consumed by both the Function App and the destination host), then move binding by binding.&lt;/p&gt;

&lt;p&gt;The companion sample lays this out concretely. &lt;code&gt;Settlement.Core&lt;/code&gt; is consumed unchanged by &lt;code&gt;Settlement.FunctionApp&lt;/code&gt; (the timeout-wall starting point), &lt;code&gt;Settlement.AppService&lt;/code&gt; (always-on, adds HTTP endpoints), and &lt;code&gt;Settlement.ContainerApp&lt;/code&gt; (KEDA-scaled, scale-to-zero, no web host). The diffs are pure hosting concerns. The algorithm and the contract are identical. Reading the three side by side makes the migration question stop being abstract: you are pricing one wrapper against another, not rewriting the workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision matrix
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7mwnlndrv0dgfd6ubed.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%2Fh7mwnlndrv0dgfd6ubed.png" alt="Outgrowing Functions: signal-by-signal decision matrix" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cost crossover at the bottom of the table is the one most teams reach for first. The decision rarely turns on it. It turns on people-time and on whether the abstraction is fighting your design or supporting it. When the abstraction is supporting the design, the right answer is almost always "stay and clean up the wiring."&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;The four signals from the opening map to the four sections you just read. Timeouts, memory, sockets, and file system are the platform telling you the workload is too big. Sub-orchestration sprawl, session-lock contention, and high function-to-feature ratios are your team telling you the deployment unit is too big. Sequential chains, shared schemas, and poison loops are the topology making both worse. The cost crossover decides whether the right move is a different plan, a different host, or a different problem statement. None of the signals on its own says "migrate." Two of them at once says "look harder." Three of them blocking says "the abstraction is no longer paying its freight."&lt;/p&gt;

&lt;p&gt;When you last looked at outgrowing Functions, did you stay and refactor, or did you extract the workload to a different host?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions Beyond the Basics&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Continues from &lt;a href="https://dev.to/martin_oehlert/series/38960"&gt;Azure Functions for .NET Developers&lt;/a&gt; (Parts 1-9)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/running-azure-functions-in-docker-why-and-how-1hal"&gt;Running Azure Functions in Docker: Why and How&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/docker-pitfalls-i-hit-and-how-to-avoid-them-2395"&gt;Docker Pitfalls I Hit (And How to Avoid Them)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/scaling-azure-functions-consumption-vs-premium-vs-dedicated-2gm"&gt;Scaling Azure Functions: Consumption vs Premium vs Dedicated&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/structuring-complex-function-apps-project-organization-5977"&gt;Structuring Complex Function Apps: Project Organization&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 5: When Azure Functions Fight Back: Signs You've Outgrown Them (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>architecture</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>CVE-2026-35433 | .NET Elevation of Privilege Vulnerability | R.A.H.S.I. Framework™ Analysis</title>
      <dc:creator>Aakash Rahsi</dc:creator>
      <pubDate>Fri, 15 May 2026 03:29:05 +0000</pubDate>
      <link>https://dev.to/aakash_rahsi/cve-2026-35433-net-elevation-of-privilege-vulnerability-rahsi-framework-analysis-3767</link>
      <guid>https://dev.to/aakash_rahsi/cve-2026-35433-net-elevation-of-privilege-vulnerability-rahsi-framework-analysis-3767</guid>
      <description>&lt;h1&gt;
  
  
  CVE-2026-35433 | .NET Elevation of Privilege Vulnerability | R.A.H.S.I. Framework™ Analysis
&lt;/h1&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%2Foilberqkwmw92xsjo2oz.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%2Foilberqkwmw92xsjo2oz.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🛡️Let's Connect &amp;amp; Continue the Conversation&lt;/p&gt;

&lt;p&gt;🛡️Read Complete Article | &lt;/p&gt;

&lt;blockquote&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://www.aakashrahsi.online/post/cve-2026-35433" 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%2Fstatic.wixstatic.com%2Fmedia%2Ffc518c_27de16843a6447dc8c85b5511199a643~mv2.png%2Fv1%2Ffill%2Fw_1280%2Ch_720%2Cal_c%2Ffc518c_27de16843a6447dc8c85b5511199a643~mv2.png" height="450" 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://www.aakashrahsi.online/post/cve-2026-35433" rel="noopener noreferrer" class="c-link"&gt;
            CVE-2026-35433 | .NET Elevation of Privilege Vulnerability | R.A.H.S.I. Framework™ Analysis
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            R.A.H.S.I. analysis of CVE-2026-35433, a .NET elevation of privilege flaw affecting runtime trust and patch strategy.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.wixstatic.com%2Fmedia%2Ffc518c_a060086ddb9e43c5aba22d4331f00d62%257Emv2.jpg%2Fv1%2Ffill%2Fw_192%252Ch_192%252Clg_1%252Cusm_0.66_1.00_0.01%2Ffc518c_a060086ddb9e43c5aba22d4331f00d62%257Emv2.jpg" width="192" height="192"&gt;
          aakashrahsi.online
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;/blockquote&gt;

&lt;p&gt;🛡️Let's Connect |&lt;/p&gt;

&lt;blockquote&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://www.aakashrahsi.online/hire-aakash-rahsi" 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%2Fstatic.wixstatic.com%2Fmedia%2Ffc518c_927a6eb6170e433389c8c2386484cc7f~mv2.gif%2Fv1%2Ffill%2Fw_858%2Ch_482%2Cal_c%2Ffc518c_927a6eb6170e433389c8c2386484cc7f~mv2.gif" height="337" class="m-0" width="600"&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://www.aakashrahsi.online/hire-aakash-rahsi" rel="noopener noreferrer" class="c-link"&gt;
            Hire Aakash Rahsi | Expert in Intune, Automation, AI, and Cloud Solutions
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Hire Aakash Rahsi, a seasoned IT expert with over 13 years of experience specializing in PowerShell scripting, IT automation, cloud solutions, and cutting-edge tech consulting. Aakash offers tailored strategies and innovative solutions to help businesses streamline operations, optimize cloud infrastructure, and embrace modern technology. Perfect for organizations seeking advanced IT consulting, automation expertise, and cloud optimization to stay ahead in the tech landscape.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.wixstatic.com%2Fmedia%2Ffc518c_a060086ddb9e43c5aba22d4331f00d62%257Emv2.jpg%2Fv1%2Ffill%2Fw_192%252Ch_192%252Clg_1%252Cusm_0.66_1.00_0.01%2Ffc518c_a060086ddb9e43c5aba22d4331f00d62%257Emv2.jpg" width="192" height="192"&gt;
          aakashrahsi.online
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;/blockquote&gt;

&lt;p&gt;.NET is not only a developer runtime.&lt;/p&gt;

&lt;p&gt;It is a trust layer inside enterprise applications, desktop workloads, cloud services, automation pipelines, and business platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2026-35433&lt;/strong&gt; is a high-severity .NET elevation of privilege vulnerability linked to improper input validation.&lt;/p&gt;

&lt;p&gt;Under the &lt;strong&gt;R.A.H.S.I. Framework™&lt;/strong&gt;, this CVE should be assessed as a runtime-trust and privilege-boundary issue.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Runtime Trust Risk
&lt;/h2&gt;

&lt;p&gt;.NET applications often run inside business-critical environments where local execution paths, dependency behavior, and application context can influence privilege boundaries.&lt;/p&gt;

&lt;p&gt;When input validation fails, the impact can move beyond a single application and affect the trust model around the host workload.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Privilege Boundary Exposure
&lt;/h2&gt;

&lt;p&gt;Elevation of privilege does not always begin with admin access.&lt;/p&gt;

&lt;p&gt;This CVE highlights why local execution surfaces, user-assisted flows, runtime permissions, and application identity must be treated as part of the enterprise attack surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Patch Confidence and Validation
&lt;/h2&gt;

&lt;p&gt;Patching is essential, but patching alone is not governance.&lt;/p&gt;

&lt;p&gt;Security teams should validate runtime coverage across endpoints, servers, CI/CD agents, packaged applications, and legacy .NET Framework dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Privilege boundaries are only as strong as the runtime layers that enforce them.&lt;/p&gt;

&lt;p&gt;Security teams should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update affected .NET and .NET Framework deployments&lt;/li&gt;
&lt;li&gt;Inventory .NET 8.0, .NET 9.0, .NET 10.0, and relevant .NET Framework assets&lt;/li&gt;
&lt;li&gt;Validate patch coverage on endpoints, servers, and build agents&lt;/li&gt;
&lt;li&gt;Restrict local user rights and remove unnecessary accounts&lt;/li&gt;
&lt;li&gt;Monitor privilege changes, unusual process launches, and application anomalies&lt;/li&gt;
&lt;li&gt;Review dependency, runtime, and application hardening controls&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  R.A.H.S.I. Framework™ View
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When a runtime layer can be abused to cross privilege boundaries, application security becomes identity, endpoint, and governance security.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>cve202635433</category>
      <category>dotnet</category>
      <category>elevation</category>
      <category>vulnerabilities</category>
    </item>
    <item>
      <title>.NET Design Patterns Deep Dive: What Still Matters in 2026</title>
      <dc:creator>Vikrant Bagal</dc:creator>
      <pubDate>Thu, 14 May 2026 15:44:02 +0000</pubDate>
      <link>https://dev.to/vikrant_bagal_afae3e25ca7/net-design-patterns-deep-dive-what-still-matters-in-2026-1g52</link>
      <guid>https://dev.to/vikrant_bagal_afae3e25ca7/net-design-patterns-deep-dive-what-still-matters-in-2026-1g52</guid>
      <description>&lt;p&gt;Design patterns have been a cornerstone of object-oriented software development for decades. Yet, with the evolution of .NET — from .NET Framework to .NET 10, C# 14, and the rise of cloud-native architectures — the relevance of each pattern has shifted dramatically. This deep dive explores which design patterns remain essential in modern .NET development, which have become anti-patterns, and how to apply them effectively for performance, maintainability, and scalability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Design Patterns Still Matter
&lt;/h2&gt;

&lt;p&gt;In 2026, the .NET ecosystem is richer than ever. From high‑performance services to AI‑driven applications, the underlying principles of good software design remain constant: separation of concerns, testability, and adaptability. Design patterns provide a shared vocabulary and proven solutions to recurring problems. However, blindly applying “textbook” patterns can lead to over‑engineered, rigid systems. As the industry has matured, we now recognize that &lt;strong&gt;context is king&lt;/strong&gt; — the right pattern depends on the problem, the scale, and the team’s expertise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Pillars of Design Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Creational Patterns
&lt;/h3&gt;

&lt;p&gt;These patterns control object creation, aiming to increase flexibility and reduce coupling.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Singleton&lt;/strong&gt; – Ensures a single instance exists globally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Factory Method&lt;/strong&gt; – Delegates instantiation to subclasses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abstract Factory&lt;/strong&gt; – Creates families of related objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builder&lt;/strong&gt; – Separates construction from representation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prototype&lt;/strong&gt; – Creates objects by cloning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Structural Patterns
&lt;/h3&gt;

&lt;p&gt;These patterns define how objects are composed to form larger structures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adapter&lt;/strong&gt; – Bridges incompatible interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decorator&lt;/strong&gt; – Adds responsibilities dynamically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facade&lt;/strong&gt; – Simplifies complex subsystems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxy&lt;/strong&gt; – Controls access to another object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite&lt;/strong&gt; – Treats individual and composite objects uniformly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Behavioral Patterns
&lt;/h3&gt;

&lt;p&gt;These patterns manage communication and responsibility between objects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strategy&lt;/strong&gt; – Encapsulates interchangeable algorithms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observer&lt;/strong&gt; – Notifies dependents of state changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command&lt;/strong&gt; – Encapsulates requests as objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State&lt;/strong&gt; – Alters behavior when internal state changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain of Responsibility&lt;/strong&gt; – Passes requests along a chain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Patterns That Matter in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Singleton: Still Useful, But Use Sparingly
&lt;/h3&gt;

&lt;p&gt;The Singleton pattern is still relevant for resources that truly require a single instance (e.g., logging, configuration). However, modern .NET encourages dependency injection (DI) as the default mechanism for managing lifetimes. Overusing Singleton can break testability and introduce hidden dependencies. &lt;strong&gt;Best practice&lt;/strong&gt;: Use DI containers (like &lt;code&gt;Microsoft.Extensions.DependencyInjection&lt;/code&gt;) to register services as singletons when appropriate, rather than implementing a manual Singleton.&lt;/p&gt;

&lt;h3&gt;
  
  
  Factory Method &amp;amp; Abstract Factory: Essential for Extensibility
&lt;/h3&gt;

&lt;p&gt;With plug‑in architectures and runtime polymorphism, Factory patterns remain vital. In .NET, they’re often implemented via interfaces and DI. For example, a payment gateway factory can switch between Stripe, PayPal, or a mock implementation based on configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPaymentProcessor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaymentResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StripeProcessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPaymentProcessor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;class&lt;/span&gt; &lt;span class="nc"&gt;PayPalProcessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPaymentProcessor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentProcessorFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&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;IPaymentProcessor&lt;/span&gt; &lt;span class="nf"&gt;Create&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;provider&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;provider&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"Stripe"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StripeProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
            &lt;span class="s"&gt;"PayPal"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PayPalProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unknown provider"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Builder: Fluent APIs and Immutable Objects
&lt;/h3&gt;

&lt;p&gt;The Builder pattern shines when constructing complex objects, especially immutable ones. Modern C# records and init‑only properties pair perfectly with builders. Entity Framework Core’s &lt;code&gt;DbContextOptionsBuilder&lt;/code&gt; is a prime example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository &amp;amp; Unit of Work: Overused, Still Valuable
&lt;/h3&gt;

&lt;p&gt;The Repository pattern (abstracting data access) and Unit of Work (transaction management) are ubiquitous in .NET applications. However, they are often misapplied — adding business logic inside repositories violates separation of concerns. Use repositories only as a data‑access abstraction; keep business rules in the domain layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy: Dynamic Behavior at Runtime
&lt;/h3&gt;

&lt;p&gt;Strategy is one of the most powerful patterns for modern applications. It eliminates long &lt;code&gt;if‑else&lt;/code&gt; chains and enables runtime behavior changes. Discount calculations, caching strategies, and serialization formats are classic use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observer: Event‑Driven Architectures
&lt;/h3&gt;

&lt;p&gt;With the rise of event‑driven systems, the Observer pattern is more relevant than ever. .NET events, &lt;code&gt;IObservable&amp;lt;T&amp;gt;&lt;/code&gt;, and message brokers (RabbitMQ, Azure Service Bus) all leverage this pattern for loose coupling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decorator: Cross‑Cutting Concerns
&lt;/h3&gt;

&lt;p&gt;Decorator is ideal for adding cross‑cutting concerns like logging, caching, and authentication. ASP.NET Core middleware uses a similar concept, and the &lt;code&gt;HttpClient&lt;/code&gt; pipeline employs DelegatingHandler (a decorator‑like pattern) for retries and circuit breaking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance‑Oriented Patterns
&lt;/h2&gt;

&lt;p&gt;High‑performance .NET applications require patterns that minimize allocations, improve cache locality, and reduce GC pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pooling (ArrayPool, MemoryPool)
&lt;/h3&gt;

&lt;p&gt;Allocating arrays and buffers in hot paths can cause GC pressure. &lt;code&gt;ArrayPool&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;MemoryPool&amp;lt;T&amp;gt;&lt;/code&gt; provide shared pools of reusable buffers. Benchmark results show &lt;strong&gt;50% reduction in allocations&lt;/strong&gt; and &lt;strong&gt;30% faster execution&lt;/strong&gt; when using pooled arrays instead of &lt;code&gt;new byte[]&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ArrayPool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Rent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Use buffer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Struct of Arrays (Data‑Oriented Design)
&lt;/h3&gt;

&lt;p&gt;When processing large collections, an “array of structs” leads to poor cache locality. A “struct of arrays” (SoA) layout can improve performance by 10×. This pattern is used in high‑performance game engines and scientific computing.&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;CustomerRepositoryDOD&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_scoring&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_earnings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_isSmoking&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stack‑Based Allocation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stackalloc&lt;/code&gt;, &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;, and &lt;code&gt;ref struct&lt;/code&gt; allow stack allocation, eliminating GC overhead. They are essential for parsing, serialization, and network protocols.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;stackalloc&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// Process buffer without heap allocations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Zero‑Copy Slicing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;Memory&amp;lt;T&amp;gt;&lt;/code&gt; enable zero‑copy slicing of arrays, strings, and unmanaged memory. This pattern reduces memory copies and improves throughput in high‑volume scenarios (e.g., HTTP request processing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Real‑World Usage Examples
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft C# Dev Kit&lt;/strong&gt; – Replaced C++ with C# for Node.js addons using Native AOT, leveraging design patterns for interop and performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASP.NET Core&lt;/strong&gt; – Uses &lt;code&gt;ArrayPool&lt;/code&gt; for Kestrel’s request/response buffers, reducing GC pauses by 80%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity Framework Core&lt;/strong&gt; – Implements Unit of Work and Repository patterns internally, with &lt;code&gt;DbContext&lt;/code&gt; as the unit of work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET Aspire&lt;/strong&gt; – Employs microservice patterns (service discovery, health checks) with minimal configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roslyn&lt;/strong&gt; – Uses object pooling for syntax nodes, dramatically reducing memory allocation during compilation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common Pitfalls and Anti‑Patterns
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Singleton overuse&lt;/strong&gt; – Leads to hidden dependencies and hinders testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository abuse&lt;/strong&gt; – Placing business logic in repositories creates “fat repositories” and violates domain‑driven design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern shopping&lt;/strong&gt; – Applying a pattern because “it sounds cool” without a concrete problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring performance patterns&lt;/strong&gt; – Allocating excessively in hot paths causes GC‑induced latency spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neglecting dependency inversion&lt;/strong&gt; – Tight coupling between layers makes swapping implementations difficult.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Best Practices for Modern .NET
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prefer composition over inheritance&lt;/strong&gt; – Use interfaces and DI to assemble objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage dependency injection&lt;/strong&gt; – Let the container manage lifetimes; avoid manual Singletons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow the Dependency Inversion Principle&lt;/strong&gt; – Depend on abstractions, not concretions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose patterns contextually&lt;/strong&gt; – Use Singleton for truly single resources, Factory for extensibility, Strategy for runtime behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure before optimizing&lt;/strong&gt; – Use profiling tools (dotTrace, PerfView) to identify bottlenecks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embrace modern C# features&lt;/strong&gt; – &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Memory&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;ref struct&lt;/code&gt;, and &lt;code&gt;record&lt;/code&gt; types enhance pattern implementations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document pattern usage&lt;/strong&gt; – Ensure team members understand the why and how.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuously refactor&lt;/strong&gt; – Patterns should evolve with requirements; don’t let them become constraints.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Design patterns are not obsolete — they have evolved. In 2026, the .NET developer’s toolkit includes powerful frameworks, high‑performance primitives, and cloud‑native patterns. By understanding which patterns still matter and applying them judiciously, you can build maintainable, scalable, and performant systems.&lt;/p&gt;

&lt;p&gt;The key is to start with the problem, not the pattern. Let the problem guide your choice, and always consider the trade‑offs. With the right patterns, you can harness the full potential of modern .NET.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Connect with me:&lt;/strong&gt;&lt;br&gt;
[&lt;a href="http://www.linkedin.com/in/vikrant-bagal" rel="noopener noreferrer"&gt;www.linkedin.com/in/vikrant-bagal&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>architecture</category>
      <category>csharp</category>
      <category>programming</category>
    </item>
    <item>
      <title>10 .NET Open Source Libraries Every Developer Should Know in 2026</title>
      <dc:creator>Vikrant Bagal</dc:creator>
      <pubDate>Thu, 14 May 2026 15:41:55 +0000</pubDate>
      <link>https://dev.to/vikrant_bagal_afae3e25ca7/10-net-open-source-libraries-every-developer-should-know-in-2026-1g2h</link>
      <guid>https://dev.to/vikrant_bagal_afae3e25ca7/10-net-open-source-libraries-every-developer-should-know-in-2026-1g2h</guid>
      <description>&lt;p&gt;The .NET ecosystem keeps evolving, and 2026 is no exception. Whether you're building APIs, CLIs, or cloud-native microservices, the right open source library can save you weeks of work. Here are 10 libraries that are dominating NuGet downloads and GitHub stars this year—spanning battle-tested essentials to rising stars you'll want on your radar.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Polly — Resilience Made Fluent
&lt;/h2&gt;

&lt;p&gt;If your app talks to external services, you need Polly. It provides retry, circuit breaker, timeout, bulkhead isolation, and fallback policies—all composed in a fluent API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pipeline&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;ResiliencePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRetry&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;RetryStrategyOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&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;MaxRetryAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DelayBackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCircuitBreaker&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;CircuitBreakerStrategyOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&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;FailureRatioThreshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SamplingDuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Distributed systems fail. Polly makes your app survive those failures gracefully instead of cascading errors to users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/app-vnext/Polly" rel="noopener noreferrer"&gt;App-vNext/Polly&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Serilog — Structured Logging That Actually Makes Sense
&lt;/h2&gt;

&lt;p&gt;Forget text-based logging. Serilog emits structured events with properties you can query, filter, and aggregate in tools like Seq, Elasticsearch, or Datadog.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order {OrderId} placed by {UserId} for ${Total:C}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; In production, you don't need "something went wrong." You need &lt;em&gt;which&lt;/em&gt; order, &lt;em&gt;which&lt;/em&gt; user, and &lt;em&gt;what&lt;/em&gt; amount. Structured logging gives you that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/serilog/serilog" rel="noopener noreferrer"&gt;serilog/serilog&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. FluentValidation — Validation as Code, Not Attributes
&lt;/h2&gt;

&lt;p&gt;Data annotations are fine for simple rules. FluentValidation handles the complex ones—cross-field checks, async database lookups, and reusable rule sets.&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;OrderValidator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AbstractValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderValidator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;WithMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order must have at least one item"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GreaterThan&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="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequiresShipping&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Clean validation logic lives in its own class, testable and reusable—not scattered across controllers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/fluentvalidation/fluentvalidation" rel="noopener noreferrer"&gt;FluentValidation/FluentValidation&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. MediatR — Decouple Your App with the Mediator Pattern
&lt;/h2&gt;

&lt;p&gt;MediatR implements the mediator pattern, enabling CQRS-style separation without ceremony. Commands and queries flow through a single pipeline.&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;// Controller stays thin&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrderCommand&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&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;_mediator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Handler lives in its own file&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequestHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateOrderCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&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;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;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrderCommand&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&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;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&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;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Controllers stay thin. Business logic stays isolated. And pipeline behaviors give you cross-cutting concerns (logging, validation) for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/jbogard/mediatr" rel="noopener noreferrer"&gt;jbogard/MediatR&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Dapper — When EF Core Is Too Much
&lt;/h2&gt;

&lt;p&gt;Dapper is a micro-ORM that extends &lt;code&gt;IDbConnection&lt;/code&gt; with simple mapping methods. It's nearly as fast as raw ADO.NET—because it barely adds overhead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;products&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;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"SELECT * FROM Products WHERE CategoryId = @catId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;catId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; For high-throughput read scenarios, reporting queries, or when you want full control over your SQL, Dapper is the lightweight choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/DapperLib/Dapper" rel="noopener noreferrer"&gt;DapperLib/Dapper&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Spectre.Console — Beautiful CLIs Without the Pain
&lt;/h2&gt;

&lt;p&gt;Building console apps used to mean fighting with &lt;code&gt;Console.ForegroundColor&lt;/code&gt;. Spectre.Console changes everything with tables, progress bars, trees, prompts, and markup formatting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;AnsiConsole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkupLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[bold green]Build succeeded![/]"&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;table&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;Table&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Border&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TableBorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rounded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Project"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddColumn&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="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"API"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"[green]✓[/]"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Worker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"[red]✗[/]"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;AnsiConsole&lt;/span&gt;&lt;span class="p"&gt;.&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;table&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; If you're building CLI tools, deployment scripts, or developer utilities, Spectre.Console makes them look professional with minimal effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/spectreconsole/spectre.console" rel="noopener noreferrer"&gt;spectreconsole/spectre.console&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. TickerQ — Source-Generated Task Scheduling
&lt;/h2&gt;

&lt;p&gt;TickerQ is a new entrant that's rapidly gaining traction. It's a reflection-free background task scheduler built with .NET source generators, EF Core integration, and a real-time dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TickerTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"daily-report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Cron&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0 8 * * *"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DailyReportTask&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ITickerTask&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GenerateAndEmailReportAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; No reflection overhead at runtime. Compile-time validation of cron expressions. And a built-in dashboard to monitor task execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/arcenox-co/TickerQ" rel="noopener noreferrer"&gt;Arcenox-co/TickerQ&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  8. TUnit — The Modern .NET Test Framework
&lt;/h2&gt;

&lt;p&gt;TUnit is a source-generated test framework designed to replace xUnit and NUnit. No reflection, no AppDomain complexity—tests are discovered at compile time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Calculator_Adds_TwoNumbers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Faster test discovery, parallel execution by default, and a fluent assertion API that reads naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/thomhurst/TUnit" rel="noopener noreferrer"&gt;thomhurst/TUnit&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Facet — Source-Generated DTOs and Mappings
&lt;/h2&gt;

&lt;p&gt;Facet generates DTOs, mappings, constructors, and LINQ projections from your domain models at compile time. No runtime reflection, no runtime mapping overhead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Facet&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;User&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;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Generates: UserDto with mapped properties, ToDto(), and LINQ projection support&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; AutoMapper's runtime mapping has a cost. Facet moves that cost to build time, and gives you LINQ projection support out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Tim-Maes/Facet" rel="noopener noreferrer"&gt;Tim-Maes/Facet&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. RazorConsole — Agentic TUIs with .NET
&lt;/h2&gt;

&lt;p&gt;RazorConsole is a 2026 standout. It combines .NET Razor templates with Spectre.Console to build interactive terminal UIs—including agentic TUIs that LLMs can drive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; As AI agents increasingly need to interact with developer tools, having a Razor-based TUI layer means agents can render rich output in terminals programmatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/RazorConsole/RazorConsole" rel="noopener noreferrer"&gt;RazorConsole/RazorConsole&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The 2026 Trend: Source Generators Win
&lt;/h2&gt;

&lt;p&gt;Notice the pattern? &lt;strong&gt;TickerQ, TUnit, and Facet&lt;/strong&gt; all leverage .NET source generators instead of runtime reflection. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero reflection overhead&lt;/strong&gt; at runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile-time validation&lt;/strong&gt; of configurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better AOT compatibility&lt;/strong&gt; for Native AOT scenarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDE IntelliSense&lt;/strong&gt; for generated code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're picking libraries in 2026, prefer the source-generated approach. It's the direction the entire .NET ecosystem is moving.&lt;/p&gt;




&lt;p&gt;Which of these libraries are you already using? What did I miss? Drop a comment—I'd love to hear what's in your &lt;code&gt;csproj&lt;/code&gt; this year.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Structured Output in .NET Agents</title>
      <dc:creator>Lukas Walter </dc:creator>
      <pubDate>Thu, 14 May 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/lukaswalter/structured-output-in-net-agents-26fo</link>
      <guid>https://dev.to/lukaswalter/structured-output-in-net-agents-26fo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is Part 8 of my series on the Microsoft Agent Framework. You can read the original post over on &lt;a href="https://www.lukaswalter.dev/posts/agentframework_1_8/" rel="noopener noreferrer"&gt;lukaswalter.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LLMs are good at generating text. But text is a weak boundary for application code.&lt;/p&gt;

&lt;p&gt;Ask a model for e.g., a specific coffee recipe, and the response might look slightly different every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a markdown list&lt;/li&gt;
&lt;li&gt;a numbered list&lt;/li&gt;
&lt;li&gt;bold section titles&lt;/li&gt;
&lt;li&gt;missing fields&lt;/li&gt;
&lt;li&gt;additional explanations&lt;/li&gt;
&lt;li&gt;a disclaimer at the end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is fine for a chat interface.&lt;br&gt;
It is not, when your application needs to save the result, display it on a UI, route a workflow, or pass the output into another system.&lt;/p&gt;

&lt;p&gt;At that point, you do not want “some text”.&lt;br&gt;
You want data with a known shape.&lt;/p&gt;
&lt;h2&gt;
  
  
  Raw LLM Text Is Hard to Automate
&lt;/h2&gt;

&lt;p&gt;The problem with unstructured output is not that it looks messy.&lt;br&gt;
The problem is that your application has to guess what the model meant.&lt;/p&gt;

&lt;p&gt;For example, if the model returns a coffee recipe as plain text, your code may need to extract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;brew method&lt;/li&gt;
&lt;li&gt;coffee dose&lt;/li&gt;
&lt;li&gt;water amount&lt;/li&gt;
&lt;li&gt;grind size&lt;/li&gt;
&lt;li&gt;water temperature&lt;/li&gt;
&lt;li&gt;brewing steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That usually means parsing strings.&lt;br&gt;
And string parsing breaks easily.&lt;/p&gt;

&lt;p&gt;One response might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. V60 recipe: Use 20g coffee and 320g water at 94°C. Grind medium-fine.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next response might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### V60 Pour-Over

- Coffee: 20 grams
- Water: 320 grams
- Temperature: 94°C
- Grind: medium-fine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are readable for humans.&lt;br&gt;
But for software, they are different formats.&lt;br&gt;
This is why raw LLM text is a fragile integration boundary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Define the Output Shape in C
&lt;/h2&gt;

&lt;p&gt;Instead of asking the model to return free-form text, you can define the shape you expect in C#.&lt;/p&gt;

&lt;p&gt;For example:&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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BrewRecipeSuggestion&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;BrewMethod&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;set&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;double&lt;/span&gt; &lt;span class="n"&gt;CoffeeGrams&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;set&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="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;WaterGrams&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;set&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;GrindSize&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;set&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;double&lt;/span&gt; &lt;span class="n"&gt;WaterTemperatureCelsius&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;set&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="n"&gt;List&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;Steps&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;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want multiple results, you can wrap the list in a response type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BrewRecipeResult&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BrewRecipeSuggestion&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Recipes&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;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your application has a contract.&lt;br&gt;
The model is no longer just asked to “write an answer”.&lt;br&gt;
It is requested that something be produced that can be represented as a known C# type.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using &lt;code&gt;RunAsync&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;With .NET agents, this becomes much cleaner.&lt;br&gt;
Instead of calling the agent and receiving plain text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;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="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Give me three pour-over coffee recipes."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can request a typed result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;AgentResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BrewRecipeResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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="n"&gt;BrewRecipeResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"""
&lt;/span&gt;        &lt;span class="n"&gt;Give&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt; &lt;span class="n"&gt;three&lt;/span&gt; &lt;span class="n"&gt;pour&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="n"&gt;coffee&lt;/span&gt; &lt;span class="n"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

        &lt;span class="n"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;brew&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;coffee&lt;/span&gt; &lt;span class="n"&gt;dose&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grams&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;water&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grams&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;grind&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;water&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Celsius&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;brewing&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;
        &lt;span class="s"&gt;""");
&lt;/span&gt;
&lt;span class="n"&gt;BrewRecipeResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important difference is the boundary.&lt;br&gt;
Your application does not receive a string that it still has to interpret.&lt;br&gt;
It receives an &lt;code&gt;AgentResponse&amp;lt;T&amp;gt;&lt;/code&gt;, and the typed result is available through &lt;code&gt;response.Result&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means you can work with the result directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;recipe&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recipes&lt;/span&gt;&lt;span class="p"&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;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BrewMethod&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;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CoffeeGrams&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;g coffee, "&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;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaterGrams&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;g water"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is much easier to use in normal application code.&lt;br&gt;
You can render it in a UI, store it in a database, pass it to another service,validate it and even test it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the Framework Does for You
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;RunAsync&amp;lt;T&amp;gt;&lt;/code&gt;, the framework can use the target C# type to describe the expected response shape.&lt;br&gt;
The model is guided toward returning data that matches that structure.&lt;br&gt;
The framework then converts the response into the requested C# type.&lt;/p&gt;

&lt;p&gt;Conceptually, the flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C# type
   ↓
Expected response shape
   ↓
Model response
   ↓
Deserialization
   ↓
Typed C# object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That removes a lot of boilerplate.&lt;br&gt;
You do not have to manually inspect markdown, split strings, or have to search for labels in generated text.&lt;br&gt;
You get a typed result that fits into the rest of your .NET code.&lt;/p&gt;

&lt;p&gt;Still, keep one thing in mind:&lt;/p&gt;

&lt;p&gt;Structured output support can vary by agent type, provider, model, and underlying chat client.&lt;br&gt;
So this is not a reason to stop thinking about validation, fallbacks, and testing.&lt;/p&gt;

&lt;p&gt;It is a better application boundary.&lt;br&gt;
Not a replacement for engineering discipline.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Structured Output Does Not Solve
&lt;/h2&gt;

&lt;p&gt;Structured output solves the shape problem.&lt;br&gt;
It does not solve the truth problem.&lt;/p&gt;

&lt;p&gt;A model can return a valid &lt;code&gt;BrewRecipeSuggestion&lt;/code&gt; object and still be wrong.&lt;/p&gt;

&lt;p&gt;For example:&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;new&lt;/span&gt; &lt;span class="n"&gt;BrewRecipeSuggestion&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BrewMethod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"V60"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CoffeeGrams&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WaterGrams&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GrindSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"very fine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WaterTemperatureCelsius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Steps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s"&gt;"Add coffee."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"Pour all water at once."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"Wait 30 seconds."&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This object may be structurally valid.&lt;/p&gt;

&lt;p&gt;It has the expected fields.&lt;br&gt;
It can be deserialized.&lt;/p&gt;

&lt;p&gt;Your application can work with it as an object.&lt;br&gt;
But that does not mean it is a good recipe. (&lt;em&gt;The ratio is unrealistic.&lt;br&gt;
The water temperature is impossible for normal brewing.&lt;br&gt;
The steps are questionable.&lt;/em&gt;)&lt;/p&gt;

&lt;p&gt;Structured output can tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The response has the expected fields&lt;/li&gt;
&lt;li&gt;The values can be deserialized&lt;/li&gt;
&lt;li&gt;The application can work with the result as an object&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not guarantee:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The facts are correct&lt;/li&gt;
&lt;li&gt;The recommendation is useful&lt;/li&gt;
&lt;li&gt;The values are reasonable&lt;/li&gt;
&lt;li&gt;The user is allowed to perform the action&lt;/li&gt;
&lt;li&gt;The result satisfies your business rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So keep in mind: typed output should usually be the first gate, not the final gate.&lt;/p&gt;

&lt;p&gt;A more robust flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model output
   ↓
Deserialize into known type
   ↓
Validate required fields
   ↓
Validate ranges and enums
   ↓
Check business rules
   ↓
Accept, reject, retry, or escalate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this coffee example, you might still check the generated recipe:&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;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BrewRecipeSuggestion&lt;/span&gt; &lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BrewMethod&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Brew method is required."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CoffeeGrams&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Coffee dose must be greater than zero."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaterGrams&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CoffeeGrams&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;ratio&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Brew ratio is outside the supported range."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaterTemperatureCelsius&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;85&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Water temperature must be between 85°C and 100°C."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&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;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"At least one brewing step is required."&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;Structured output makes validation easier.&lt;br&gt;
It does not remove the need for validation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Practical Example: Intent Routing
&lt;/h2&gt;

&lt;p&gt;One useful pattern is intent routing.&lt;/p&gt;

&lt;p&gt;Imagine an assistant that can answer questions about coffee brewing and guitar tone.&lt;br&gt;
A user might ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How do I get a dirty Hendrix tone on my Strat?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can you give me a V60 recipe for 18g of coffee?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could first send the user request to a small routing agent.&lt;br&gt;
That agent should not answer the question.&lt;br&gt;
It should only classify the intent.&lt;/p&gt;

&lt;p&gt;For example:&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;enum&lt;/span&gt; &lt;span class="n"&gt;AssistantIntent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CoffeeBrewing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GuitarTone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Unknown&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;IntentResult&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;AssistantIntent&lt;/span&gt; &lt;span class="n"&gt;Intent&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;set&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="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;Confidence&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;set&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Reason&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;set&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;Then you can request a typed result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"How do I get a dirty Hendrix tone on my Strat?"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;AgentResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IntentResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;intentResponse&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;intentAgent&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="n"&gt;IntentResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="s"&gt;$"""
&lt;/span&gt;        &lt;span class="n"&gt;Classify&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;request&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;intent&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;answer&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;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

        &lt;span class="n"&gt;Supported&lt;/span&gt; &lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;CoffeeBrewing&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;GuitarTone&lt;/span&gt;
        &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Unknown&lt;/span&gt;

        &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;""");
&lt;/span&gt;
&lt;span class="n"&gt;IntentResult&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intentResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, your C# code stays simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Intent&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AssistantIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CoffeeBrewing&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;coffeeAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;AssistantIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GuitarTone&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;guitarAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fallbackAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is much cleaner than asking the model to return text like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The user is probably asking about guitar tone.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then trying to parse that sentence.&lt;/p&gt;

&lt;p&gt;The routing decision becomes a typed value.&lt;br&gt;
Your application code can switch on it.&lt;br&gt;
You can log it, test it and add validation around it.&lt;/p&gt;

&lt;p&gt;For example:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Confidence&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Intent confidence must be between 0 and 1."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Confidence&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fallbackAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&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;Again, the typed object does not make the model perfect.&lt;br&gt;
But it gives your application a reliable shape to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Structured Output Fits
&lt;/h2&gt;

&lt;p&gt;Structured output is useful whenever the model response has to cross into application logic.&lt;/p&gt;

&lt;p&gt;Common examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;extracting fields from user input&lt;/li&gt;
&lt;li&gt;classifying intent&lt;/li&gt;
&lt;li&gt;routing workflows&lt;/li&gt;
&lt;li&gt;generating UI-ready data&lt;/li&gt;
&lt;li&gt;creating database records&lt;/li&gt;
&lt;li&gt;preparing tool arguments&lt;/li&gt;
&lt;li&gt;returning validation results&lt;/li&gt;
&lt;li&gt;producing evaluation summaries&lt;/li&gt;
&lt;li&gt;generating configuration-like output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is always the same:&lt;/p&gt;

&lt;p&gt;Do not let free-form text leak into places where your application expects structured data.&lt;br&gt;
Use a typed boundary.&lt;/p&gt;

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

&lt;p&gt;Structured output is one of the most important patterns when combining LLMs with traditional software systems.&lt;br&gt;
Not because it makes the model perfect.&lt;br&gt;
But because it gives your application a clear contract.&lt;/p&gt;

&lt;p&gt;Instead of parsing unstable text, your .NET code can work with known types, which makes the system easier to build, test, and reason about.&lt;/p&gt;

&lt;p&gt;LLM output should not be treated as a string once it enters your application boundary.&lt;br&gt;
It should become a typed object.&lt;br&gt;
And from there, normal engineering practices apply again.&lt;/p&gt;

&lt;p&gt;We now know that structured output defines how an agent answers.&lt;br&gt;
But useful agents also need ways to access capabilities beyond the current prompt.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://www.lukaswalter.dev/posts/agentframework_1_7/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;, we looked at local C# function tools: methods exposed directly from your .NET application.&lt;/p&gt;

&lt;p&gt;Next, we will move one step further and look at MCP tools and Agent Skills.&lt;br&gt;
MCP tools expose capabilities from external systems through the Model Context Protocol.&lt;br&gt;
Agent Skills package reusable instructions, domain knowledge, scripts, and procedures that can be loaded when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/agents/structured-outputs" rel="noopener noreferrer"&gt;Producing Structured Outputs with agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/agents/running-agents" rel="noopener noreferrer"&gt;Running Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/overview/" rel="noopener noreferrer"&gt;Microsoft Agent Framework Overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
