<?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 hardened my multi-agent AI support copilot</title>
      <dc:creator>Eelco Los</dc:creator>
      <pubDate>Fri, 24 Apr 2026 14:56:25 +0000</pubDate>
      <link>https://dev.to/eelcolos/how-i-hardened-my-multi-agent-ai-support-copilot-15p7</link>
      <guid>https://dev.to/eelcolos/how-i-hardened-my-multi-agent-ai-support-copilot-15p7</guid>
      <description>&lt;p&gt;The first post in this series was about the design. This one is about what happened when the first real tickets hit the wiring, and about the hardening work that followed once those runs exposed the weak spots.&lt;/p&gt;

&lt;p&gt;The good news is that the architecture mostly held up. The orchestrator-worker model was still the right shape. Parallel evidence gathering still made sense. Shared incident context still made sense. Human approval still made sense.&lt;/p&gt;

&lt;p&gt;What broke was the boundary layer: what actually executes a skill, how incomplete configuration should fail, what a spawned sub-agent can really invoke, which tools are scoped to the parent session instead of the child, how incident state should persist, and how much of the system could be tested like normal software.&lt;/p&gt;

&lt;p&gt;The examples here are specific to Claude Code, but the class of problem is broader than one tool. In multi-agent systems, runtime semantics matter more than diagrams.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 1: Skills are documents. Agents are executors.
&lt;/h2&gt;

&lt;p&gt;The first live test was against a real Zendesk ticket from a customer tenant: "The request is blocked" during provisioning admin consent. The Management Agent ran. Sub-agents were dispatched. Evidence came back.&lt;/p&gt;

&lt;p&gt;But something was wrong: no &lt;code&gt;az&lt;/code&gt; commands had actually run. No APIs were called. The "evidence" was the agents describing what the skills &lt;em&gt;would&lt;/em&gt; do if they were run: accurate, thoughtful, and completely useless.&lt;/p&gt;

&lt;p&gt;The root cause took a minute to understand. I had treated the skill files as if they were runnable workers. They weren't.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SKILL.md&lt;/code&gt; files in Claude Code are &lt;strong&gt;execution instructions for an agent that already has the right tools&lt;/strong&gt;. The frontmatter declares &lt;code&gt;allowed-tools: Bash(az *)&lt;/code&gt;, but that only scopes permissions when the skill is directly invoked as a user command. In this repo, I call the task-spawned executors evidence agents. The &lt;code&gt;.claude/agents/*.md&lt;/code&gt; files define which one runs, what tools it gets, and that it should execute the skill's commands rather than just read the skill file. When the Management Agent does &lt;code&gt;TaskCreate -&amp;gt; "run the SCIM skill"&lt;/code&gt;, it spawns one of those evidence agents. That evidence agent needs its own &lt;code&gt;.md&lt;/code&gt; definition that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Declares it has &lt;code&gt;Bash(az *)&lt;/code&gt; in its &lt;code&gt;tools:&lt;/code&gt; frontmatter&lt;/li&gt;
&lt;li&gt;Has a system prompt that says &lt;em&gt;execute the skill's commands&lt;/em&gt;, not just &lt;em&gt;read the skill file&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We had written the instructions, but not the workers. The &lt;code&gt;.claude/agents/&lt;/code&gt; directory was mostly empty. There was nothing for &lt;code&gt;TaskCreate&lt;/code&gt; to dispatch to by name.&lt;/p&gt;

&lt;p&gt;The fix was straightforward once the problem was clear: create &lt;code&gt;.claude/agents/apm.md&lt;/code&gt;, &lt;code&gt;crm.md&lt;/code&gt;, &lt;code&gt;scim.md&lt;/code&gt;, &lt;code&gt;b2c.md&lt;/code&gt;, &lt;code&gt;alm.md&lt;/code&gt;. Each with:&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="nn"&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;APM Evidence Agent&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;Evidence-gathering sub-agent for application telemetry. Reads the APM skill&lt;/span&gt;
  &lt;span class="s"&gt;instructions, executes the query steps, and returns structured claim[] entries.&lt;/span&gt;
  &lt;span class="s"&gt;Dispatched by the Management Agent via TaskCreate; not user-invocable directly.&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet-4.5&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash(az *)&lt;/span&gt;
