<?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: Eelco Los</title>
    <description>The latest articles on DEV Community by Eelco Los (@eelcolos).</description>
    <link>https://dev.to/eelcolos</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1407552%2Fa0f81cf1-e1f2-4c20-850b-4b42878ec20f.jpeg</url>
      <title>DEV Community: Eelco Los</title>
      <link>https://dev.to/eelcolos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eelcolos"/>
    <language>en</language>
    <item>
      <title>How I designed a multi-agent AI support copilot</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Fri, 27 Mar 2026 14:21:37 +0000</pubDate>
      <link>https://dev.to/eelcolos/how-i-designed-a-multi-agent-ai-support-copilot-4fpj</link>
      <guid>https://dev.to/eelcolos/how-i-designed-a-multi-agent-ai-support-copilot-4fpj</guid>
      <description>&lt;p&gt;Every support ticket starts the same way at our SaaS company: open the ticket, scan the description, then spend the next 15 minutes manually gathering context across five different systems. Check application telemetry for exceptions. Look up the customer in the CRM. Grep provisioning logs for failed sync events. Search work item systems and source control for related bugs. Cross-reference identity policy config if authentication is involved.&lt;/p&gt;

&lt;p&gt;That context is all out there. It's just scattered. By the time you've assembled it, you've already spent most of your time budget on information retrieval, not on reasoning.&lt;/p&gt;

&lt;p&gt;That's what I set out to fix. This is the first post in a series about the multi-agent AI support copilot.&lt;/p&gt;




&lt;h2&gt;
  
  
  The idea: a side-by-side AI copilot, not a replacement
&lt;/h2&gt;

&lt;p&gt;The typical AI support story starts with a ticket routing chatbot or an automatic responder. That's not this. We're not changing the helpdesk agent's job or asking the model to speak to the customer. Instead, we start a background process alongside the human support agent. The moment a ticket arrives, the copilot gathers context from the surrounding systems and checks whether that evidence corroborates, weakens, or contradicts what the customer is reporting.&lt;/p&gt;

&lt;p&gt;The first milestone is deliberately modest from the product side: show the ticket and the supporting evidence side by side so the support agent can reason faster with better context. Under the hood, the implementation already goes further and can synthesize ranked hypotheses with confidence scores, but I still want the first user-visible win to be evidence the human can inspect. For now, the human stays in charge.&lt;/p&gt;

&lt;p&gt;Everything that touches the customer still requires a human to approve it.&lt;/p&gt;

&lt;p&gt;The design had three hard requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parallel evidence gathering&lt;/strong&gt;: all domains queried at the same time, not sequentially&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured outputs&lt;/strong&gt;: every agent returns validated JSON, not prose that needs re-parsing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop for every action&lt;/strong&gt;: the copilot informs judgment, humans approve, deterministic code acts&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Where the design came from
&lt;/h2&gt;

&lt;p&gt;The initial architecture sketch came from a long ChatGPT conversation, the kind where you brain-dump a problem and the model helps you think through the components. That session produced the five-agent skeleton: identity resolution, observability telemetry, provisioning logs, work items, and a synthesis layer. It also produced the confidence arbitration formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FinalConf = sigmoid(Support - Conflict) × AgreementMultiplier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;Support&lt;/code&gt; and &lt;code&gt;Conflict&lt;/code&gt; are weighted sums of evidence claims, each claim weighted by the originating agent's reliability (&lt;code&gt;R_agent&lt;/code&gt;) and the claim's local confidence.&lt;/p&gt;