&lt;span class="na"&gt;maxTurns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="s"&gt;You are the APM Evidence Agent. Given an IncidentContext with resolved_identity,&lt;/span&gt;
&lt;span class="s"&gt;execute the APM skill bootstrap and query steps. Write structured claim[] entries&lt;/span&gt;
&lt;span class="s"&gt;to IncidentContext.evidence[]. Return a compact summary to the Management Agent.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The takeaway&lt;/strong&gt;: &lt;code&gt;SKILL.md&lt;/code&gt; is documentation. An agent definition is what actually runs. Don't confuse the map for the territory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 2: Fail fast on configuration. Silent dry-run is an anti-pattern.
&lt;/h2&gt;

&lt;p&gt;The second live test exposed a different problem. All four evidence agents returned:&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;"agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claim_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;"gap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dry_run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Required config fields empty: key_resources[].name, resource_group, subscription_id"&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;Same from CRM, provisioning, and identity config. The Management Agent continued, the Synthesis Agent synthesised from nothing. We got a hypothesis based purely on domain pattern-matching, with zero real telemetry. And we'd burned a full round of context tokens to get there.&lt;/p&gt;

&lt;p&gt;The worst part was not that evidence was missing. It was that the output still looked plausible. A clean failure would have been better than a confident-looking answer built on gaps.&lt;/p&gt;

&lt;p&gt;The design intent was different. The &lt;code&gt;--dry-run&lt;/code&gt; flag existed for demos and training: an opt-in mode. But in practice, any time config was incomplete, the system silently fell back to dry-run instead of reporting what was missing.&lt;/p&gt;

&lt;p&gt;The fix: a &lt;code&gt;validate-expertise&lt;/code&gt; skill that runs at session start, checks all required fields across the five domain YAML files, and reports what's missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Domain   | Field                     | Status  | Auto-discovery
---------|---------------------------|---------|------------------------------------------------
apm      | key_resources[0].name     | ❌ EMPTY | az monitor app-insights component list -o table
crm      | base_url.prod             | ❌ EMPTY | (manual; check deployment docs)
scim     | scim_enterprise_app_sp_id | ❌ EMPTY | az rest ... /servicePrincipals?$filter=...
identity | identity_tenant_id        | ❌ EMPTY | az ad tenant list
alm      | key_repos[0].owner        | ❌ EMPTY | gh repo list --json owner,name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any required field is empty → print the fill table, log to audit, &lt;strong&gt;stop&lt;/strong&gt;. No evidence agents dispatch.&lt;/p&gt;

&lt;p&gt;We also annotated the YAML files directly:&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="c1"&gt;# apm.yaml&lt;/span&gt;
&lt;span class="na"&gt;key_resources&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# REQUIRED: az monitor app-insights component list -o table&lt;/span&gt;
    &lt;span class="na"&gt;resource_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# REQUIRED: same command&lt;/span&gt;
    &lt;span class="na"&gt;subscription_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# REQUIRED: az account show --query id -o tsv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The meta-lesson: dry-run mode should be opt-in and explicit, not the default fallback when setup is incomplete. If the system silently degrades, you don't know it's wrong until you compare the output to reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 3: Slash commands don't exist in sub-agents
&lt;/h2&gt;

&lt;p&gt;This one was embarrassing.&lt;/p&gt;

&lt;p&gt;The Management Agent's Step 0 contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/validate-expertise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Onboarding flow contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/validate-expertise &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of these were silently not working. The agent treated them as skill tool calls, got "skill not found" errors, and then manually read the YAML files anyway, which produced a plausible output that masked the actual failure.&lt;/p&gt;

&lt;p&gt;Skills in Claude Code are not registered slash commands. They are markdown files. The correct invocation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read .claude/skills/validate-expertise/SKILL.md and execute the validation steps.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every place in agent instructions that used &lt;code&gt;/skill-name&lt;/code&gt; syntax needed updating to explicit file-read instructions. The &lt;code&gt;prime&lt;/code&gt; skill had the right pattern all along: "Load current context: execute the prime skill by reading &lt;code&gt;.claude/skills/prime/SKILL.md&lt;/code&gt;". We just hadn't applied it consistently.&lt;/p&gt;

&lt;p&gt;The fix (pr-13) also introduced an &lt;strong&gt;Onboarding Agent&lt;/strong&gt;, a dedicated agent that runs &lt;code&gt;az&lt;/code&gt;/&lt;code&gt;gh&lt;/code&gt; auto-discovery commands for each domain, presents the discovered values interactively, and writes confirmed non-secret values into the YAML files. After it runs, it re-validates. The support engineer goes from "11 empty required fields" to "ready to triage" in one guided session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 4: Constrain the interface, not just the credentials
&lt;/h2&gt;

&lt;p&gt;This one came in two layers. The CRM skill was the first fully working skill. It uses an MCP server (&lt;code&gt;CRM-*&lt;/code&gt; tools) that wraps an internal customer-data API. The API key behind it had write access across the CRM surface, so I intentionally exposed only the read endpoints through the MCP layer. That kept the agent's direct permissions narrow.&lt;/p&gt;

&lt;p&gt;The deeper boundary showed up when we wired that same capability into a &lt;code&gt;TaskCreate -&amp;gt; "CRM Evidence Agent"&lt;/code&gt; dispatch. The sub-agent started and then hit &lt;code&gt;AADSTS65001&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AADSTS65001: The user or administrator has not consented to use the application
with ID '{ResourceId}'...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP tools (&lt;code&gt;CRM-get_company&lt;/code&gt;, &lt;code&gt;CRM-lookup_user_by_email&lt;/code&gt;, etc.) are session-scoped to the Management Agent's Claude Code session. When a sub-agent is spawned via &lt;code&gt;task&lt;/code&gt;, it starts with a clean tool set; MCP tools don't carry over. The lesson was not just "use read-only endpoints" but also "don't assume the child session has the parent's tools." The sub-agent fell back to &lt;code&gt;az account get-access-token --resource {ResourceId}&lt;/code&gt;, which fails because that service principal hasn't been granted consent for interactive CLI flows.&lt;/p&gt;

&lt;p&gt;The workaround (and current design): &lt;strong&gt;CRM runs as a direct call in the Management Agent session&lt;/strong&gt;, not as a TaskCreate dispatch. The Management Agent calls &lt;code&gt;CRM-lookup_user_by_email&lt;/code&gt; directly before dispatching other evidence agents. Identity resolution happens first, synchronously, in the parent session.&lt;/p&gt;

&lt;p&gt;Worth separating two things here that are easy to conflate. The direct-call pattern is a &lt;em&gt;workaround&lt;/em&gt; for the MCP tool-scope limitation. But it may also be the &lt;em&gt;right shape&lt;/em&gt; regardless. Resolving identity synchronously in the parent session, before dispatching parallel workers, is a reasonable sequencing decision on its own terms: it keeps identity resolution out of the sub-agents, surfaces auth failures early, and means workers can assume a resolved identity rather than having to negotiate it. When MCP tool forwarding lands, the question isn't automatically "switch back to the dispatch model." It's "is synchronous identity resolution still the better shape?" The workaround and the correct architecture might happen to be the same thing.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;decisions.md&lt;/code&gt; entry captures the distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Current state: MCP tool forwarding in &lt;code&gt;task&lt;/code&gt;: Not supported.&lt;br&gt;
When to re-evaluate: New Claude Code version / MCP spec update.&lt;br&gt;
When available: CRM can be dispatched as &lt;code&gt;TaskCreate → "CRM Evidence Agent"&lt;/code&gt; with &lt;code&gt;CRM-*&lt;/code&gt; tools. Remove direct-call pattern from management-agent.md.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Document the current workaround, document the future path, document the trigger for re-evaluation. That's the pattern for any runtime constraint you're working around rather than solving.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 5: local-dev auth and production auth are different systems
&lt;/h2&gt;