&lt;p&gt;The second source was internal: a reusable agentic scaffolding template we use for experiments. It had a graded architecture philosophy that mapped almost exactly to what we needed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;Template capability&lt;/th&gt;
&lt;th&gt;What we needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; session memory&lt;/td&gt;
&lt;td&gt;SupportAgent persona + IncidentContext schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Domain expertise YAML files&lt;/td&gt;
&lt;td&gt;Mental models per support domain (APM, CRM, SCIM, B2C, ALM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Skills (&lt;code&gt;SKILL.md&lt;/code&gt; with frontmatter)&lt;/td&gt;
&lt;td&gt;Evidence agent skill implementations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Closed-loop validation&lt;/td&gt;
&lt;td&gt;Re-evidence loop + Reviewer gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Orchestration (parallel worker dispatch)&lt;/td&gt;
&lt;td&gt;Management Agent with parallel evidence dispatch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The template's &lt;code&gt;confidence_score&lt;/code&gt; field in expertise YAML files turned out to be exactly our &lt;code&gt;R_agent&lt;/code&gt; reliability weight. Agents that update their own expertise files after each incident self-improve over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two-layer repo structure
&lt;/h3&gt;

&lt;p&gt;The template introduced a two-layer structure we adopted verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;support-agent-research/
├── .agentic/              ← domain knowledge layer
│   ├── CLAUDE.md          - SupportAgent persona, session bootstrap
│   ├── memory/            - architecture decisions, ADR log
│   ├── plans/             - living docs for major system design changes
│   ├── expertise/         - per-domain YAML mental models
│   └── specs/             - per-incident IncidentContext files
│
└── .claude/               ← execution layer
    ├── agents/            - agent definitions (.md with frontmatter)
    ├── skills/            - evidence skill implementations (SKILL.md)
    └── settings.json      - hook wiring (Reviewer gate, Policy Gate)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that &lt;code&gt;.agentic/&lt;/code&gt; is the knowledge layer. It keeps the system's memory between incidents: persona, architecture notes, domain expertise, plans, and incident files. In this article, &lt;code&gt;plans/&lt;/code&gt; just shows that the system's design and build history live in the same place. &lt;code&gt;.claude/&lt;/code&gt; is the execution layer. It turns that knowledge into agents, skills, and hooks. Keeping them separate means you can update domain knowledge without touching execution logic, and vice versa.&lt;/p&gt;

&lt;h3&gt;
  
  
  How this matches broader agentic patterns
&lt;/h3&gt;

&lt;p&gt;I didn't invent this split in a vacuum. The Techorama 2025 sessions I captured in my notebook pointed at the same shape: a central orchestrator, specialist workers, shared context, and a clean split between knowledge and execution. I then turned that shape into a reusable agentic template, basically a starter kit that bootstraps those layers into a repo and keeps the supporting plans, memory, and delivery notes in one place.&lt;/p&gt;

&lt;p&gt;That also matches the plan modes now showing up in current CLIs: they separate thinking from doing. The template goes further by preserving state, specialist workers, and feedback loops, so the agent can plan, execute, and improve without starting from zero each time.&lt;/p&gt;

&lt;p&gt;These public repos corroborate the same building blocks. &lt;a href="https://github.com/microsoft/skills" rel="noopener noreferrer"&gt;microsoft/skills&lt;/a&gt; covers skills, custom agents, AGENTS.md templates, and MCP configs. &lt;a href="https://github.com/iannuttall/dotagents" rel="noopener noreferrer"&gt;dotagents&lt;/a&gt; and &lt;a href="https://github.com/iannuttall/source-agents" rel="noopener noreferrer"&gt;source-agents&lt;/a&gt; focus on keeping one canonical instruction set synchronized across tools. &lt;a href="https://github.com/BayramAnnakov/claude-reflect" rel="noopener noreferrer"&gt;claude-reflect&lt;/a&gt; turns corrections into durable memory and reusable skills. &lt;a href="https://github.com/agent-sh/agnix" rel="noopener noreferrer"&gt;agnix&lt;/a&gt; adds validation gates by linting agent configs before they break workflows. For the orchestrator-worker shape itself, I point to the architecture docs from Anthropic and OpenAI below rather than forcing a weak repo comparison.&lt;/p&gt;

&lt;p&gt;Anthropic draws a line between workflows and agents and recommends starting simple. The composable patterns they call out, like routing, parallelization, orchestrator-workers, and evaluator-optimizer loops, are the same kinds of building blocks I ended up using here. OpenAI's Agents SDK makes a similar point by keeping the primitive set small: instructions, tools, handoffs, guardrails, sessions, and tracing. It also separates orchestration done by the LLM from orchestration done in code. That distinction matters to me because I want the model to reason, but I want deterministic code to route work and enforce boundaries. See &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Anthropic's "Building effective agents"&lt;/a&gt;, &lt;a href="https://openai.github.io/openai-agents-python/" rel="noopener noreferrer"&gt;OpenAI Agents SDK docs&lt;/a&gt;, and &lt;a href="https://openai.github.io/openai-agents-python/multi_agent/" rel="noopener noreferrer"&gt;OpenAI Agents orchestration docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; fits this pattern too. It's basically a repo-local README for agents, a human-readable place for durable instructions that complements the normal &lt;code&gt;README.md&lt;/code&gt;. That's exactly what &lt;code&gt;.agentic/CLAUDE.md&lt;/code&gt; is doing for me here. See &lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;.agentic&lt;/code&gt; piece&lt;/th&gt;
&lt;th&gt;What it stores&lt;/th&gt;
&lt;th&gt;Broader pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.agentic/CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persona, operating rules, bootstrap guidance&lt;/td&gt;
&lt;td&gt;Repo-local agent instructions, AGENTS.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.agentic/memory/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Architecture decisions and shared context&lt;/td&gt;
&lt;td&gt;Durable instructions and session memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.agentic/plans/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Living record of the system's design and build history&lt;/td&gt;
&lt;td&gt;Bootstrap paths, planning, and rollout history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.agentic/expertise/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Domain-specific mental models&lt;/td&gt;
&lt;td&gt;Specialist workers and routing inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.agentic/specs/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Incident-scoped state and evidence&lt;/td&gt;
&lt;td&gt;Persistent session state, blackboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.claude/agents/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Worker definitions&lt;/td&gt;
&lt;td&gt;Handoffs, agent-as-tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.claude/skills/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Narrow deterministic actions&lt;/td&gt;
&lt;td&gt;Tools and guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hook wiring and policy gates&lt;/td&gt;
&lt;td&gt;Deterministic routing and execution control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's why I ended up with a central Management Agent, specialized workers, a shared IncidentContext, and deterministic gates. The manager handles routing, the workers stay narrow, and the shared context gives every worker the same incident memory without letting them talk to each other directly. Then the reviewer and policy hooks decide what can actually happen. I want the model to gather evidence and propose answers, but I want code to decide when work is allowed to move forward. That same split shows up in &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Anthropic's guidance&lt;/a&gt;, &lt;a href="https://openai.github.io/openai-agents-python/" rel="noopener noreferrer"&gt;OpenAI's Agents SDK docs&lt;/a&gt;, and &lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------------+      +------------------------+
| .agentic               | ---&amp;gt; | .claude                |
| knowledge layer        |      | execution layer        |
|                        |      |                        |
| - CLAUDE.md            |      | - agents/              |
| - memory/              |      | - skills/              |
| - plans/               |      | - settings.json        |
| - expertise/           |      |                        |
| - specs/               |      |                        |
+------------------------+      +------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Core design decisions
&lt;/h3&gt;

&lt;p&gt;Before writing a line, we laid out the key architectural bets in &lt;code&gt;decisions.md&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator-Worker over Decentralised&lt;/strong&gt;: agents never pass control to each other. This prevents circular reasoning and cascading hallucinations. The Orchestrator is the only router.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IncidentContext as Blackboard&lt;/strong&gt;: the shared knowledge base all agents write to and the Synthesis Agent reads from. Enables cross-domain correlation without agent-to-agent communication. "Identity policy changed last week" + "provisioning returned null" + "telemetry shows NullReferenceException" all point to the same root cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI-first auth&lt;/strong&gt;: every skill uses the vendor's own CLI (&lt;code&gt;az&lt;/code&gt;, &lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;acli&lt;/code&gt;) for authentication. Skills are credential-free; the bootstrap section validates auth before any query runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No LLM both concludes and acts&lt;/strong&gt;: reasoning and execution are separate. The Policy Gate is deterministic code, not a model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Orchestrator-Worker at a glance
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      Support ticket
            |
            v
     Management Agent
   /   |    |    |   \
  /    |    |    |    \
  v    v    v    v     v
 APM  CRM  SCIM  B2C  ALM
  \    |    |    |    /
   \   |    |    |   /
    v  v    v    v  v
     IncidentContext
            |
            v
     Synthesis Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  IncidentContext at a glance
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IncidentContext
  - intake
  - resolved_identity
  - evidence
  - hypotheses
  - audit_log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the design story. In part 2, I'll show what happened when the first live tickets hit the wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Anthropic, "Building effective agents"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.github.io/openai-agents-python/" rel="noopener noreferrer"&gt;OpenAI Agents SDK docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.github.io/openai-agents-python/multi_agent/" rel="noopener noreferrer"&gt;OpenAI Agents orchestration docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;AGENTS.md project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Cross-boundary communication between desktop and web</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Mon, 23 Feb 2026 10:59:32 +0000</pubDate>
      <link>https://dev.to/eelcolos/cross-boundary-communication-between-desktop-and-web-575n</link>
      <guid>https://dev.to/eelcolos/cross-boundary-communication-between-desktop-and-web-575n</guid>
      <description>&lt;p&gt;We have a desktop product that customers actively use, and we want to migrate toward a SaaS offering. In practice, that means we need &lt;strong&gt;backwards compatibility&lt;/strong&gt; while we ship new features.&lt;/p&gt;

&lt;p&gt;During that transition you often end up in a "hybrid" state: a desktop shell still exists, but more and more UI and logic moves into web technology (for example hosted inside WebView2).&lt;/p&gt;

&lt;p&gt;That hybrid state introduces a core challenge: &lt;strong&gt;how do you preserve everyday interactions across boundaries?&lt;/strong&gt; Drag &amp;amp; drop, keyboard copy/paste, focus, selection, and other "it just works" behaviors tend to break the moment parts of the UI live in different browsing contexts (iframes/windows) or even in a host process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo repo (reference implementation):&lt;/strong&gt; &lt;a href="https://github.com/EelcoLos/iframe-dnd-demo" rel="noopener noreferrer"&gt;https://github.com/EelcoLos/iframe-dnd-demo&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://eelcolos.github.io/iframe-dnd-demo/" rel="noopener noreferrer"&gt;https://eelcolos.github.io/iframe-dnd-demo/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A pragmatic way to do incremental delivery is to introduce new modules behind &lt;strong&gt;explicit boundaries&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embed legacy/new pieces via IFrame&lt;/li&gt;
&lt;li&gt;split experiences into separate windows when the host is desktop (or when multi-monitor workflows help)&lt;/li&gt;
&lt;li&gt;use Web Components to build reusable UI pieces without betting on one framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next question becomes: how do those pieces communicate so interactions (drag &amp;amp; drop, keyboard copy/paste) still work across boundaries. And how does it bridge to the desktop host?&lt;/p&gt;

&lt;p&gt;This post outlines a simple architecture: &lt;strong&gt;message passing all the way down&lt;/strong&gt;, but with clear layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM messaging for iframe ↔ parent (&lt;code&gt;window.postMessage&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Cross-window transport when needed (BroadcastChannel with fallbacks)&lt;/li&gt;
&lt;li&gt;WebView2 web messaging for web ↔ host (&lt;code&gt;window.chrome.webview.postMessage&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------------ Desktop host (WebView2) -------------------------+
| WebMessageReceived  &amp;lt;---  chrome.webview.postMessage(...)                |
|        ^                                                                 |
|        |  CoreWebView2.PostWebMessageAsJson/String(...)                  |
|        |                                                                 |
|  +---------------- Parent web shell (coordinator) --------------------+  |
|  | iframe &amp;lt;-&amp;gt; parent: window.postMessage                              |  |
|  | cross-window (optional): BroadcastChannel                          |  |
|  |   fallback: postMessage relay via coordinator (Firefox ETP)        |  |
|  +--------------------------------------------------------------------+  |
+--------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In the web layer, the parent shell acts as a coordinator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;routes messages between iframes&lt;/li&gt;
&lt;li&gt;handles cross-iframe pointer interactions (coordinate conversion, hit-testing)&lt;/li&gt;
&lt;li&gt;holds shared state (e.g., clipboard-like state for keyboard copy/paste)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the parent shell optionally bridges certain messages to the desktop host.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 1: iframe ↔ parent (DOM &lt;code&gt;postMessage&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;This is regular browser messaging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set an explicit &lt;code&gt;targetOrigin&lt;/code&gt; (avoid &lt;code&gt;'*'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;validate the sender origin on receive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the demo, this routing enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pointer-based drag &amp;amp; drop across iframes&lt;/li&gt;
&lt;li&gt;keyboard copy/paste across iframes&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Layer 2: parent ↔ desktop host (WebView2 WebMessage)
&lt;/h2&gt;

&lt;p&gt;WebView2 provides a separate channel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JS → host: &lt;code&gt;window.chrome.webview.postMessage(...)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;host → JS: &lt;code&gt;CoreWebView2.PostWebMessageAsJson/String(...)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;JS receive: &lt;code&gt;window.chrome.webview.addEventListener('message', ...)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft’s docs emphasize treating web content as untrusted and validating origins / message payloads.&lt;/p&gt;
&lt;h2&gt;
  
  
  Message contracts: be explicit (the “action/type” field)
&lt;/h2&gt;

&lt;p&gt;The implementation choice I like most in this demo: every message carries a clear &lt;strong&gt;discriminator&lt;/strong&gt; so the receiver can route behavior.&lt;/p&gt;
&lt;h3&gt;
  
  
  Web ↔ web (iframes/windows): &lt;code&gt;type&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;In the browser-to-browser layer, the repo uses &lt;code&gt;type&lt;/code&gt; values like &lt;code&gt;dragStart&lt;/code&gt;, &lt;code&gt;parentDrop&lt;/code&gt;, &lt;code&gt;itemCopied&lt;/code&gt;, &lt;code&gt;requestPaste&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;Example shapes from the repo’s &lt;code&gt;API.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dragStart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Item 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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frame-a"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parentDrop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dragData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&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;If you want versioning too, you can wrap that idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"v"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"itemCopied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frame-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"itemData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Web ↔ host (WebView2): &lt;code&gt;action&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For the WebView2 host bridge, the demo uses an &lt;code&gt;action&lt;/code&gt; field for the same purpose (route on &lt;code&gt;action&lt;/code&gt; in &lt;code&gt;WebMessageReceived&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Example shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"copy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"450"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Contract in action (from the demo repo)
&lt;/h3&gt;

&lt;p&gt;JS side (from &lt;code&gt;public/webcomponent-table-source-html5.html&lt;/code&gt;) posts JSON strings to the WebView2 host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// If running in WebView2 (C# host), also send copy to native code&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedRow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedRow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedRow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&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 same page also posts other actions like &lt;code&gt;dragstart&lt;/code&gt;, &lt;code&gt;dragend&lt;/code&gt;, and (on double-click) &lt;code&gt;drop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;C# side (from &lt;code&gt;WebView2App/HybridModeWindow.xaml.cs&lt;/code&gt;) receives and routes based on &lt;code&gt;action&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;WebViewSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CoreWebView2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebMessageReceived&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;CoreWebView2_WebMessageReceived&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;CoreWebView2_WebMessageReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&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;CoreWebView2WebMessageReceivedEventArgs&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;string&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="k"&gt;null&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;message&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="nf"&gt;TryGetWebMessageAsString&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Message is not a string, try getting it as JSON&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebMessageAsJson&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Message format examples:&lt;/span&gt;
    &lt;span class="c1"&gt;// {"action":"drop","description":"...","quantity":12,"unitPrice":450}&lt;/span&gt;
    &lt;span class="c1"&gt;// {"action":"copy","description":"...","quantity":"12","unitPrice":"450"}&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;IsNullOrEmpty&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&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;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JsonDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RootElement&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;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;actionProp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&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;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actionProp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"drop"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// parse description/quantity/unitPrice and add to the target DataGridView&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&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;action&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"copy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// store the copied data so Ctrl+V in the target window can paste it&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;Why it matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can be backwards compatible across modules&lt;/li&gt;
&lt;li&gt;you can implement request/response via &lt;code&gt;correlationId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;you can validate shape (and reject unknown/untrusted messages)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing strategy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In-web behavior: Playwright is a good fit, treating iframes and windows as first-class.

&lt;ul&gt;
&lt;li&gt;Testing iframe drag and drop with Playwright&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Cross-window behavior: add tests that open child pages from a coordinator and assert keyboard interactions.&lt;/li&gt;

&lt;li&gt;Host bridge behavior: test separately at the desktop integration layer (WebMessageReceived handlers, navigation/origin checks).&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security checklist (practical)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Validate origins for DOM postMessage: set a strict &lt;code&gt;targetOrigin&lt;/code&gt; on send, and on receive validate
&lt;code&gt;event.origin&lt;/code&gt; (allowlist) and optionally &lt;code&gt;event.source&lt;/code&gt; before trusting &lt;code&gt;event.data&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In WebView2, always check the current document origin before trusting messages.&lt;/li&gt;
&lt;li&gt;Prefer JSON messages and validate schema.&lt;/li&gt;
&lt;li&gt;Disable features you don’t need (host objects, web messaging, scripts).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Web/native interop: &lt;a href="https://learn.microsoft.com/en-us/microsoft-edge/webview2/how-to/communicate-btwn-web-native" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/microsoft-edge/webview2/how-to/communicate-btwn-web-native&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;WebView2 security: &lt;a href="https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/security" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/security&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Frames in WebView2: &lt;a href="https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/frames" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/frames&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo repo: &lt;a href="https://github.com/EelcoLos/iframe-dnd-demo" rel="noopener noreferrer"&gt;https://github.com/EelcoLos/iframe-dnd-demo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>javascript</category>
      <category>webview2</category>
      <category>webdev</category>
    </item>
    <item>
      <title>.NET File‑Based Apps for API Prototyping: What Bit Me on First Run</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Thu, 29 Jan 2026 12:03:21 +0000</pubDate>
      <link>https://dev.to/eelcolos/net-file-based-apps-for-api-prototyping-what-bit-me-on-first-run-na2</link>
      <guid>https://dev.to/eelcolos/net-file-based-apps-for-api-prototyping-what-bit-me-on-first-run-na2</guid>
      <description>&lt;p&gt;Using .NET file-based apps (via &lt;code&gt;dotnet run app.cs&lt;/code&gt;) enables rapid prototyping and simplified project structure by eliminating project scaffolding.&lt;br&gt;
This feature was announced for dotnet 10 just before .Net Conf 2025: &lt;a href="https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a neat way to try ideas quickly.&lt;br&gt;
I tried to find out if this would work with FastEndpoints to demo an API that way.&lt;/p&gt;
&lt;h2&gt;
  
  
  Example (FastEndpoints + file-based app)
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Web&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;FastEndpoints&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="m"&gt;7&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;FastEndpoints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;span class="n"&gt;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;AddFastEndpoints&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;UseFastEndpoints&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;MyRequest&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;FirstName&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;LastName&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;Age&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;MyResponse&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;FullName&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;IsOver18&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;MyEndpoint&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/user/create"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;AllowAnonymous&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;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="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OkAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Age&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="n"&gt;cancellation&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;h2&gt;
  
  
  Gotcha: JSON/AOT defaults can break “works with web”
&lt;/h2&gt;

&lt;p&gt;When running with the web SDK in file-based apps, you can hit a runtime startup error like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.NotSupportedException: JsonTypeInfo metadata for type
'System.Collections.Generic.IEnumerable`1[System.String]' was not provided by TypeInfoResolver of type
'[AppJsonSerializerContext]'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is effectively a &lt;code&gt;System.Text.Json&lt;/code&gt; source-generation/AOT mismatch: the app is configured to require generated metadata, but the required root types were not generated.&lt;br&gt;
This 'gotcha' is not pointed out in the dev blog, but one of the first bullet points at the learn article does hint at it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Key benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;   Reduced boilerplate for simple applications.&lt;/li&gt;
&lt;li&gt;   Self-contained source files with embedded configuration.&lt;/li&gt;
&lt;li&gt;   &lt;strong&gt;Native AOT publishing enabled by default.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;   Automatic packaging as .NET tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Why this happens (NativeAOT + JSON)
&lt;/h2&gt;

&lt;p&gt;With NativeAOT / trimming, reflection is restricted, so &lt;code&gt;System.Text.Json&lt;/code&gt; can't reliably do reflection-based (de)serialization.&lt;br&gt;
In practice, you need to provide compile-time metadata via &lt;strong&gt;source generation&lt;/strong&gt; (&lt;code&gt;JsonSerializerContext&lt;/code&gt; / &lt;code&gt;JsonSerializable&lt;/code&gt;), i.e. custom serialization setup for the types your endpoints bind/return.&lt;/p&gt;

&lt;p&gt;Because this is a lot of "serialization bookkeeping" for a single-file prototype, I currently prefer disabling NativeAOT for this scenario.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fix (per gist): disable AOT
&lt;/h2&gt;

&lt;p&gt;To opt out of the default NativeAOT behavior (and avoid the JSON metadata requirement while prototyping), add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="n"&gt;PublishAot&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the app as regular JIT again, so &lt;code&gt;System.Text.Json&lt;/code&gt; can use reflection and the error goes away.&lt;/p&gt;

&lt;p&gt;You can find the entire gist at:&lt;br&gt;
&lt;a href="https://gist.github.com/EelcoLos/1c5f3c6be9ac765719ee880f3dcfec71" rel="noopener noreferrer"&gt;https://gist.github.com/EelcoLos/1c5f3c6be9ac765719ee880f3dcfec71&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/serialization/8.0/publishtrimmed" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/dotnet/core/compatibility/serialization/8.0/publishtrimmed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>fastendpoints</category>
      <category>aot</category>
    </item>
    <item>
      <title>Fantastic Knowledge &amp; How to Retain It</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Tue, 09 Dec 2025 15:29:44 +0000</pubDate>
      <link>https://dev.to/eelcolos/fantastic-knowledge-how-to-retain-it-3682</link>
      <guid>https://dev.to/eelcolos/fantastic-knowledge-how-to-retain-it-3682</guid>
      <description>&lt;p&gt;In the spirit of giving, consider this &lt;em&gt;a&lt;/em&gt; guide to retaining knowledge using a &lt;strong&gt;personal&lt;/strong&gt; knowledge base system. The goal is simple: turn 'aha' moments into captured insights you can reliably reuse. Then, turn those captured insights into reliable recall using everyday practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Thoughts to Notes
&lt;/h2&gt;

&lt;p&gt;When I'm talking about retaining knowledge, I myself think back to the days of note taking in school.&lt;br&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%2Ff7fs2gs859ftdp4e1rlp.jpg" 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%2Ff7fs2gs859ftdp4e1rlp.jpg" alt="notes in the margin" width="800" height="602"&gt;&lt;/a&gt;&lt;br&gt;
Back then, before I had note-taking apps and wikis, I realized that remembering starts with externalizing. I used to fill the margins of books with notes, underlines, symbols, and even loose inlays. Small, personal anchors to help me retain what mattered. In this guide, we'll look at how to recreate that same kind of durable knowledge retention with modern tools and habits.&lt;/p&gt;

&lt;p&gt;From that, I learned a bit of the techniques that still work today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Techniques That Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Capture 'aha' immediately: write a note, i.e. marginal notes or drop sticky inlays while interacting with information, especially the things that make you understand what is being written: the 'aha'.&lt;/li&gt;
&lt;li&gt;Title + one-sentence thesis: force clarity early so future-you retrieves faster.&lt;/li&gt;
&lt;li&gt;Context first: add why it matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: What note did I create for Copilot Spaces
&lt;/h3&gt;

&lt;p&gt;So, to give an example of what I wrote when I first learned about Copilot Spaces (url: &lt;a href="https://github.com/copilot/spaces" rel="noopener noreferrer"&gt;https://github.com/copilot/spaces&lt;/a&gt;), I wrote the following:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Within GitHub, &lt;strong&gt;Copilot Spaces&lt;/strong&gt; can serve as focused, persistent workspaces for specific knowledge domains (e.g. "knowledge retainment", "copilot agent"). Each Space aggregates code examples, ADRs, runbooks, exploratory prototypes, and curated prompts, reducing siloed tribal knowledge and enabling asynchronous onboarding.&lt;br&gt;
it does help for [[zettelkasten]] type of knowledge, as it scopes to the topic itself&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It shows the 'aha', for it being a shareable topic scoped option for GitHub regarding knowledge retainment. Especially use of aggregate code examples, ADRs, etc.&lt;br&gt;
the 'Why it matters' is addressed as well: it helps the Zettelkasten type of knowledge, scoping it to topic.&lt;br&gt;
As title, I defined this as 'copilot-spaces-can-be-topic-specific-knowledge-base'.&lt;/p&gt;

&lt;p&gt;Now, these techniques make you have a bunch of notes, but these by themselves are a ball-of-mud. So, after this, we want to think of mapping these thoughts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Mind Mapping Still Matters
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Mind mapping is a brainstorming technique that organizes information hierarchically around a central topic&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The brainstorming technique on the topic can be disjointed from the information retrieval, like a conference. Though you can brainstorm on the topic while being present on the session too. &lt;br&gt;
At that moment, you can bind the information of the retrieval session to an idea. &lt;br&gt;
To have this information structured in a general way of thinking, we can use a system called 'Zettelkasten'.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zettelkasten: Core Workflow Anatomy
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The Zettelkasten method (German for 'slip box' or 'note box') is a system for personal knowledge management and note-taking, developed by the German sociologist Niklas Luhmann. Its purpose is to capture and connect ideas in a dynamic network that fosters creative thinking and the generation of new insights, rather than simply collecting information. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The 'zettel' (slip) is a note in a 'kast' (box) to organize (parts of) a topic. As you build more and more notes an organized box can look like this:&lt;/p&gt;

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

&lt;p&gt;A Zettelkasten note &lt;em&gt;can&lt;/em&gt; have a lot of information on it. what it primarily should do is to help you structure the earlier created note in a way that it can be found and that it finds its way into other information you found before.&lt;br&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%2Far16luszmy7bs7swo4mn.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%2Far16luszmy7bs7swo4mn.png" alt="note-overview" width="800" height="415"&gt;&lt;/a&gt;&lt;br&gt;
The body is still here and still the most important. It's just being added with metadata in the header, such as the unique identifier title and tags regarding which topics it's close to.&lt;br&gt;
A footer can be used; it's main regarding source of knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a web of information using links
&lt;/h3&gt;

&lt;p&gt;The earlier created title in the example shows usage here in the 'linkage'. As shown in the example above, you can use double brackets to reference a link to another note. These connections are in a way your 'oh, this thing refers to another'. For me, the earlier usage of 'copilot-spaces-can-be-topic-specific-knowledge-base', makes the searching of such notes easier, as it would start or complete a (phrase of a) sentence.&lt;br&gt;
When you get to connect a decent chunk of these cards, you can probably also say something about the higher level topic. To me, in this regard of the title just now, it could be regarding this blog: writing about how Zettelkasten works for me. It wouldn't be an entire blog, but just some keywords for you to write what things are about and how that connects on a grand stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  When would you use it then?
&lt;/h3&gt;

&lt;p&gt;I've been asked as well: well, nice and all, but storing a lot and not using it, is just hoarding.&lt;br&gt;
All my blogs so far are mainly made of those 'aha' moments in one way or another. Furthermore, I can think of your observability log queries that you might need from time to time to be those 'aha' or 'don't google this over and over again' sections. I've lately been saving Application Insights  KQL queries. So, applying it to recall occasionally used information is a good use-case. &lt;/p&gt;

&lt;p&gt;Should you carry a notebook with you at all times learning about this? Maybe so, maybe not. It doesn't have to be your main retainer of notes though. We have tools to help store them digitally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing Notes Digitally (Obsidian, Notion)
&lt;/h2&gt;

&lt;p&gt;For Zettelkasten-style notes, Obsidian and Notion both work well: Obsidian excels with local Markdown and fast link creation; Notion adds flexible databases and views. My setup leans toward  Markdown for speed, with occasional Obsidian views for structure notes that index neighborhoods of related ideas. Whatever you choose, keep notes atomic, use clear titles, and maintain lightweight structure notes rather than sprawling folders.&lt;/p&gt;

&lt;p&gt;My experience transferring knowledge from some keywords or one sentence in a knowledge base system, and have it already have links, etc. is really hard. Too hard to maintain yourself, I would argue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using AI to help your knowledgebase
&lt;/h2&gt;

&lt;p&gt;Then I noticed a YouTube video by NetworkChuck titled 'You've Been Using AI the Hard Way'. In it, he describes the use of AI as an accelerator. The CLI tools like Gemini CLI, Copilot and Claude might focus on code by default, but it is never stated that they should &lt;strong&gt;solely&lt;/strong&gt; be used for it.&lt;br&gt;
In fact, if you pass a small instruction file to those CLI tools, you can explain that you'd like the output in a Zettelkasten form. This way, you can leverage the 'aha' moment even more: state the topic and 'aha' to the LLM and it'll create the note for you.&lt;/p&gt;

&lt;p&gt;AI can also help as a research tool. For this, you could also check out &lt;strong&gt;NotebookLM&lt;/strong&gt; from Google. Technically, you should be able to have the same result via CLI. However, I tried to get a training regarding musical world building with a dozen YouTube, PDF and website sources. Here, NotebookLM &lt;em&gt;does&lt;/em&gt; provide value over cli in my humble opinion. It could be because it will use the entire transcripts of YouTube videos, whereas the CLI will only use your markdown notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Niklas Luhmann - Slip-box Method (original practice descriptions and archival analyses). also   &lt;a href="https://zettelkasten.de/introduction/" rel="noopener noreferrer"&gt;https://zettelkasten.de/introduction/&lt;/a&gt; has a lot of information on this topic.&lt;/li&gt;
&lt;li&gt;NetworkChuck - "You've Been Using AI the Hard Way (Use This Instead)" (video): &lt;a href="https://www.youtube.com/watch?v=MsQACpcuTkU" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=MsQACpcuTkU&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Copilot Spaces - Topic-scoped knowledge workspaces: &lt;a href="https://docs.github.com/en/copilot/concepts/context/spaces" rel="noopener noreferrer"&gt;https://docs.github.com/en/copilot/concepts/context/spaces&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NotebookLM - Using external sources (e.g., videos) to augment a personal knowledge base: &lt;a href="https://notebooklm.google/" rel="noopener noreferrer"&gt;https://notebooklm.google/&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Personal Experience Highlights
&lt;/h2&gt;

&lt;p&gt;So, all in all, my highlights on retaining knowledge are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Topic-scoped workspaces (e.g., Copilot Spaces) function as focused knowledge bases; scoping artifacts and prompts per topic improves retrieval and supports a Zettelkasten-like flow. In Obsidian vaults these can be folders.&lt;/li&gt;
&lt;li&gt;CLI-driven AI (e.g. Gemini CLI) is faster than browser UIs and works directly with local Markdown notes, keeping ideation and distillation tightly integrated.&lt;/li&gt;
&lt;li&gt;Experimenting with NotebookLM and AI CLI to incorporate external sources (like videos) into the graph, followed by manual distillation into atomic notes and vetted links.&lt;/li&gt;
&lt;li&gt;Treat the system as an active thinking partner: keep notes atomic, rewrite often, and periodically refresh structure notes that organize neighborhoods.&lt;/li&gt;
&lt;li&gt;Tooling matters: VS Code with lightweight mind-mapping add-ons accelerates the map-to-note funnel without turning maps into storage.&lt;/li&gt;
&lt;li&gt;Converting quotes into single-idea notes and linking back to an overview improves reuse and prevents annotation bloat.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A healthy Zettelkasten is less a knowledge base 'warehouse' and more a conversational partner. Mind maps and AI copilots amplify its adaptability when used as catalysts, not crutches. The goal is compound insight: small, precise notes that keep paying dividends across projects and time.&lt;/p&gt;

</description>
      <category>knowledgemanagement</category>
      <category>ai</category>
      <category>zettelkasten</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Lessons learned implementing SCIM with Microsoft Entra and the SCIM Validator</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Fri, 28 Nov 2025 15:00:54 +0000</pubDate>
      <link>https://dev.to/eelcolos/lessons-learned-implementing-scim-with-microsoft-entra-and-the-scim-validator-20gl</link>
      <guid>https://dev.to/eelcolos/lessons-learned-implementing-scim-with-microsoft-entra-and-the-scim-validator-20gl</guid>
      <description>&lt;p&gt;I had to redo the entire SCIM validator journey after we've migrated to a new company. This article shares the practical lessons from that rework: tightening concurrency, adding hybrid caching, clarifying when /Schemas actually matters and structuring validation runs so progress is repeatable instead of guesswork. SCIM still promises automated provisioning from an IdP, but the path to a production-grade, Entra-compatible implementation is about disciplined iteration, not just “passing the tests.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SCIM
&lt;/h2&gt;

&lt;p&gt;Once integrated, lifecycle events (create, update, deactivate) flow from your IdP without manual admin work, improving security posture and reducing bespoke connector logic. Interoperability hinges on spec fidelity: consistent status codes, predictable resource representation, correct filtering, and pagination behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Train of thought
&lt;/h2&gt;

&lt;p&gt;The Microsoft SCIM Validator applies stricter rules for connectors intended for publication in the Microsoft Entra app store. If targeting store publication, aim for full compliance across correctness, concurrency, and performance; if not, be mostly compliant with RFC 7643/7644 and Entra guidance while prioritizing reliability and pragmatic trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation strategy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start with the synchronous (“preview”) validator suite to establish deterministic behavior. This path exposed spec gaps cleanly (status codes, PATCH semantics, schema responses) without concurrency noise.&lt;/li&gt;
&lt;li&gt;Tackle the parallel/asynchronous suite only after idempotency and locking are solid. Rapid sequences (like PATCH immediately after CREATE) surfaced race conditions when write visibility lagged behind reads.&lt;/li&gt;
&lt;li&gt;Use slight pacing as a diagnostic tool (about 300ms between requests) to reduce false negatives from artificial races; remove pacing once your server proves truly concurrent-safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;Essential for Entra-compatible performance. The validator repeatedly fetches stable resources (e.g., &lt;code&gt;/Schemas&lt;/code&gt;, frequent user/group reads), so hitting your datastore for every request becomes a bottleneck and tempts reimplementation of local validator logic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal/dev: a simple in-memory cache (e.g., &lt;code&gt;IMemoryCache&lt;/code&gt;) suffices for single-instance development.&lt;/li&gt;
&lt;li&gt;Production/multi-instance: a hybrid model with in-memory plus a distributed cache (e.g., Redis) improves throughput and keeps repeated fetches fast across instances.&lt;/li&gt;
&lt;li&gt;Cache stable data (schemas, group memberships that change infrequently) aggressively; respect cache invalidation for writes and reflect updated &lt;code&gt;etags&lt;/code&gt;/versioning correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Concurrency and idempotency
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Make PUT/PATCH idempotent — retries from the validator should not corrupt state or produce inconsistent responses.&lt;/li&gt;
&lt;li&gt;Adopt optimistic concurrency (versioning or &lt;code&gt;etags&lt;/code&gt;) and enforce If-Match semantics where applicable, so conflicting updates are clear and recoverable.&lt;/li&gt;
&lt;li&gt;Normalize case for attributes (userName, emails) to avoid equality-test failures, and ensure multi-valued attributes return deterministic ordering or include explicit primary markers.&lt;/li&gt;
&lt;li&gt;Throttle intentionally with &lt;code&gt;429 Too Many Requests&lt;/code&gt; and include Retry-After; avoid generic 500s under load which obscure recoverable capacity issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Spec coverage priorities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Implement &lt;code&gt;/Schemas&lt;/code&gt; early if targeting Microsoft Entra store publication (the validator uses it for capability discovery). For non-store implementations, &lt;code&gt;/Schemas&lt;/code&gt; is not required; focus on RFC 7643/7644 compliance and core provisioning endpoints.&lt;/li&gt;
&lt;li&gt;Return precise status codes: 200/201/204 for success paths, 400 for bad requests, 404 for unknown resources, 409 for conflicts, 412 for precondition failures, and 429 for throttling.&lt;/li&gt;
&lt;li&gt;Gradually flesh out filtering and pagination. Support the basics first (_startIndex, count, filter eq/and), then expand to more complex filters once stability is proven.&lt;/li&gt;
&lt;li&gt;Ensure consistent resource representations and stable IDs; avoid transient fields that vary run-to-run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local exposure &amp;amp; tunneling
&lt;/h2&gt;

&lt;p&gt;High-fidelity SCIM validation requires the external validator to reach your local endpoint reliably. Lightweight tunnels (e.g., &lt;code&gt;ngrok&lt;/code&gt;) are fast to start but their free/low tiers impose connection/session limits that parallel SCIM runs can exhaust. Azure Dev Tunnels (&lt;code&gt;devtunnel&lt;/code&gt;) offer longer-lived, higher-volume sessions with configurable access, reducing flakiness during heavy validator cycles.&lt;/p&gt;

&lt;p&gt;Example commands:&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;# Start local app on http://localhost:5010 (HTTP)&lt;/span&gt;
ngrok http 5010

&lt;span class="c"&gt;# Azure Dev Tunnel for HTTPS endpoint on port 5011 (longer-lived, higher volume)&lt;/span&gt;
devtunnel host &lt;span class="nt"&gt;-p&lt;/span&gt; 5011 &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; https
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a consistent port strategy (5010 HTTP dev, 5011 HTTPS) to simplify validator configuration.&lt;/li&gt;
&lt;li&gt;Regenerate tunnels per session; do not reuse stale URLs in cached validator runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tunneling checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;code&gt;ngrok&lt;/code&gt; for quick, short sessions; switch to &lt;code&gt;devtunnel&lt;/code&gt; for sustained or parallel test runs.&lt;/li&gt;
&lt;li&gt;Prefer HTTPS tunnel endpoints to match production security and avoid mixed content.&lt;/li&gt;
&lt;li&gt;Capture and inject the tunnel URL into validator configuration (SCIM base), then tear down afterward.&lt;/li&gt;
&lt;li&gt;Monitor for throttling/timeouts; change provider if limits introduce test noise.&lt;/li&gt;
&lt;li&gt;Avoid exposing non-SCIM debug endpoints; scope the tunnel to required routes only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Validate your SCIM base URL and TLS setup; the Microsoft SCIM Validator needs a reachable, secure endpoint with correct schema and auth.&lt;/li&gt;
&lt;li&gt;Log at correlation/request IDs to tie parallel validation threads back to server decisions, especially when diagnosing races or caching effects.&lt;/li&gt;
&lt;li&gt;Document supported attributes and mapping expectations for IdP admins; clarity reduces misconfiguration and brittle assumptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recommended path to success
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Decide early whether your connector targets Microsoft Entra store publication; choose the validator implementation accordingly (store-level: stricter; pass all, general: mostly compliant; pass most).&lt;/li&gt;
&lt;li&gt;Pass the synchronous validator suite to establish a clean baseline and identify spec gaps without concurrency noise.&lt;/li&gt;
&lt;li&gt;Add hybrid caching and optimistic concurrency to harden performance and correctness (idempotent PUT/PATCH, etags/If-Match, 429 with Retry-After).&lt;/li&gt;
&lt;li&gt;Run the parallel suite with slight pacing (~300ms) to isolate true concurrency defects, then remove pacing once stable.&lt;/li&gt;
&lt;li&gt;Re-run under realistic concurrent load and verify determinism in responses, versions, and resource representations.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;SCIM success is less about “just pass the tests” and more about disciplined concurrency, idempotency, and caching that make those tests repeatably pass at scale. Treat the synchronous suite as the baseline for correctness, and the parallel suite as the proving ground for production readiness.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SCIM Validator: &lt;a href="https://scimvalidator.microsoft.com/" rel="noopener noreferrer"&gt;https://scimvalidator.microsoft.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Entra SCIM guide: &lt;a href="https://learn.microsoft.com/en-us/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SCIM RFC 7643 (Schema): &lt;a href="https://www.rfc-editor.org/rfc/rfc7643" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc7643&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SCIM RFC 7644 (Protocol): &lt;a href="https://www.rfc-editor.org/rfc/rfc7644" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc7644&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>scim</category>
      <category>identity</category>
      <category>azure</category>
      <category>provisioning</category>
    </item>
    <item>
      <title>Where coding agents excel (and where they don't)</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Thu, 06 Nov 2025 07:04:52 +0000</pubDate>
      <link>https://dev.to/eelcolos/where-coding-agents-excel-and-where-they-dont-kba</link>
      <guid>https://dev.to/eelcolos/where-coding-agents-excel-and-where-they-dont-kba</guid>
      <description>&lt;p&gt;This is a first-person, hands-on writeup of how I experienced using Copilot coding agents today. I include examples, a gotcha I hit while using it, and a short checklist so you can try it in your repos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I started using Copilot coding agents
&lt;/h2&gt;

&lt;p&gt;I started experimenting with Copilot coding agents because I wanted to scaffold a lot of things which IDE-based agents do not, and to see what an assistant could do for me (refactoring scaffolding, creating test harnesses, running quick migrations) and actually &lt;em&gt;execute&lt;/em&gt; code in a prepared environment instead of just suggesting edits. Over the last few months I tested agents on various projects and refined a small set of rules that helped me experience the current possibilities of delegating tasks to an agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond IDE Agents: What Copilot Coding Agents Can Do
&lt;/h2&gt;

&lt;p&gt;Unlike traditional IDE-based agents, Copilot coding agents can perform a broader range of actions, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Branch Creation:&lt;/strong&gt; Automatically create new branches for development tasks.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Environment Setup and WIP PRs:&lt;/strong&gt; Set up development environments and initiate Work-In-Progress (WIP) Pull Requests on GitHub. &lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Task/Issue Deduction:&lt;/strong&gt; Understand and deduce tasks from issues or descriptions. Those tasks will be put in the WIP PR description.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Building and Running Tasks:&lt;/strong&gt; Execute build processes and run various development tasks defined in earlier steps.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WIP PR to Draft PR:&lt;/strong&gt; Transition a WIP Pull Request to a draft Pull Request with a pre-filled body.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where coding agents excel (and where they don't)
&lt;/h2&gt;

&lt;p&gt;Based on my experience, here's where coding agents currently shine and where they struggle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ Good at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Little tasks:&lt;/strong&gt; Small, well-defined tasks are handled well.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Dependency updates:&lt;/strong&gt; Updating dependencies is a task that agents can perform reliably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Okay at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Tasks with a lot of moving parts:&lt;/strong&gt; For example, major refactors can be challenging for agents to handle on their own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Terrible at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Major UI things:&lt;/strong&gt; User interface work is not a strong suit for coding agents at the moment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How agents run in practice: the &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt; convention
&lt;/h2&gt;

&lt;p&gt;The typical flow I use now is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;.github/workflows/copilot-setup-steps.yml&lt;/code&gt; on the repository's default branch.&lt;/li&gt;
&lt;li&gt;In that workflow, prepare everything the agent needs: runtimes, credentials (via environment secrets), databases, caches.&lt;/li&gt;
&lt;li&gt;Let the agent run against that prepared environment:  the agent then performs code changes, runs tests, and can open PRs as usual.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treat &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt; as a &lt;strong&gt;minimal pre-flight&lt;/strong&gt; script: it should be fast, idempotent, and only prepare what the agent actually needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal example I use as a starting point
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub Copilot Setup&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workflow_dispatch&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;copilot-setup-steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Environment for GitHub Copilot&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup dotnet&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;9.0.x&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure NuGet sources&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [[ -n "${{ secrets.PACKAGES_PAT }}" ]]; then&lt;/span&gt;
            &lt;span class="s"&gt;dotnet nuget update source "github" --username this-is-irrelevant --password ${{ secrets.PACKAGES_PAT }} --store-password-in-clear-text&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "Could not find Packages PAT"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure login&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;subscription-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_SUBSCRIPTION_ID }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes from my experience:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the job name should be &lt;code&gt;copilot-setup-steps&lt;/code&gt; and file &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt; &lt;strong&gt;exactly&lt;/strong&gt;. others are ignored.&lt;/li&gt;
&lt;li&gt;Keep this workflow &lt;strong&gt;quick&lt;/strong&gt; — long boot times mean more waiting (and, on paid plans, more compute cost).&lt;/li&gt;
&lt;li&gt;remember to have permissions regarding &lt;code&gt;checkout&lt;/code&gt; and your login. Respectively &lt;code&gt;contents: read&lt;/code&gt; and &lt;code&gt;id-token: write&lt;/code&gt;. &lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Secrets, environments, and the things that bit me
&lt;/h2&gt;

&lt;p&gt;I use a repository Environment named &lt;code&gt;copilot&lt;/code&gt; and put environment variables and secrets there instead of raw repository secrets. In practice put credentials the agent legitimately needs into the &lt;code&gt;copilot&lt;/code&gt; environment in the repo settings. Limit who can modify that environment. You might need to put organization credentials here as well. This mindset seems to be changing from time to time. Earlier, this wasn't necessary, now it is.&lt;/p&gt;

&lt;p&gt;A gotcha I ran into and how I handled them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites not always picked up&lt;/strong&gt;: Even when I put prerequisites steps in a GitHub issue, the agent wouldn't always pick them up. I found I needed to refer to the prerequisites in between every step. Luckily, with the new steering capabilities, I could guide the agent mid-session to ensure it followed the necessary steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical checklist (so you can try this quickly)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;.github/workflows/copilot-setup-steps.yml&lt;/code&gt; to your default branch.&lt;/li&gt;
&lt;li&gt;Add a repo environment named &lt;code&gt;copilot&lt;/code&gt; and add environment variables / environment secrets used by the setup workflow.&lt;/li&gt;
&lt;li&gt;Keep the setup job fast: install only what is necessary and cache dependencies.&lt;/li&gt;
&lt;li&gt;Consider self-hosted runners if you need internal network access or want to keep secrets entirely on-prem.&lt;/li&gt;
&lt;li&gt;Add a short &lt;code&gt;AGENTS.md&lt;/code&gt; describing rules (what an agent may change, code style, testing expectations).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Defining Agent Instructions: The &lt;code&gt;AGENTS.md&lt;/code&gt; Approach
&lt;/h2&gt;

&lt;p&gt;To ensure your Copilot coding agent operates effectively and adheres to project standards, it's crucial to provide clear and explicit instructions. A common practice is to create an &lt;code&gt;AGENTS.md&lt;/code&gt; file (or &lt;code&gt;copilot-instructions.md&lt;/code&gt; as I initially used) in your repository to house these guidelines. These instructions act as guardrails, directing the agent's behavior and ensuring consistency.&lt;/p&gt;

&lt;p&gt;Here are some examples of instructions I've used to guide my Copilot agent (rename 'Test' and 'Production' to your own purposes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Azure Best Practices:**&lt;/span&gt; When working with Azure, always invoke the &lt;span class="sb"&gt;`azure_development-get_best_practices`&lt;/span&gt; tool if available to ensure adherence to best practices.
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**E2E Test Rules:**&lt;/span&gt; Ensure End-to-End (E2E) tests do not contain &lt;span class="sb"&gt;`.only`&lt;/span&gt; in &lt;span class="sb"&gt;`.describe`&lt;/span&gt; or &lt;span class="sb"&gt;`.it`&lt;/span&gt; blocks. Remove any instances found.
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Coding Standards:**&lt;/span&gt; Apply specific coding standards for languages like C#, Bicep, Dockerfile, and TypeScript by following guidelines in their respective instruction files (e.g., &lt;span class="sb"&gt;`./.github/instructions/csharp.instructions.md`&lt;/span&gt;).
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Azure Environment Rules:**&lt;/span&gt; Adhere to strict rules for Azure deployments:
&lt;span class="p"&gt;    *&lt;/span&gt;   Use 'Test' for Test deployments and 'Production' for Production deployments.
&lt;span class="p"&gt;    *&lt;/span&gt;   Never deploy to test and production simultaneously.
&lt;span class="p"&gt;    *&lt;/span&gt;   Production deployments require PIM (Privileged Identity Management) role activation.
&lt;span class="p"&gt;    *&lt;/span&gt;   Prefer updating Bicep templates over direct Azure CLI modifications for infrastructure changes.
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**ADR Adherence:**&lt;/span&gt; Before implementing changes, review accepted Architectural Decision Records (ADRs) in the &lt;span class="sb"&gt;`docs/design-decisions`&lt;/span&gt; directory, ensuring their status is "Accepted" and considering their consequences.
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**PR Creation:**&lt;/span&gt; When creating a Pull Request, include a reference to an Azure Boards work item using the format &lt;span class="sb"&gt;`[AB#12345]`&lt;/span&gt; above any headings in the PR description.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pitfalls &amp;amp; advice from real runs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explore the capabilities of coding agents, with boundaries&lt;/strong&gt;: Know what it's good at (the small things first). Then, as you start to build your repo with instructions, gradually increase the hand-off to coding agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write clear tasks&lt;/strong&gt;: the agent will be much more useful if you give it a ticket with clear acceptance criteria and a test it must pass. I started using a PR template specifically for agent-generated PRs so reviewers know what to focus on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review everything&lt;/strong&gt;: agents will make suggestions that look plausible but can introduce subtle mistakes. Always review and run the test suite locally.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;I like how the combination of mission control (Agent HQ) + explicit pre-flight workflows made agents &lt;em&gt;useful&lt;/em&gt; rather than noisy. The plan mode and steering affordances gave me the confidence to run medium-length tasks without babysitting everything.&lt;/p&gt;

&lt;p&gt;At the same time, good repository hygiene is now essential: lock permissions on the &lt;code&gt;copilot&lt;/code&gt; environment, and maintain a short &lt;code&gt;AGENTS.md&lt;/code&gt; documenting what agents should and should not do. If you need absolute control over secrets or internal network access, &lt;/p&gt;

&lt;p&gt;Please try it 3 times before judging. Share your thoughts in the comments below.&lt;/p&gt;

</description>
      <category>github</category>
      <category>codingagent</category>
      <category>agentic</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Your ASP.NET Core LogLevel 'Warning' Still Sends Information Logs to Application Insights</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Mon, 29 Sep 2025 05:32:39 +0000</pubDate>
      <link>https://dev.to/eelcolos/why-your-aspnet-core-loglevel-warning-still-sends-information-logs-to-application-insights-e94</link>
      <guid>https://dev.to/eelcolos/why-your-aspnet-core-loglevel-warning-still-sends-information-logs-to-application-insights-e94</guid>
      <description>&lt;p&gt;I first noticed this pattern while helping a teammate trim noisy telemetry costs. We had set the global logging level in an ASP.NET Core app to Warning. We redeployed. Yet the Azure Portal continued to show a steady stream of Information traces arriving from the same service. It felt like the platform was ignoring us. It wasn’t. We were ignoring a subtle layering rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The puzzle
&lt;/h2&gt;

&lt;p&gt;Configuration (simplified):&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Everyone expected Warning to be the minimum loglevel. Application Insights kept getting &lt;code&gt;Information&lt;/code&gt; entries. The instinct is to blame the SDK. The real cause is that provider specific configuration lowers the threshold just for that provider. The Application Insights logger never re‑applies a minimum. It faithfully forwards what the &lt;code&gt;Microsoft.Extensions.Logging&lt;/code&gt; infrastructure lets through. That infrastructure had already been told: for the provider whose alias is ApplicationInsights(Abbreviated: AI), allow &lt;code&gt;Information&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the layers actually line up
&lt;/h2&gt;

&lt;p&gt;Think of the journey of a log:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your code calls &lt;code&gt;logger.LogInformation("User logged in")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The generic logging infrastructure consults all configured filters. A global default says Warning. A provider override for ApplicationInsights says Information. Because the target is that provider, Information is allowed.&lt;/li&gt;
&lt;li&gt;The Application Insights logger receives the entry. It calls its own &lt;code&gt;IsEnabled&lt;/code&gt;. That only checks two things: the level is not None and telemetry has not been globally disabled.&lt;/li&gt;
&lt;li&gt;The logger maps &lt;code&gt;LogLevel.Information&lt;/code&gt; to &lt;code&gt;SeverityLevel.Information&lt;/code&gt; and sends a &lt;code&gt;TraceTelemetry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Optional sampling or processors may still drop it downstream, but cost has already been incurred in your process and often in ingestion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No secret minimum. No hidden widening. Just configuration precedence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The confirmation in code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/microsoft/ApplicationInsights-dotnet/blob/98bc6c540e2dcccc78e4b356cd70e03d146a01ad/LOGGING/src/ILogger/ApplicationInsightsLogger.cs#L59-L69" rel="noopener noreferrer"&gt;&lt;code&gt;IsEnabled&lt;/code&gt; inside the AI logger&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt; &lt;span class="n"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;logLevel&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;None&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&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;telemetryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabled&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;a href="https://github.com/microsoft/ApplicationInsights-dotnet/blob/98bc6c540e2dcccc78e4b356cd70e03d146a01ad/LOGGING/src/ILogger/ApplicationInsightsLogger.cs#L125-L147" rel="noopener noreferrer"&gt;Level mapping (translation only)&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SeverityLevel&lt;/span&gt; &lt;span class="nf"&gt;GetSeverityLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt; &lt;span class="n"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&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;logLevel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&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;Critical&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;SeverityLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Critical&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&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;Error&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;SeverityLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SeverityLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SeverityLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&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;Debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;case&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="k"&gt;default&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;SeverityLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verbose&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;a href="https://github.com/microsoft/ApplicationInsights-dotnet/blob/98bc6c540e2dcccc78e4b356cd70e03d146a01ad/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs#L86-L100" rel="noopener noreferrer"&gt;&lt;code&gt;TelemetryClient.IsEnabled()&lt;/code&gt;&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsEnabled&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="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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisableTelemetry&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;So if telemetry is not disabled, everything that passed filtering is shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where teams stumble
&lt;/h2&gt;

&lt;p&gt;A provider override left behind by a template or copy paste. A category override intended for console logs but applied broadly. Or sampling giving the &lt;em&gt;illusion&lt;/em&gt; that filtering is working because only some Information entries survive. All of these hide the fact that the AI provider was fed the lower level in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making your minimum level actually stick
&lt;/h2&gt;

&lt;p&gt;Remove the provider override if you do not want it:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Or explicitly align it so future contributors see intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="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;If you prefer a programmatic assertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationInsightsLoggerProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="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="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine grained category tuning still works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Microsoft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyApp.NoisyComponent"&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;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationInsightsLoggerProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"MyApp.Important"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you must drop lower severity traces after they enter the pipeline (for example, during a temporary diagnostic burst), a telemetry processor can discard them, but remember you are paying the cost of generating them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MinimumSeverityProcessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ITelemetryProcessor&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;ITelemetryProcessor&lt;/span&gt; &lt;span class="n"&gt;_next&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;MinimumSeverityProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ITelemetryProcessor&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ITelemetry&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;TraceTelemetry&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeverityLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeverityLevel&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;SeverityLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&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="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;Registering a small factory (pattern varies by version) wires it in. Use this sparingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify instead of assuming
&lt;/h2&gt;

&lt;p&gt;Add console logging side by side. Emit one log at every level:&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"T"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogDebug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"D"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"W"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"E"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogCritical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Azure Logs query editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;traces
| where message in ("T","D","I","W","E","C")
| project timestamp, message, severityLevel
| order by timestamp desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see Information rows and you did not intend to, search your configuration for a provider or category override before blaming sampling or the SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal repro and its repair
&lt;/h2&gt;

&lt;p&gt;Broken:&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;AddApplicationInsightsTelemetry&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;ClearProviders&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;AddApplicationInsights&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;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;AddInMemoryCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Logging:LogLevel:Default"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Logging:ApplicationInsights:LogLevel:Default"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// silent lowering&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;MapGet&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="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"This still goes to AI"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"This also goes to AI"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Hi"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Repaired:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddInMemoryCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Logging:LogLevel:Default"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Warning"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or declare the intended loglevel again via &lt;code&gt;AddFilter&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful quick snippets
&lt;/h2&gt;

&lt;p&gt;An appsettings template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ConnectionString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InstrumentationKey=00000000-0000-0000-0000-000000000000"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"MyApp.ImportantArea"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A runtime toggle (handy for experiments, not production best practice):&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/toggle-ai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TelemetryConfiguration&lt;/span&gt; &lt;span class="n"&gt;cfg&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="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisableTelemetry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisableTelemetry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"Telemetry enabled: &lt;/span&gt;&lt;span class="p"&gt;{!&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisableTelemetry&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;h2&gt;
  
  
  When a provider override is the right choice
&lt;/h2&gt;

&lt;p&gt;You might intentionally gather richer telemetry centrally while keeping local console lean. You might temporarily elevate verbosity during an incident. You might selectively keep a high value category at Information in AI while leaving everything else at Warning. All valid, provided the difference is deliberate and documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final pre deploy pass
&lt;/h2&gt;

&lt;p&gt;Read through your combined logging configuration. Search for &lt;code&gt;ApplicationInsights&lt;/code&gt; under &lt;code&gt;Logging&lt;/code&gt;. Confirm any category lines that lower severity truly need to. Confirm sampling configuration matches what you expect. Issue a burst of test logs and verify what surfaces in the portal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;Application Insights .NET SDK repository:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/microsoft/ApplicationInsights-dotnet" rel="noopener noreferrer"&gt;https://github.com/microsoft/ApplicationInsights-dotnet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Official logging docs:&lt;br&gt;&lt;br&gt;
&lt;a href="https://learn.microsoft.com/aspnet/core/fundamentals/logging/" rel="noopener noreferrer"&gt;https://learn.microsoft.com/aspnet/core/fundamentals/logging/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sampling guidance:&lt;br&gt;&lt;br&gt;
&lt;a href="https://learn.microsoft.com/azure/azure-monitor/app/sampling" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/azure-monitor/app/sampling&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Nothing magical forced those Information traces through. Configuration precedence invited them. Once you internalize the path, debugging level mismatches becomes quick and boring. That is exactly what you want in an observability foundation.&lt;/p&gt;

&lt;p&gt;What are your experiences? Let them know in the comments below&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>logging</category>
      <category>applicationinsights</category>
      <category>observability</category>
    </item>
    <item>
      <title>Using FakeLoggerProvider (and ILoggerFactory) in FastEndpoints</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Fri, 05 Sep 2025 12:31:12 +0000</pubDate>
      <link>https://dev.to/eelcolos/using-fakeloggerprovider-and-iloggerfactory-in-fastendpoints-4oj6</link>
      <guid>https://dev.to/eelcolos/using-fakeloggerprovider-and-iloggerfactory-in-fastendpoints-4oj6</guid>
      <description>&lt;p&gt;In the first post we focused on &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt; for simple, targeted logger assertions.&lt;/p&gt;

&lt;p&gt;This follow-up goes a layer deeper: capturing &lt;em&gt;all&lt;/em&gt; logging via &lt;code&gt;FakeLoggerProvider&lt;/code&gt;, optionally wiring it into an &lt;code&gt;ILoggerFactory&lt;/code&gt;, and asserting over snapshots (not just the latest record). This is especially useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple categories log during a request&lt;/li&gt;
&lt;li&gt;Ordering matters (e.g. warning → error path)&lt;/li&gt;
&lt;li&gt;You want to assert absence or presence of specific patterns&lt;/li&gt;
&lt;li&gt;You're testing pipeline-style frameworks (here: FastEndpoints)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All examples use &lt;code&gt;.NET 8+&lt;/code&gt; and &lt;code&gt;Microsoft.Extensions.Diagnostics.Testing&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to use FakeLoggerProvider instead of FakeLogger
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Just assert one logger category&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observe everything (multiple categories)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FakeLoggerProvider&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need a factory (framework constructs loggers)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ILoggerFactory&lt;/code&gt; + provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query all log records with LINQ&lt;/td&gt;
&lt;td&gt;&lt;code&gt;provider.Collector.GetSnapshot()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;FakeLoggerProvider&lt;/code&gt; acts like any other logging provider. It gathers every &lt;code&gt;LogRecord&lt;/code&gt;. You can get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Collector.LatestRecord&lt;/code&gt; (single)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Collector.GetSnapshot()&lt;/code&gt; (immutable list)&lt;/li&gt;
&lt;li&gt;Structured state (&lt;code&gt;record.StateValues&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Exceptions (&lt;code&gt;record.Exception&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Scopes (&lt;code&gt;record.Scopes&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Minimal example (provider + factory)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FastEndpoints test sample&lt;/span&gt;
&lt;span class="c1"&gt;// NuGet: Microsoft.Extensions.Diagnostics.Testing&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;FastEndpoints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.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;Microsoft.Extensions.Logging.Testing&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;Xunit&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;Demo.LoggingTests&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;GetThingEndpoint&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EndpointWithoutRequest&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="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;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetThingEndpoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_log&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;GetThingEndpoint&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;GetThingEndpoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/things/{id}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&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;id&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;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&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;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;_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Empty id provided"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;ThrowError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid 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;_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fetching thing {ThingId}"&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;_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogDebug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Repository call starting"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Returning thing {ThingId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&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;cancellation&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;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;GetThingEndpointTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Logs_All_Expected_Messages_In_Order&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;provider&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;FakeLoggerProvider&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;ep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetThingEndpoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTestServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerProvider&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="p"&gt;);&lt;/span&gt;

                &lt;span class="c1"&gt;// If the framework builds loggers via ILoggerFactory, supply one:&lt;/span&gt;
                &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&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;=&amp;gt;&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggerFactory&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;ILoggerProvider&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="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ABC123"&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;None&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;records&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CategoryName&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;GetThingEndpoint&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Collection&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;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&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;Debug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&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="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Returning thing"&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;structured&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="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&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="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fetching thing"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kv&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="s"&gt;"ThingId"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&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;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"ABC123"&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;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We register &lt;code&gt;FakeLoggerProvider&lt;/code&gt; as &lt;code&gt;ILoggerProvider&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We create a concrete &lt;code&gt;LoggerFactory&lt;/code&gt; so anything resolving &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; via factory still flows through our provider.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Collector.GetSnapshot()&lt;/code&gt; gives an immutable list—safe to query multiple times.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Using LatestRecord vs GetSnapshot
&lt;/h2&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;last&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LatestRecord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Full list (ordered by arrival)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSnapshot&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;warnings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;LatestRecord&lt;/code&gt; when you just expect &lt;em&gt;one&lt;/em&gt; or want a quick “something logged” assertion. Prefer snapshots plus LINQ for clarity when order or filtering matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling no logs (expected or defensive)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FakeLoggerProvider.Collector.LatestRecord&lt;/code&gt; throws &lt;code&gt;InvalidOperationException&lt;/code&gt; if nothing was logged yet. Catch/Assert when verifying &lt;em&gt;absence&lt;/em&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;ex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Throws&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;&amp;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;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LatestRecord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No records logged."&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or simply:&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;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSnapshot&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Asserting warnings for not-found cases (mirroring the example)
&lt;/h2&gt;

&lt;p&gt;From the original test style:&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;warning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&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;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                         &lt;span class="n"&gt;r&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;$"Unable to find user by name &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&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;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Structured state and scopes
&lt;/h2&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;rec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&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="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fetching thing"&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;thingId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateValues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kv&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="s"&gt;"ThingId"&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;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ABC123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thingId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Scopes (if code used BeginScope)&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;scope&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// scope is a formatted scope string or key/value pair sequence&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When you must register ILoggerFactory
&lt;/h2&gt;

&lt;p&gt;Some frameworks (or helper libs) explicitly request &lt;code&gt;ILoggerFactory&lt;/code&gt;. If you only register &lt;code&gt;ILoggerProvider&lt;/code&gt;, they create their own factory and your provider may be skipped. Supplying:&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;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&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;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggerFactory&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;ILoggerProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ensures a single pipeline. (If you add multiple providers, pass them all in the array.)&lt;/p&gt;

&lt;p&gt;I often start with &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt; and switch to provider + factory the moment I need to assert more than one message or category.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pitfall&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No logs captured&lt;/td&gt;
&lt;td&gt;Ensure provider registered before endpoint created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LatestRecord throws&lt;/td&gt;
&lt;td&gt;Use GetSnapshot() for empties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong category filtered&lt;/td&gt;
&lt;td&gt;Compare &lt;code&gt;record.CategoryName&lt;/code&gt; to &lt;code&gt;typeof(TheType).FullName&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured value missing&lt;/td&gt;
&lt;td&gt;Ensure you used named template: LogInformation("Id {ThingId}", id)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Quick FastEndpoints factory helper (optional)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggingTestSetup&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEndpoint&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FakeLoggerProvider&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateWithLogging&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEndpoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TEndpoint&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IEndpoint&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;provider&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;FakeLoggerProvider&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;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEndpoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTestServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerProvider&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="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&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;=&amp;gt;&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggerFactory&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;ILoggerProvider&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;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;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;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FakeLoggerProvider&lt;/code&gt; elevates log testing from “one-off assert” to “full pipeline visibility.” Pairing it with an explicit &lt;code&gt;ILoggerFactory&lt;/code&gt; gives deterministic, framework-friendly logging in test hosts (including FastEndpoints). Use it when you outgrow the single-category simplicity of &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Happy testing.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>testing</category>
      <category>logging</category>
      <category>fastendpoints</category>
    </item>
    <item>
      <title>Better ILogger testing in .NET</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Fri, 22 Aug 2025 11:48:18 +0000</pubDate>
      <link>https://dev.to/eelcolos/better-ilogger-testing-in-net-2n1b</link>
      <guid>https://dev.to/eelcolos/better-ilogger-testing-in-net-2n1b</guid>
      <description>&lt;p&gt;Logging in .NET is solid in production, but when it comes to unit and integration tests… things get messy.&lt;br&gt;
I’ve often found myself skipping over logger checks because mocking &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; felt brittle or just not worth the effort.&lt;/p&gt;

&lt;p&gt;But here’s the thing: logs aren’t just background noise. They’re often the &lt;em&gt;first place you look&lt;/em&gt; when something goes wrong in production. If the logging isn’t there, or if it’s wrong, you’ll notice it the hard way.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why it’s actually worth checking logs in tests&lt;/li&gt;
&lt;li&gt;Why mocking loggers is frustrating&lt;/li&gt;
&lt;li&gt;How &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt; makes this easy, with examples you can drop straight into your tests (&lt;a href="https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.Testing/" rel="noopener noreferrer"&gt;Nuget&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Why check logging?
&lt;/h2&gt;

&lt;p&gt;Logs are your safety net. They give you visibility into what your app was doing when something went sideways. Without them, debugging turns into guesswork.&lt;/p&gt;

&lt;p&gt;A few reasons to test your logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Catch silly mistakes early, like logging the wrong variable, or writing unreadable object dumps, like &lt;code&gt;[Object object]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make sure log levels make sense. &lt;code&gt;LogError&lt;/code&gt; vs &lt;code&gt;LogDebug&lt;/code&gt; matters when monitoring and alerting are wired up.&lt;/li&gt;
&lt;li&gt;Keep operational contracts intact. Logs are consumed by dashboards, pipelines, and alerting systems. Breaking them silently can hurt in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So while it’s tempting to skip logging checks in tests, a little validation here goes a long way.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mocking &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; feels awkward
&lt;/h2&gt;

&lt;p&gt;If you’ve ever tried mocking ILogger with Moq, NSubstitute, or FakeItEasy, you probably know the pain.&lt;/p&gt;

&lt;p&gt;For example, to me, FakeItEasy states: if you use &lt;code&gt;A.Fake&lt;/code&gt; on any interface, it'll create something usable. However, when using&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;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fake&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;DemoClass&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then try and run it, it'll show:&lt;/p&gt;

&lt;p&gt;
  FakeItEasy error output
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to create fake of type Microsoft.Extensions.Logging.ILogger`1:
    No usable default constructor was found on the type Microsoft.Extensions.Logging.ILogger`1.
    An exception of type System.ArgumentException was caught during this call. Its message was:
    Can not create proxy for type Microsoft.Extensions.Logging.ILogger`1 because the target type is not accessible. 
    Make it public, or internal and mark your assembly with 
    [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=...")] 
    attribute, because assembly Microsoft.Extensions.Logging.Abstractions is strong-named. 
    (Parameter 'additionalInterfacesToProxy')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;Another way is shown at Moq (from &lt;a href="https://www.freecodecamp.org/news/how-to-use-fakelogger-to-make-testing-easier-in-net/" rel="noopener noreferrer"&gt;https://www.freecodecamp.org/news/how-to-use-fakelogger-to-make-testing-easier-in-net/&lt;/a&gt; ):&lt;/p&gt;

&lt;p&gt;
  Moq NotSupported example
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Arrange&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockLogger&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// pass the mockedLogger to our service&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;mockLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IInvoiceService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;Object&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;customerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="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="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;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Products&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;Product&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;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Ping pong balls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.00M&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
            &lt;span class="n"&gt;OrderDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="c1"&gt;// Act&lt;/span&gt;
        &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessOrder&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="c1"&gt;// Assert&lt;/span&gt;
        &lt;span class="n"&gt;mockLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&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="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order..."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Once&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;mockLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&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="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order processed successfully."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Once&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;which shows&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; System.NotSupportedException: 
Unsupported expression: x =&amp;gt; x.LogInformation("Processing order...", new[] {  })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Microsoft recognized how painful logging tests could be in real-world services. Mocking &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; often led to brittle tests or convoluted setups. To address this, they introduced &lt;strong&gt;&lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; in .NET 8, as part of the testing-friendly tooling described in &lt;a href="https://devblogs.microsoft.com/dotnet/fake-it-til-you-make-it-to-production/" rel="noopener noreferrer"&gt;“Fake It Til You Make It…To Production”&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt; is tiny, test-oriented, and captures logs in memory. You can assert log messages, levels, exceptions, and even structured state without relying on Moq, FakeItEasy, or reflection hacks.&lt;/p&gt;

&lt;p&gt;Here’s a minimal 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;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;Microsoft.Extensions.Logging.Testing&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;Xunit&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;MyServiceTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;DoWork_LogsExpectedMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Arrange: create a FakeLogger for MyService&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fakeLogger&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;FakeLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fakeLogger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// inject logger into service&lt;/span&gt;

        &lt;span class="c1"&gt;// Act&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;DoWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Assert: verify log message&lt;/span&gt;
        &lt;span class="kt"&gt;var&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;fakeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LatestRecord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Work done"&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;Message&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;With &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt;, you can quickly verify your logs without wrestling with generic delegates or fragile mock setups. For more complex scenarios, it also exposes all captured &lt;code&gt;LogRecord&lt;/code&gt;s, which is handy for asserting multiple messages or structured logging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full integration-test example (xUnit + WebApplicationFactory)
&lt;/h3&gt;

&lt;p&gt;Here’s a complete integration test that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates a &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt; for the category &lt;code&gt;T&lt;/code&gt; you care about (replace &lt;code&gt;MyService&lt;/code&gt; with the concrete type used as &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; in your app).&lt;/li&gt;
&lt;li&gt;Injects that instance into the test host via &lt;code&gt;WithWebHostBuilder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Calls an endpoint.&lt;/li&gt;
&lt;li&gt;Retrieves the registered logger from the test host and asserts using &lt;code&gt;LatestRecord&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&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.Linq&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;using&lt;/span&gt; &lt;span class="nn"&gt;FluentAssertions&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.AspNetCore.Mvc.Testing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.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;Microsoft.Extensions.Logging.Testing&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;Xunit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Replace `Program` with the class that defines your app's host entry (top-level Program in minimal APIs)&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;BudgetIntegrationTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IClassFixture&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WebApplicationFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;WebApplicationFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_factory&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;BudgetIntegrationTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebApplicationFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;GetBudget_ReturnsFile_AndLogsMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Replace `MyService` with the concrete type T used in ILogger&amp;lt;T&amp;gt;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fakeLogger&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;FakeLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Create a factory that injects our fake logger into DI&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithWebHostBuilder&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;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Remove any existing ILogger&amp;lt;MyService&amp;gt; registration (if present)&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existing&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;SingleOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceType&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;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;));&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="c1"&gt;// Register our fake logger as the ILogger&amp;lt;MyService&amp;gt; implementation&lt;/span&gt;
                &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;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;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="n"&gt;fakeLogger&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;// Create client and call the endpoint that triggers the log&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/budget/download"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// adjust path to your endpoint&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="c1"&gt;// Retrieve the registered logger from the test host and assert&lt;/span&gt;
        &lt;span class="kt"&gt;var&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;FakeLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="n"&gt;factory&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;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LatestRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;NotBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LatestRecord&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="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Returning budget file in"&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;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MyService&lt;/code&gt; should be replaced with the type used when your code calls &lt;code&gt;logger.LogInformation(...)&lt;/code&gt; or similar (often the service or controller class).&lt;/li&gt;
&lt;li&gt;If the app registers loggers differently (or uses factory-style providers), removing the existing descriptor ensures your fake gets picked up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LatestRecord&lt;/code&gt; will be &lt;code&gt;null&lt;/code&gt; if nothing was logged. Use assertions accordingly.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; is fantastic for production observability, but not designed to be easily asserted in tests. Instead of wrestling with &lt;code&gt;Log&amp;lt;TState&amp;gt;&lt;/code&gt; signatures or brittle mock setups, &lt;code&gt;FakeLogger&amp;lt;T&amp;gt;&lt;/code&gt; gives you a simple, inspectable test surface and keeps tests readable.&lt;/p&gt;

&lt;p&gt;I added a small FakeLogger demo that shows how to capture and assert ILogger output&lt;br&gt;
from a .NET service in unit tests. See the code and run the tests locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/EelcoLos/nx-tinkering" rel="noopener noreferrer"&gt;https://github.com/EelcoLos/nx-tinkering&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Implementation &amp;amp; review (merged PR): &lt;a href="https://github.com/EelcoLos/nx-tinkering/pull/726" rel="noopener noreferrer"&gt;https://github.com/EelcoLos/nx-tinkering/pull/726&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Exact file (on main):
&lt;a href="https://github.com/EelcoLos/nx-tinkering/blob/main/apps/fakelogger-demo.Test/MyServiceTests.cs" rel="noopener noreferrer"&gt;https://github.com/EelcoLos/nx-tinkering/blob/main/apps/fakelogger-demo.Test/MyServiceTests.cs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run locally (PowerShell)&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;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\apps\fakelogger-demo.Test\fakelogger-demo.Test.csproj&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy testing! 🚀&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
      <category>logging</category>
    </item>
    <item>
      <title>Lessons Learned Shipping .NET Apps with Docker, Alpine, and Kubernetes</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Fri, 27 Jun 2025 09:36:18 +0000</pubDate>
      <link>https://dev.to/eelcolos/lessons-learned-shipping-net-apps-with-docker-alpine-and-kubernetes-4n59</link>
      <guid>https://dev.to/eelcolos/lessons-learned-shipping-net-apps-with-docker-alpine-and-kubernetes-4n59</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🛠️ &lt;strong&gt;Update (4th of July, 2025):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Microsoft’s official .NET 8+ base images now define a secure numeric user via &lt;code&gt;APP_UID&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
If you're using those, prefer &lt;code&gt;USER $APP_UID&lt;/code&gt; over manually creating &lt;code&gt;appuser&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
I’ve updated the Docker Security section to reflect this recommended approach while keeping the broader &lt;code&gt;appuser&lt;/code&gt; pattern for non-Microsoft base images.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I love containerization.&lt;/p&gt;

&lt;p&gt;From personal projects running at home to production-grade services, containers have transformed the way I build and ship software. They're lightweight, consistent, and (when used correctly) secure. For &lt;strong&gt;local development&lt;/strong&gt;, I usually prefer to work with full SDKs. But for deployments, I lean heavily on containers, DevContainers, and GitHub Actions.&lt;/p&gt;

&lt;p&gt;This post will walk you through a solid workflow for building and running &lt;strong&gt;.NET apps in Docker using Alpine&lt;/strong&gt;, preparing images with CI, and tuning for &lt;strong&gt;Kubernetes deployments&lt;/strong&gt; with realistic resource limits.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧊 Running .NET in Alpine Containers
&lt;/h2&gt;

&lt;p&gt;Alpine is a super minimal Linux distro that makes for compact Docker images. Microsoft ships Alpine-based variants of .NET like this (at the time of writing this is &lt;code&gt;dotnet&lt;/code&gt; 9):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/dotnet/aspnet:9.0-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To have a minimal container is what I feel containerization is really about: work with the OS that is minimal in scope and just focuses on the app execution.&lt;br&gt;
But there's a gotcha: &lt;strong&gt;cultural and timezone data&lt;/strong&gt; isn’t included by default. To make your app work correctly across locales and timezones, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; icu-libs tzdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;➡️ See &lt;a href="https://andrewlock.net/dotnet-core-docker-and-cultures-solving-culture-issues-porting-a-net-core-app-from-windows-to-linux/" rel="noopener noreferrer"&gt;Andrew Lock’s excellent guide&lt;/a&gt; for deeper insights on this issue.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ Docker Security: Running as Non-Root (And Doing It Right)
&lt;/h2&gt;

&lt;p&gt;One of the most common but overlooked Docker security pitfalls is that containers run as &lt;code&gt;root&lt;/code&gt; &lt;strong&gt;by default&lt;/strong&gt;. If someone breaks out of your app process, they’re root inside the container. And that's bad news.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining the Non-Root User
&lt;/h3&gt;

&lt;p&gt;Start by creating a lightweight user and group in the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; appuser &lt;span class="nt"&gt;-G&lt;/span&gt; appgroup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-S&lt;/code&gt; creates system users/groups (no home directory, no password).&lt;/li&gt;
&lt;li&gt;This keeps the image small and secure.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔎 &lt;strong&gt;Note on &lt;code&gt;$APP_UID&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;$APP_UID&lt;/code&gt; pattern is a &lt;strong&gt;Microsoft documented convention&lt;/strong&gt; introduced in .NET 8+ base images. These images define a numeric non-root user internally and expose its UID via the &lt;code&gt;APP_UID&lt;/code&gt; environment variable. This makes it easy to write:&lt;/p&gt;


&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; $APP_UID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;If you're using a &lt;strong&gt;non-Microsoft base image&lt;/strong&gt; (like Alpine or Debian), this variable won't exist unless you define it yourself:&lt;/p&gt;


&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; APP_UID=10001&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; $APP_UID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;So while the pattern is technically portable, &lt;strong&gt;only Microsoft's .NET base images provide it by default&lt;/strong&gt;. For broader compatibility, the traditional &lt;code&gt;adduser appuser &amp;amp;&amp;amp; USER appuser&lt;/code&gt; pattern is still widely used and understood. Read more about Microsofts recommendation at &lt;a href="https://devblogs.microsoft.com/dotnet/securing-containers-with-rootless/#using-app" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/dotnet/securing-containers-with-rootless/#using-app&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Secure File Ownership: Use &lt;code&gt;COPY --chown&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I used to rely on fixing permissions like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./build/api .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; appuser:appgroup .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this isn’t ideal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adds an extra layer.&lt;/li&gt;
&lt;li&gt;Slower on large file sets.&lt;/li&gt;
&lt;li&gt;Messy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I learned to &lt;strong&gt;assign ownership directly at copy time&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appgroup ./build/api .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Instantly assigns correct ownership.&lt;/li&gt;
&lt;li&gt;Avoids extra &lt;code&gt;RUN chown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Makes your &lt;code&gt;Dockerfile&lt;/code&gt; cleaner and more declarative.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Putting It Together
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/dotnet/aspnet:9.0-alpine&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; appuser &lt;span class="nt"&gt;-G&lt;/span&gt; appgroup

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appgroup ./build/api .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appgroup entrypoint.sh .&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["./entrypoint.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The app runs with the &lt;strong&gt;least privilege&lt;/strong&gt; necessary.&lt;/li&gt;
&lt;li&gt;Files are owned properly the moment they're brought into the image.&lt;/li&gt;
&lt;li&gt;Clean. Predictable. Secure.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ CI Builds Artifacts, Docker Just Packages
&lt;/h2&gt;

&lt;p&gt;One of the best things you can do is &lt;strong&gt;keep your Dockerfile lean&lt;/strong&gt;. Not only to avoid compiling inside Docker: it bloats your image and slows builds, but also because of the what docker is for: containerizing your application. Therefore, your app should be ready to be containerized. That is, how I experience Docker to primarily be: the 'containerizer'. So, to build then, use your CI pipeline to &lt;strong&gt;build and publish the app&lt;/strong&gt;, then use Docker to package the output. This will give you inspectable artifacts of the build that.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔧 GitHub Actions: Build and Upload Artifacts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Publish&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;dotnet publish -o ${{ env.PUBLISH_FOLDER_NAME }} ${{ inputs.publish-args }}&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Build Artifact&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.artifact-name }}&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.project-folder }}/${{ env.PUBLISH_FOLDER_NAME }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your Docker build step, pull the artifacts back down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download artifacts&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;IFS=',' read -ra artifacts &amp;lt;&amp;lt;&amp;lt; "${{ inputs.download-artifact }}"&lt;/span&gt;
    &lt;span class="s"&gt;for artifact in "${artifacts[@]}"; do&lt;/span&gt;
      &lt;span class="s"&gt;mkdir -p "${{ inputs.working-directory }}/build/$artifact"&lt;/span&gt;
      &lt;span class="s"&gt;gh run download --name "$artifact" --dir "${{ inputs.working-directory }}/build/$artifact"&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, build and push the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.DOCKERFILE }}&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.working-directory }}&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.container-tags }}&lt;/span&gt;
    &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
    &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;strong&gt;artifact-first approach&lt;/strong&gt; gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reproducibility&lt;/li&gt;
&lt;li&gt;Cleaner build caching&lt;/li&gt;
&lt;li&gt;Easy debugging (you can inspect the build output separately)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ☸️ Kubernetes + Helm: Resource Limits That Actually Work
&lt;/h2&gt;

&lt;p&gt;Let’s be real: .NET isn’t the smallest kid on the block. You can’t slap a tiny resource limit on it without consequences.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 What Microsoft Recommends for AKS
&lt;/h3&gt;

&lt;p&gt;Microsoft’s official guidance for &lt;strong&gt;AKS&lt;/strong&gt; firmly states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Set pod requests and limits on all pods in your YAML manifests. If the AKS cluster uses resource quotas and you don't define these values, your deployment may be rejected.”&lt;/strong&gt;&lt;br&gt;
— AKS Best Practices (resource requests &amp;amp; limits) &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They further caution:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Pod CPU and memory limits define the maximum amount of CPU and memory a pod can use… avoid setting a pod limit higher than your nodes can support.”&lt;/strong&gt;&lt;br&gt;
— AKS Best Practices (resource guidelines) &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Microsoft also provides a &lt;strong&gt;default starting configuration&lt;/strong&gt; in their examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128Mi&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;250m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn’t a strict minimum. But it is a &lt;strong&gt;realistic baseline&lt;/strong&gt; that balances scheduling, performance, and cost .&lt;/p&gt;




&lt;h3&gt;
  
  
  My .NET-Focused Configuration
&lt;/h3&gt;

&lt;p&gt;Here’s the setup that consistently works for .NET workloads I test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20Mi&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;175Mi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ While &lt;code&gt;.NET&lt;/code&gt; can technically run with ~125 Mi memory, in practice this leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sluggish cold starts&lt;/li&gt;
&lt;li&gt;Failing health probes&lt;/li&gt;
&lt;li&gt;Garbage collector thrash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pushing memory to &lt;strong&gt;175 Mi&lt;/strong&gt; ensures decent startup times and runtime stability.&lt;/p&gt;




&lt;h3&gt;
  
  
  ⚖️ TL;DR Recommendations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always&lt;/strong&gt; define both &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;limits&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; Setting &lt;code&gt;limit &amp;gt; request&lt;/code&gt; improves stability: start around 175 Mi for .NET&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; A reasonable request (~100m) with a higher limit helps performance without causing throttling&lt;/li&gt;
&lt;li&gt;These aren’t arbitrary: they reflect &lt;strong&gt;Microsoft’s AKS baseline examples&lt;/strong&gt; (&lt;a href="https://learn.microsoft.com/en-us/azure/aks/best-practices-app-cluster-reliability?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Deployment and cluster reliability best practices for Azure&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/azure/aks/developer-best-practices-resource-management?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Resource management best practices for Azure Kubernetes Service&lt;/a&gt;, &lt;a href="https://docs.azure.cn/en-us/aks/best-practices-app-cluster-reliability?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Deployment and cluster reliability best practices for Azure&lt;/a&gt;, &lt;a href="https://stackoverflow.com/questions/68561083/what-is-the-best-practice-to-have-request-and-limit-values-to-a-pod-in-k8s?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;What is the best practice to have request and limit values to a pod in&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;Use small sized base images, like Alpine, but patch it with you needs (ie: &lt;code&gt;icu-libs&lt;/code&gt; and &lt;code&gt;tzdata&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Run as a non-root user inside your Docker containers&lt;/li&gt;
&lt;li&gt;Use CI to build the app, and let Docker just package it&lt;/li&gt;
&lt;li&gt;Tune your K8s Helm charts to keep .NETs footprint small, but still responsive under pressure of your required workload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Containers are amazing, but they're even better when treated with care. With these practices, you’ll ship faster, safer, and smarter—whether it's production, staging, or even your home lab.&lt;/p&gt;




&lt;p&gt;Got questions or tweaks to share? Drop them in the comments—I'd love to hear your workflow!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>docker</category>
      <category>kubernetes</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Practical Versioning Considerations for .NET APIs and Packages</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Mon, 02 Jun 2025 11:09:39 +0000</pubDate>
      <link>https://dev.to/eelcolos/practical-versioning-considerations-for-net-apis-and-packages-2cjp</link>
      <guid>https://dev.to/eelcolos/practical-versioning-considerations-for-net-apis-and-packages-2cjp</guid>
      <description>&lt;p&gt;Versioning in .NET development goes beyond simply incrementing numbers. In my maintenance of a business critical service and NuGet package I had to migrate to a higher major version on both the API and package. I noticed subtle compatibility considerations when upgrading/migrating.&lt;br&gt;
In this post, we'll explore those versioning challenges and solutions across these key areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
The Hidden Complexity of Package Versioning - NuGet vs assembly version discrepancies and pre-release strategies&lt;/li&gt;
&lt;li&gt;
API Versioning and Compatibility Challenges - URL versioning approaches and the subtle breaking change of JSON casing&lt;/li&gt;
&lt;li&gt;
Breaking Changes You Might Miss - Default behavior changes, dependency bumps, and configuration modifications&lt;/li&gt;
&lt;li&gt;
Practical Implementation Guidelines - Concrete strategies for package authors and API designers&lt;/li&gt;
&lt;li&gt;
Testing Your Versioning Strategy - Package compatibility and API contract testing approaches&lt;/li&gt;
&lt;li&gt;
Version Communication Strategy - How to effectively communicate changes to internal teams and public consumers&lt;/li&gt;
&lt;li&gt;
Common Pitfalls to Avoid - Key mistakes to watch out for when versioning&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Hidden Complexity of Package Versioning
&lt;/h2&gt;
&lt;h3&gt;
  
  
  NuGet vs Assembly Version Discrepancy
&lt;/h3&gt;

&lt;p&gt;One often overlooked issue is the disconnect between NuGet package versions and assembly versions. When you create a pre-release like &lt;code&gt;4.0.3-beta&lt;/code&gt;, the assembly version remains &lt;code&gt;4.0.3.0&lt;/code&gt;. Developers using decompilers see no indication it's a pre-release, leading to confusion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: Use the build number strategically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Instead of this --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Version&amp;gt;&lt;/span&gt;4.0.3-beta&lt;span class="nt"&gt;&amp;lt;/Version&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Do this --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Version&amp;gt;&lt;/span&gt;4.0.3.1-beta&lt;span class="nt"&gt;&amp;lt;/Version&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures both NuGet and decompilers show version progression correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-Release Versioning Strategies
&lt;/h3&gt;

&lt;p&gt;Building on this approach, you can choose different strategies depending on your workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic Pre-Release&lt;/strong&gt; (for major versions):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6.0.0-beta.1 → 6.0.0-beta.2 → 6.0.0-rc.1 → 6.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rolling Minor Versions&lt;/strong&gt; (for continuous development):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5.35.0 → 5.36.0-beta.1 → 5.36.0-beta.2 → 5.36.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Build Number Strategy&lt;/strong&gt; (when assembly version clarity matters):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4.0.3.1-beta → 4.0.3.2-beta → 4.0.4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: Use &lt;code&gt;beta.1&lt;/code&gt; not &lt;code&gt;beta1&lt;/code&gt; for proper sorting on NuGet.org, and consider build numbers when assembly version visibility is important for your consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Versioning and Compatibility Challenges
&lt;/h2&gt;

&lt;p&gt;When evolving REST APIs, you'll encounter several versioning strategies and subtle breaking changes that can catch you off guard.&lt;/p&gt;

&lt;h3&gt;
  
  
  URL Versioning Strategies
&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;// Path-based versioning&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;"api/v1/products"&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;"api/v2/products"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="c1"&gt;// Header-based versioning&lt;/span&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;ApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="c1"&gt;// Query parameter versioning&lt;/span&gt;
&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Subtle Breaking Change: JSON Casing
&lt;/h3&gt;

&lt;p&gt;Here's a compatibility issue many developers overlook: &lt;strong&gt;property casing changes between API frameworks&lt;/strong&gt;. I encountered this one in migrating my business critical service when changing one of the properties from &lt;code&gt;boolean&lt;/code&gt; to &lt;code&gt;DateTime?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional .NET Controllers&lt;/strong&gt; (default PascalCase):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ProductId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ProductName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Widget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CreatedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-15T10:30:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Minimal APIs&lt;/strong&gt; (default camelCase):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Widget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-15T10:30:00Z"&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 seemingly minor change can break client applications that expect specific casing. Consider this when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrating from Controllers to Minimal APIs&lt;/li&gt;
&lt;li&gt;Moving between different API frameworks&lt;/li&gt;
&lt;li&gt;Updating serialization configurations&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Explicit casing control&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;ConfigureHttpJsonOptions&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;SerializerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropertyNamingPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
        &lt;span class="n"&gt;JsonNamingPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CamelCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Or maintain compatibility&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;ConfigureHttpJsonOptions&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;SerializerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropertyNamingPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// PascalCase&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Breaking Changes You Might Miss
&lt;/h2&gt;

&lt;p&gt;These are the changes that seem harmless but can completely break your consumers' applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Default Behavior Changes
&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;// Version 1.0 - throws exceptions&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;GetUser&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;id&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;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&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="c1"&gt;// Version 2.0 - returns null (breaking!)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;GetUser&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;id&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;userRepository&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;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dependency Version Bumps
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Your library targets .NET 6 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net6.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Upgrading to .NET 8 might break consumers still on .NET 6 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net8.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration Changes
&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;// Version 1.x&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;AddMyService&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;EnableFeature&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="c1"&gt;// Default was false&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Version 2.x - default changed to true&lt;/span&gt;
&lt;span class="c1"&gt;// Consumers expecting false behavior will break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Implementation Guidelines
&lt;/h2&gt;

&lt;p&gt;Success in versioning comes down to clear strategies for both package authors and API designers.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Package Authors
&lt;/h3&gt;

&lt;p&gt;Package maintainers need strategies that balance innovation with stability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version Your Interfaces Explicitly&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IMyService&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="kt"&gt;string&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;string&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// When adding parameters, create a new interface&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IMyServiceV2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IMyService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;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;ProcessAsync&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;input&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use Obsolete Attributes Effectively&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Phase 1: Warning only (default behavior)&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obsolete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Use ProcessV2Async instead. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"This method will be removed in v3.0.0"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&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;input&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;ProcessV2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Phase 2: Force compilation error in next major version&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obsolete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Use ProcessV2Async instead. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"This method has been removed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&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;input&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;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"This method has been removed. Use ProcessV2Async instead."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Planned obsolescence with clear timeline&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obsolete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Use ProcessV2Async instead. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"This method will become a compilation error in v3.0.0 (June 2024)"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&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;input&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;ProcessV2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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;None&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;Obsolete Progression Strategy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v2.0.0: Introduce new method
v2.1.0: Mark old method [Obsolete] with warning
v2.2.0: Update message with removal timeline  
v3.0.0: Set [Obsolete(error: true)] - forces compilation errors 
        but method still exists
v3.0.1: Actually remove the obsolete method entirely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Strategic Migration Approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v2.1.0 - Soft warning&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obsolete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Use ProcessV2Async instead. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"This method will cause compilation errors in v3.0.0"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&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;input&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;ProcessV2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// v3.0.0 - Hard error, but method still exists for emergency fallback&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obsolete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Use ProcessV2Async instead. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"This method has been removed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&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;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Still functional for those who suppress the error temporarily&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;ProcessV2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// v3.0.1 or v3.1.0 - Complete removal&lt;/span&gt;
&lt;span class="c1"&gt;// Method is entirely deleted from codebase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits of this approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;v3.0.0 upgrade path&lt;/strong&gt;: Consumers can upgrade to the major version and see exactly what needs to be fixed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency override&lt;/strong&gt;: Teams can temporarily suppress the error (&lt;code&gt;#pragma warning disable CS0618&lt;/code&gt;) while they migrate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear timeline&lt;/strong&gt;: Everyone knows the method will be completely gone in the next minor release&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safer major version adoption&lt;/strong&gt;: Teams aren't afraid to upgrade to v3.0.0 knowing they can still compile&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Advanced Migration Pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v3.0.0 - Compilation error with escape hatch&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obsolete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Use ProcessV2Async instead. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"Method will be removed in v3.1.0. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"Suppress CS0618 if you need temporary compatibility."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&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;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log usage for monitoring migration progress&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Obsolete method ProcessAsync called. Migrate to ProcessV2Async."&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;ProcessV2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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;None&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 approach gives consumers a much better migration experience and reduces the fear of upgrading major versions!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document Breaking Changes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Breaking Changes in v2.0.0&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; JSON response casing changed from PascalCase to camelCase
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`GetUser()`&lt;/span&gt; now returns null instead of throwing for missing users
&lt;span class="p"&gt;-&lt;/span&gt; Minimum .NET version requirement increased to .NET 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For API Designers
&lt;/h3&gt;

&lt;p&gt;API versioning requires balancing backward compatibility with the need to evolve and improve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan for Backward Compatibility&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Good: Additive changes&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;ProductResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;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;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;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;DateTime&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&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="c1"&gt;// New in v1.1 - doesn't break existing clients&lt;/span&gt;
    &lt;span class="k"&gt;public&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;Description&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use Content Negotiation&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="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromHeader&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;apiVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;apiVersion&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;"2.0"&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="n"&gt;productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProductV2&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;_&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="n"&gt;productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProduct&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;Graceful Deprecation&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="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/v1/products"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obsolete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Use /api/v2/products instead"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetProductsV1&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="n"&gt;Headers&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;"X-API-Deprecated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&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;Headers&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;"X-API-Sunset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"2024-12-31"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;GetProductsV2&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;h2&gt;
  
  
  Testing Your Versioning Strategy
&lt;/h2&gt;

&lt;p&gt;Testing isn't just about functionality—it's about ensuring your versioning strategy actually works in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Compatibility Testing
&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;# Test your package with different framework versions&lt;/span&gt;
dotnet pack
dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--framework&lt;/span&gt; net6.0
dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--framework&lt;/span&gt; net8.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  API Contract Testing
&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;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;ApiShouldMaintainJsonCasingAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/products/1"&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;json&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;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;ReadAsStringAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Ensure backward compatibility&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProductId"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Does&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"productId"&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;h2&gt;
  
  
  Version Communication Strategy
&lt;/h2&gt;

&lt;p&gt;Clear communication is just as important as technical implementation when it comes to versioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Internal Teams
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Changelog&lt;/strong&gt;: Document every change, no matter how small&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration guides&lt;/strong&gt;: Provide step-by-step upgrade instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deprecation timeline&lt;/strong&gt;: Give consumers time to adapt&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Public APIs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API documentation&lt;/strong&gt;: Version-specific endpoint documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDKs&lt;/strong&gt;: Maintain SDK versions aligned with API versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support policy&lt;/strong&gt;: Clear support timelines for each version&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Pitfalls to Avoid
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assuming minor changes aren't breaking&lt;/strong&gt; - JSON casing, default values, and behavior changes can break consumers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not testing with real consumer scenarios&lt;/strong&gt; - Your breaking change detector might miss runtime issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inadequate deprecation periods&lt;/strong&gt; - Give consumers time to migrate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inconsistent versioning across related packages&lt;/strong&gt; - Keep related packages in sync&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Effective versioning requires thinking beyond semantic version numbers. Consider the entire ecosystem: package consumers, API clients, deployment scenarios, and migration paths. The goal isn't just to communicate changes—it's to enable smooth evolution while maintaining trust with your consumers.&lt;/p&gt;

&lt;p&gt;Remember: every change is potentially breaking to someone. The key is understanding its impact and communicating clearly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What versioning challenges have you encountered in your .NET projects? Share your experiences in the comments below!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;Semantic Versioning Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/nuget/concepts/package-versioning" rel="noopener noreferrer"&gt;NuGet Package Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/versions" rel="noopener noreferrer"&gt;ASP.NET Core API Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/customize-properties" rel="noopener noreferrer"&gt;JSON Naming Policies in .NET&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>nuget</category>
      <category>versioning</category>
      <category>api</category>
    </item>
    <item>
      <title>Disabling ApplicationInsights' deprecated unload event listener in Angular</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Tue, 29 Apr 2025 13:20:31 +0000</pubDate>
      <link>https://dev.to/eelcolos/disabling-applicationinsights-deprecated-unload-event-listener-in-angular-16kp</link>
      <guid>https://dev.to/eelcolos/disabling-applicationinsights-deprecated-unload-event-listener-in-angular-16kp</guid>
      <description>&lt;p&gt;When using the default TypeScript implementation of Application Insights in our Angular app, Chrome Lighthouse flags the use of unload event listeners as deprecated (they block the back/forward cache and degrade UX&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;). In this post we'll explore why this matters, why unload is deprecated, what the ApplicationInsights client does by default and how to use it properly&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this matter?
&lt;/h2&gt;

&lt;p&gt;You might ask yourself: why does this matter?&lt;br&gt;
To be frank: it probably doesn't. It does not (yet) affect your (clients) browser experience.&lt;br&gt;
It could matter if you (or your clients) are caring for google lighthouses numbers regarding best practices.&lt;br&gt;
Second, it also shows that you are aware of the deprecation issue and are actively working to a solution.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why “Unload” Is Deprecated
&lt;/h2&gt;

&lt;p&gt;Browsers are moving away from the legacy &lt;code&gt;unload&lt;/code&gt; event because it breaks the back/forward cache (bfcache), an in-memory snapshot that makes “back”/“forward” navigations nearly instant. Any page with an &lt;code&gt;unload&lt;/code&gt; listener is ineligible for bfcache, slowing down users when they navigate away and then return&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt;.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome’s Deprecation Timeline&lt;/strong&gt;
Starting Chrome 115 you can opt out of delivering &lt;code&gt;unload&lt;/code&gt; to handlers; Chrome 117+ will default-disable them on all sites, with full removal in the next few milestones&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;sup id="fnref5"&gt;5&lt;/sup&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse Audit&lt;/strong&gt;
The “Deprecated feature used: Unload event listeners are deprecated and will be removed” warning lives in Lighthouse’s Best Practices category under the &lt;code&gt;no-unload-listeners&lt;/code&gt; audit&lt;sup id="fnref3"&gt;3&lt;/sup&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  ApplicationInsights’ Default Behavior
&lt;/h2&gt;

&lt;p&gt;By default, the ApplicationInsights JavaScript SDK attaches to several page-lifecycle events—including &lt;code&gt;unload&lt;/code&gt;, &lt;code&gt;beforeunload&lt;/code&gt;, &lt;code&gt;pagehide&lt;/code&gt; and &lt;code&gt;visibilitychange&lt;/code&gt;—so it can flush telemetry exactly when the user leaves&lt;sup id="fnref6"&gt;6&lt;/sup&gt;. This is great for reliability but trips Lighthouse’s deprecated-API check:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Deprecated feature used&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Unload event listeners are deprecated and will be removed.&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%2Fodesc6cdi4md1opc80ty.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%2Fodesc6cdi4md1opc80ty.png" alt="Lighthouse warning: Unload event listeners are deprecated" width="800" height="59"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Suppressing Just the Unload Hook
&lt;/h2&gt;

&lt;p&gt;Since &lt;strong&gt;v3.0.3&lt;/strong&gt; the SDK supports an almost-undocumented config field, &lt;code&gt;disablePageUnloadEvents?: string[]&lt;/code&gt;, where you list the event names you &lt;strong&gt;do not&lt;/strong&gt; want hooked&lt;sup id="fnref6"&gt;6&lt;/sup&gt;. To disable only the &lt;code&gt;"unload"&lt;/code&gt; listener while preserving &lt;code&gt;pagehide&lt;/code&gt; and &lt;code&gt;visibilitychange&lt;/code&gt;, initialize like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ApplicationInsights&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@microsoft/applicationinsights-web&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appInsights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;YOUR_CONNECTION_STRING&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// …other settings…&lt;/span&gt;
    &lt;span class="na"&gt;disablePageUnloadEvents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unload&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;appInsights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadAppInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in place, Lighthouse will no longer complain about &lt;code&gt;unload&lt;/code&gt; listeners, and your pages remain eligible for bfcache and near-instant navigations&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying Your Fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rerun Lighthouse&lt;/strong&gt; and confirm the “Unload event listeners” warning has disappeared.
&lt;/li&gt;
&lt;li&gt;In Chrome DevTools, go to &lt;strong&gt;Application → Back/forward cache&lt;/strong&gt;, click &lt;strong&gt;Test back/forward cache&lt;/strong&gt;, and ensure your page can be cached (no unload handlers present)&lt;sup id="fnref4"&gt;4&lt;/sup&gt;.
&lt;/li&gt;
&lt;li&gt;Optionally, inspect &lt;strong&gt;Issues&lt;/strong&gt; in DevTools for any residual warnings.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Balancing Telemetry &amp;amp; Best Practices
&lt;/h2&gt;

&lt;p&gt;If you still need to capture last-minute metrics on page exit, use the bfcache-compatible events:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pagehide&lt;/code&gt;&lt;/strong&gt; fires whenever a page is about to be hidden or cached.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;visibilitychange&lt;/code&gt;&lt;/strong&gt; (when &lt;code&gt;document.hidden === true&lt;/code&gt;) fires reliably across modern browsers and mobile devices&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;sup id="fnref8"&gt;8&lt;/sup&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events let you flush critical data without slipping back into deprecated territory.&lt;/p&gt;

&lt;p&gt;Did this tip help you keep your scores green? Drop a comment below 😊&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;“Avoid unload event listeners,” GTmetrix: &lt;a href="https://gtmetrix.com/avoid-unload-event-listeners.html" rel="noopener noreferrer"&gt;https://gtmetrix.com/avoid-unload-event-listeners.html&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;“Window: unload event,” MDN Web Docs: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;“Issue #10848: Create a Performance audit for avoiding the unload event,” GoogleChrome/lighthouse (GitHub): &lt;a href="https://github.com/GoogleChrome/lighthouse/issues/10848" rel="noopener noreferrer"&gt;https://github.com/GoogleChrome/lighthouse/issues/10848&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;“Deprecating the unload event,” Chrome Web Platform: &lt;a href="https://developer.chrome.com/docs/web-platform/deprecating-unload" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/web-platform/deprecating-unload&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;“Permissions Policy for unload,” Chrome Web Platform: &lt;a href="https://developer.chrome.com/docs/web-platform/deprecating-unload#permissions-policy" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/web-platform/deprecating-unload#permissions-policy&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;“Chrome: Deprecation – Unload event listeners are deprecated and will be removed #2220,” microsoft/ApplicationInsights-JS (GitHub): &lt;a href="https://github.com/microsoft/ApplicationInsights-JS/issues/2220" rel="noopener noreferrer"&gt;https://github.com/microsoft/ApplicationInsights-JS/issues/2220&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;“Back/forward cache,” web.dev: &lt;a href="https://web.dev/articles/bfcache" rel="noopener noreferrer"&gt;https://web.dev/articles/bfcache&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;“Time to unload your unload events,” RUMvision: &lt;a href="https://www.rumvision.com/blog/time-to-unload-your-unload-events/" rel="noopener noreferrer"&gt;https://www.rumvision.com/blog/time-to-unload-your-unload-events/&lt;/a&gt;   ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>angular</category>
      <category>lighthouse</category>
      <category>applicationinsights</category>
    </item>
  </channel>
</rss>