&lt;p&gt;One of the first hardening lessons was that I had been mentally treating authentication as one thing. It wasn't.&lt;/p&gt;

&lt;p&gt;In the research phase, the working assumption was simple: if &lt;code&gt;az login&lt;/code&gt; works in the parent session, the sub-agents should be able to use the same credentials. Real runs made that assumption look shaky. We saw cases where the APM evidence path returned a credential gap even though the parent session was already authenticated.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; automatically mean the agent logic was wrong. It means the runtime boundary was more important than the prompt. A child session is not the same thing as the parent. A local CLI flow is not the same thing as a hosted workload identity. "Authenticated on my machine" is not a production auth strategy.&lt;/p&gt;

&lt;p&gt;For local development, we found one concrete pattern that was reliable: user-level environment variables were visible to new PowerShell sessions, while process-level &lt;code&gt;$env:&lt;/code&gt; values were not.&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="c"&gt;# Set once for local dev:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;SetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AZURE_DEVOPS_EXT_PAT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$yourToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'User'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Read inside a spawned session:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AZURE_DEVOPS_EXT_PAT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'User'&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;That solved a local dev problem. It did not solve the production problem.&lt;/p&gt;

&lt;p&gt;The broader lesson was that auth should be treated as part of the architecture, not as bootstrap glue. If the system depends on interactive CLI auth in production, it is not productionized yet.&lt;/p&gt;

&lt;p&gt;The production direction is a workload identity model: managed identity on the host, workload identity federation for cross-tenant scenarios, service principals with explicit RBAC rather than user delegation. How that maps to this system's deployment shape is Part 3 territory. The design implication was already clear in Part 2: auth is an architectural decision, not something you patch in at the prompt layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 6: IncidentContext needed to be durable and layered
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;IncidentContext&lt;/code&gt; pattern held up extremely well. It was still the right abstraction: every evidence agent writes into shared incident state, the synthesis layer reads from it, and agents do not talk directly to one another.&lt;/p&gt;

&lt;p&gt;But the storage story behind that abstraction had to evolve.&lt;/p&gt;

&lt;p&gt;At first, &lt;code&gt;IncidentContext&lt;/code&gt; was mostly conceptual. It lived in prompts, in in-memory state, and in repeated task payloads. That was enough to prove the architecture, but it broke down as soon as the system had to resume work, survive interruptions, or preserve a real audit trail.&lt;/p&gt;

&lt;p&gt;The first hardening step was to make it a real file. That solved two immediate problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token overhead&lt;/strong&gt;: we stopped re-injecting the full incident state into every sub-agent prompt&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume&lt;/strong&gt;: the incident could survive beyond one chat turn or one session&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But even at that stage, the more general lesson was clear: not all state belongs in the model context, and not all state should stay implicit.&lt;/p&gt;

&lt;p&gt;Once incidents become long-lived objects rather than prompt-shaped blobs, you can start talking seriously about revisions, late evidence, replay, and audit continuity.&lt;/p&gt;

&lt;p&gt;That's what "layered" means here. An incident has distinct phases: intake state, live evidence under collection, synthesized hypothesis, reviewer annotations, and final audit record. Those phases have different lifecycles and different consumers. Flattening them into a single blob works fine when an incident is simple and completes in one run. It breaks when evidence arrives late, when the system is interrupted mid-investigation, or when you need to replay synthesis without re-running the full evidence pass. The file-backed &lt;code&gt;IncidentContext&lt;/code&gt; was the first step toward treating those phases as first-class state, not just named sections of a big prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 7: you can test more of this than you think
&lt;/h2&gt;

&lt;p&gt;One of the most useful hardening lessons in the repo was how much of a multi-agent system is actually testable with normal engineering techniques.&lt;/p&gt;

&lt;p&gt;At first, it is tempting to think of an agent system as mostly prompt behavior, and therefore mostly manual validation. That does not scale for long.&lt;/p&gt;

&lt;p&gt;The breakthrough was to stop thinking about the whole thing as one fuzzy AI system and start testing its surfaces separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Static validation tests
&lt;/h3&gt;

&lt;p&gt;These do not touch an LLM at all. They just assert that the repo is internally coherent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expertise YAML files contain the required fields&lt;/li&gt;
&lt;li&gt;agent and skill markdown files have valid frontmatter&lt;/li&gt;
&lt;li&gt;incident context JSON files match schema&lt;/li&gt;
&lt;li&gt;claim objects have the required fields and valid confidence ranges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That catches a surprising amount of breakage before you ever run a live incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Deterministic logic tests
&lt;/h3&gt;

&lt;p&gt;Some of the system is not AI behavior. It is just logic.&lt;/p&gt;

&lt;p&gt;We wrote tests for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synthesis confidence scoring logic&lt;/li&gt;
&lt;li&gt;policy gate thresholds&lt;/li&gt;
&lt;li&gt;intake regexes such as &lt;code&gt;regression_from_version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;recalibration boundaries like &lt;code&gt;[0.45, 0.65)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are product decisions encoded in code. They deserve ordinary tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Contract tests for agents
&lt;/h3&gt;

&lt;p&gt;Because so much of the runtime lives in markdown, agent files and skill files need to be treated as contracts, not prose.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every documented agent must have a corresponding &lt;code&gt;.md&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;every evidence agent must allow the claim types it actually emits&lt;/li&gt;
&lt;li&gt;dry-run gap claims must have &lt;code&gt;confidence = 0.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;routing rules stay consistent with the management agent documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If markdown is part of the runtime, markdown deserves tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Golden cases from real incidents
&lt;/h3&gt;

&lt;p&gt;Past incidents became fixtures. We asserted things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at least four evidence agents contributed&lt;/li&gt;
&lt;li&gt;the top hypothesis stays within a target confidence range&lt;/li&gt;
&lt;li&gt;no PII leaks into hypothesis text&lt;/li&gt;
&lt;li&gt;audit steps appear in the right order&lt;/li&gt;
&lt;li&gt;known incidents still produce the same class of answer after refactors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turns solved incidents into reusable engineering assets.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. LLM evals where model behavior really matters
&lt;/h3&gt;

&lt;p&gt;There is still a model-shaped layer, especially around synthesis quality, reviewer behavior, and policy gate quality. That is where evals belong.&lt;/p&gt;

&lt;p&gt;The repo now includes evals for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synthesis quality&lt;/li&gt;
&lt;li&gt;reviewer PII and promise detection&lt;/li&gt;
&lt;li&gt;policy gate correctness&lt;/li&gt;
&lt;li&gt;incident epistemic safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the tests are not just local rituals anymore. They are wired into CI, so pushes and pull requests get automated results instead of "I tried a few prompts and it seemed okay."&lt;/p&gt;

&lt;p&gt;The broader lesson is simple: multi-agent systems are still software. Test the deterministic parts deterministically. Test contracts as contracts. Use golden cases for regressions. Save LLM evals for the surfaces that actually require model judgment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What these failures had in common
&lt;/h2&gt;

&lt;p&gt;None of these were "the model reasoned badly" problems. They were places where I had relied on an implicit contract that wasn't actually real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I treated instructions as executors&lt;/li&gt;
&lt;li&gt;I treated missing config as something the system could gracefully work around&lt;/li&gt;
&lt;li&gt;I treated slash-command syntax as if it were a portable invocation layer&lt;/li&gt;
&lt;li&gt;I treated parent-session tools as if they would be inherited by child sessions&lt;/li&gt;
&lt;li&gt;I treated local auth as if it would behave the same across runtime contexts&lt;/li&gt;
&lt;li&gt;I treated prompt memory as if it were durable system state&lt;/li&gt;
&lt;li&gt;I treated too much of the system as if it had to be tested manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix in each case was the same: make the boundary explicit, codify the workaround, and stop relying on implied behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  The debugging order that actually worked
&lt;/h2&gt;

&lt;p&gt;Once I noticed the pattern, I stopped starting with the prompt and started with the execution chain. This ordered checklist is the most transferable thing in this post, and it applies to any multi-agent system, not just Claude Code.&lt;/p&gt;

&lt;p&gt;In practice, I kept asking the same questions in the same order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Did the right worker actually run?&lt;/strong&gt; If the wrong agent was dispatched, or no agent existed for that role, nothing downstream mattered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did it have the tools I thought it had?&lt;/strong&gt; A skill that mentions &lt;code&gt;az&lt;/code&gt; is not the same thing as an agent that can execute &lt;code&gt;az&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was configuration complete?&lt;/strong&gt; If required fields were empty, the system needed to stop, not improvise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was I using a real invocation mechanism?&lt;/strong&gt; Slash-command looking syntax felt convenient, but convenience is not the same thing as runtime support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was the tool available in this session or only in the parent?&lt;/strong&gt; MCP scope turned out to matter more than I expected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only then: was the model reasoning actually wrong?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That order saved a lot of time. If step 1 is false, better prompting will not help. If step 3 is false, the best model in the world can only produce a sophisticated answer around missing inputs. If step 5 is false, you do not have an agent-quality problem. You have a system-boundary problem.&lt;/p&gt;

&lt;p&gt;That was one of the most useful mindset shifts of the whole project. Multi-agent debugging often looks like prompt debugging at first, but a lot of it is closer to distributed systems debugging: execution path, capability scope, state, and contracts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed in the repo because of these runs
&lt;/h2&gt;

&lt;p&gt;The first live incidents changed the codebase in very concrete ways.&lt;/p&gt;

&lt;p&gt;We added explicit evidence-agent definitions instead of assuming skills were executable on their own. We added &lt;code&gt;validate-expertise&lt;/code&gt; so incomplete setup blocked the run instead of quietly degrading into dry-run mode. We introduced an Onboarding Agent so filling the YAML files became a guided flow instead of a scavenger hunt. We rewrote skill invocations in agent instructions to explicit file-read-and-execute patterns. And we documented the CRM direct-call workaround in &lt;code&gt;decisions.md&lt;/code&gt; with a re-evaluation trigger instead of pretending the sub-agent path already worked.&lt;/p&gt;

&lt;p&gt;That sounds mundane, but that's the point. The system got better not because we discovered a magic prompt, but because we tightened the contracts around how it actually runs.&lt;/p&gt;

&lt;p&gt;That was the point where the support copilot stopped being a design exercise and started feeling like software.&lt;/p&gt;

&lt;p&gt;One thread from Part 1 that doesn't resurface here: the confidence formula (&lt;code&gt;FinalConf = sigmoid(Support - Conflict) × AgreementMultiplier&lt;/code&gt;) and the self-improvement loop through expertise YAML updates. The formula survived and is tested (Lesson 7). &lt;/p&gt;

&lt;p&gt;The next post is about productionization: Teams timeouts, async replies, attachments, blob storage, containerization, Azure deployment, and the IT-admin realities that only show up once you leave the notebook.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>debugging</category>
      <category>testing</category>
    </item>
    <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>
  </channel>
</rss>
