<?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: Sukhpinder Singh</title>
    <description>The latest articles on DEV Community by Sukhpinder Singh (@ssukhpinder).</description>
    <link>https://dev.to/ssukhpinder</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F628027%2Fcfd80bd1-85eb-45d1-95c2-6fa4c5931782.png</url>
      <title>DEV Community: Sukhpinder Singh</title>
      <link>https://dev.to/ssukhpinder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ssukhpinder"/>
    <language>en</language>
    <item>
      <title>Claude API Workspace Verification: Catch Misrouted Requests Before Attribution</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Fri, 28 Aug 2026 15:35:07 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/claude-api-workspace-verification-catch-misrouted-requests-before-attribution-3h51</link>
      <guid>https://dev.to/ssukhpinder/claude-api-workspace-verification-catch-misrouted-requests-before-attribution-3h51</guid>
      <description>&lt;p&gt;Claude API workspace verification is a small check that closes an awkward observability gap. A multi-workspace credential can send a request toward one workspace, while a stale deployment setting, copied ID, or routing mistake points somewhere else. If I record only the configured workspace, every later cost and resource lookup begins with an assumption.&lt;/p&gt;

&lt;p&gt;Anthropic now returns &lt;code&gt;anthropic-workspace-id&lt;/code&gt; on workspace-resolved Claude API responses. I first compare routing configuration with an independently owned authorization mapping, then compare that authoritative response value with the authorized workspace before the application parses, persists, or attributes the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Claude API workspace verification belongs on the response
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://platform.claude.com/docs/en/manage-claude/workspaces" rel="noopener noreferrer"&gt;Anthropic workspace documentation&lt;/a&gt; separates two ideas that are easy to blur together. A request can carry &lt;code&gt;anthropic-workspace-id&lt;/code&gt; when a multi-workspace key selects its target. A successful response carries the workspace that the credential actually resolved to.&lt;/p&gt;

&lt;p&gt;That second value is useful evidence. Configuration tells me what the application meant to do. The response tells me where the provider handled the request.&lt;/p&gt;

&lt;p&gt;This matters beyond cost dashboards. Files, message batches, Skills, prompt caches, and other resources can be workspace-scoped. If I save a resource ID under the wrong internal tenant or environment, the failure often appears later as a missing resource, an unexpected quota, or usage that seems to vanish.&lt;/p&gt;

&lt;p&gt;The fix is not to log more configuration. I use two independently governed inputs: a routing target and an authorized tenant-to-workspace mapping. I assert both invariants at the HTTP boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;routing target == authorized workspace == resolved response workspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both expected values are aliases for one unchecked setting, the first comparison adds no protection. In my sample, the authorization mapping is a distinct parameter so that a stale deployment target fails before any request leaves the process.&lt;/p&gt;

&lt;p&gt;I also keep the provider &lt;code&gt;request-id&lt;/code&gt; beside the verified workspace. That pair is much more useful during support and attribution work than the configured value alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare intent with the resolved workspace
&lt;/h2&gt;

&lt;p&gt;The check should run after the HTTP status succeeds but before the body reaches application code. Anthropic documents the response headers in its &lt;a href="https://platform.claude.com/docs/en/api/overview" rel="noopener noreferrer"&gt;API overview&lt;/a&gt;, and official SDKs expose raw-response accessors for reading them.&lt;/p&gt;

&lt;p&gt;Here is the core of the .NET sample:&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"anthropic-workspace-id"&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;values&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;InvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Successful response omitted anthropic-workspace-id."&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;actual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;values&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="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;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;$"Resolved &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; expected &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsStringAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I validate the routing, authorized, and returned values as &lt;code&gt;wrkspc_&lt;/code&gt; followed by an alphanumeric identifier. I reject a routing-versus-authorization mismatch before sending. After success, I require exactly one response value and compare with ordinal equality. A provider mismatch fails before a caller can parse or store the payload.&lt;/p&gt;

&lt;p&gt;For a multi-workspace key, the routing target is sent on the request while the authorized value comes from a separately reviewed tenant registry or environment allowlist. This sample is intentionally limited to that path. A separate wrapper for a key already bound to one workspace could omit the outbound selector and still verify the response against an independently authorized workspace.&lt;/p&gt;

&lt;p&gt;Do not replace the original HTTP failure with a workspace error. Anthropic says the header can be absent when authentication does not complete, such as a 401 response. Calling &lt;code&gt;EnsureSuccessStatusCode&lt;/code&gt; first preserves the useful authentication failure; header verification applies to the successful workspace-scoped response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the workspace check deterministic
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/084-claude-workspace-verification" rel="noopener noreferrer"&gt;complete sample&lt;/a&gt; uses a fake &lt;code&gt;HttpMessageHandler&lt;/code&gt;, so it never calls Anthropic and needs no credential. Its fixtures run seven checks across these paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;matching routing, authorization, and response values pass;&lt;/li&gt;
&lt;li&gt;stale routing is rejected before the transport runs;&lt;/li&gt;
&lt;li&gt;a different response workspace fails before body use;&lt;/li&gt;
&lt;li&gt;a missing or malformed workspace on success fails closed;&lt;/li&gt;
&lt;li&gt;invalid configuration makes no HTTP request; and&lt;/li&gt;
&lt;li&gt;a 401 remains an authentication error even without the header.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the kind of contract test I want in CI. It is fast, has no model output to compare, and makes a deployment invariant executable. I can add one fixture for each configured environment without creating API keys or spending tokens.&lt;/p&gt;

&lt;p&gt;In a live service, the authorization value should come from a reviewed mapping that is independent of the component choosing the routing header. The API key still belongs in a secret store. The workspace ID is an identifier rather than an authentication secret, but I avoid scattering it through business logic because that makes routing changes harder to audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and when not to enforce it
&lt;/h2&gt;

&lt;p&gt;This guard is not authorization. A matching header does not prove that an end user may access a resource, and it does not replace provider authentication or application tenant checks. It only proves that this successful response resolved to the workspace the application expected.&lt;/p&gt;

&lt;p&gt;Do not require the header on Admin API calls or failures that occur before authentication. Applications that intentionally route across several workspaces also need an explicit, independently governed per-request mapping rather than one process-wide expected value. Copying the routing setting into the authorization check would only verify provider resolution, not business intent.&lt;/p&gt;

&lt;p&gt;The Default Workspace deserves one final caution. It has a real &lt;code&gt;wrkspc_&lt;/code&gt; ID in response headers, even though List Workspaces omits it and some usage or Admin API fields represent it as &lt;code&gt;null&lt;/code&gt;. Store the returned ID for request tracing; do not translate it to &lt;code&gt;null&lt;/code&gt; inside this verification step.&lt;/p&gt;

&lt;p&gt;Would you fail a mismatched workspace immediately, or quarantine the response for investigation?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>claude</category>
      <category>api</category>
      <category>dotnet</category>
      <category>testing</category>
    </item>
    <item>
      <title>Claude Structured Outputs Refusal Handling: Stop Parsing HTTP 200 Refusals</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:04:28 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/claude-structured-outputs-refusal-handling-stop-parsing-http-200-refusals-42bl</link>
      <guid>https://dev.to/ssukhpinder/claude-structured-outputs-refusal-handling-stop-parsing-http-200-refusals-42bl</guid>
      <description>&lt;p&gt;Claude structured outputs refusal handling belongs before domain deserialization. A successful HTTP exchange only says the API accepted and processed the request; it does not guarantee that the text block contains the JSON object my application expects. Claude can return an HTTP 200 response with &lt;code&gt;stop_reason: "refusal"&lt;/code&gt;, and a response stopped by &lt;code&gt;max_tokens&lt;/code&gt; can contain incomplete JSON. If I unwrap &lt;code&gt;content[0].text&lt;/code&gt; and immediately call &lt;code&gt;JsonSerializer.Deserialize&lt;/code&gt;, I turn a documented response state into a misleading parsing failure.&lt;/p&gt;

&lt;p&gt;The safer boundary is small: inspect the response envelope, classify the stop reason, and deserialize only a completed structured result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema is only one part of the contract
&lt;/h2&gt;

&lt;p&gt;For the current stable API, I put the JSON Schema under &lt;code&gt;output_config.format&lt;/code&gt;. This replaces the earlier beta &lt;code&gt;output_format&lt;/code&gt; request shape, and the beta header is no longer required. The &lt;a href="https://platform.claude.com/docs/en/build-with-claude/structured-outputs" rel="noopener noreferrer"&gt;official structured outputs guide&lt;/a&gt; documents the current request format and its exceptional cases.&lt;/p&gt;

&lt;p&gt;The relevant part of a request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output_config"&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;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json_schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"approve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalate"&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;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"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;"reason"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="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;Structured outputs normally give me schema-compliant JSON, but I still treat the envelope as authoritative. A refusal is a valid API response and may not follow my output schema. A &lt;code&gt;max_tokens&lt;/code&gt; stop can cut the generated document short. The &lt;a href="https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons" rel="noopener noreferrer"&gt;stop-reason guidance&lt;/a&gt; explains why each reason needs deliberate handling instead of a blanket success path.&lt;/p&gt;

&lt;p&gt;There is one more subtlety: enum and &lt;code&gt;const&lt;/code&gt; text can differ in letter casing. I do not weaken validation into “accept any string.” I normalize casing only while matching against the finite enum declared by my application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Claude structured outputs refusal handling a decoder boundary
&lt;/h2&gt;

&lt;p&gt;I model decoding as a result, not an exception-driven happy path. The decoder first parses the outer message envelope, reads &lt;code&gt;stop_reason&lt;/code&gt;, and rejects refusal or truncation. Only then does it pass the text block to &lt;code&gt;JsonSerializer.Deserialize&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json.Serialization&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;enum&lt;/span&gt; &lt;span class="n"&gt;DecodeStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Refusal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Truncated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InvalidPayload&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;enum&lt;/span&gt; &lt;span class="n"&gt;ReviewAction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Approve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Escalate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ReviewWire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;JsonPropertyName&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reason"&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;Reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ReviewResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReviewAction&lt;/span&gt; &lt;span class="n"&gt;Action&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;Reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;DecodeResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DecodeStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ReviewResult&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;DecodeResult&lt;/span&gt; &lt;span class="nf"&gt;Decode&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;messageJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&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;messageJson&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;message&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stopReason&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;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stop_reason"&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="c1"&gt;// Gate on the response envelope before touching structured text.&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;stopReason&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"refusal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DecodeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Refusal&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stopReason&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DecodeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Truncated&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stopReason&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"end_turn"&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;TryGetTextBlock&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="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DecodeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidPayload&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wire&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReviewWire&lt;/span&gt;&lt;span class="p"&gt;&amp;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;JsonSerializerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Strict&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;wire&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;TryMapAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wire&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="k"&gt;out&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DecodeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidPayload&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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DecodeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wire&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&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;JsonException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DecodeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidPayload&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryMapAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;ReviewAction&lt;/span&gt; &lt;span class="n"&gt;action&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="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"approve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&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;ReviewAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Approve&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;true&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="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"escalate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&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;ReviewAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Escalate&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;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;action&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="k"&gt;false&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 order is the key behavior. A refusal containing prose never reaches the inner deserializer. Truncated JSON is classified as truncation, not reported as a random syntax defect. An enum value such as &lt;code&gt;APPROVE&lt;/code&gt; maps to the declared &lt;code&gt;Approve&lt;/code&gt; member, while an undeclared value still fails.&lt;/p&gt;

&lt;p&gt;In the complete sample, &lt;code&gt;TryGetTextBlock&lt;/code&gt; scans the content array instead of assuming the first block is text. The outer parser also turns a missing &lt;code&gt;stop_reason&lt;/code&gt;, missing text block, or malformed message body into an envelope failure. That separation keeps transport shape, generation outcome, and business data as three observable contracts rather than one catch-all JSON exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove the edge cases offline
&lt;/h2&gt;

&lt;p&gt;I keep API transport out of decoder tests. Small response fixtures make the contract deterministic and avoid paid model calls. At minimum, I test four messages: a valid &lt;code&gt;end_turn&lt;/code&gt; response, an HTTP 200 refusal with non-schema text, a &lt;code&gt;max_tokens&lt;/code&gt; response with truncated JSON, and a completed response whose enum casing differs.&lt;/p&gt;

&lt;p&gt;Each test asserts the classification as well as the absence or presence of a domain value. That prevents a later refactor from moving deserialization above the stop-reason gate. It also avoids brittle assertions about generated wording.&lt;/p&gt;

&lt;p&gt;I deliberately put invalid inner text in the refusal and truncation fixtures. If a future change parses either payload too early, the verifier fails for the wrong classification immediately. A separate &lt;code&gt;end_turn&lt;/code&gt; fixture contains malformed JSON and must produce &lt;code&gt;InvalidPayload&lt;/code&gt;; this proves the inner parser still reports a real contract defect when the envelope says generation completed.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/061-claude-structured-refusals" rel="noopener noreferrer"&gt;runnable sample&lt;/a&gt; includes those fixtures, source, and test commands. The associated &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/51" rel="noopener noreferrer"&gt;pull request&lt;/a&gt; shows the complete change and validation record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and when not to use this pattern
&lt;/h2&gt;

&lt;p&gt;This decoder is intentionally strict: it accepts &lt;code&gt;end_turn&lt;/code&gt; for a request that expects one text result. If I intentionally use stop sequences, tool calls, or streaming, I need a state machine and an allowlist designed for those response paths. I would not silently treat every unfamiliar stop reason as success.&lt;/p&gt;

&lt;p&gt;I also would not automatically retry a refusal. A refusal is not a transport outage, and retrying the same request can waste capacity without changing the outcome. For &lt;code&gt;max_tokens&lt;/code&gt;, a caller can choose to reduce the requested structure or adjust its token budget, but that policy belongs above the decoder.&lt;/p&gt;

&lt;p&gt;Finally, case-insensitive enum matching is appropriate only when casing is not meaningful in the domain. The declared values remain the boundary; normalization should never turn arbitrary model text into an accepted business decision.&lt;/p&gt;

&lt;p&gt;Where does your integration currently check &lt;code&gt;stop_reason&lt;/code&gt;: before deserialization, or only after parsing fails?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>dotnet</category>
      <category>testing</category>
    </item>
    <item>
      <title>.NET 10 ActivitySamplingResult PropagationData: Why Recorded Turns False</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Thu, 27 Aug 2026 16:21:09 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-activitysamplingresult-propagationdata-why-recorded-turns-false-d1g</link>
      <guid>https://dev.to/ssukhpinder/net-10-activitysamplingresult-propagationdata-why-recorded-turns-false-d1g</guid>
      <description>&lt;p&gt;.NET 10 changed a small tracing rule that can quietly invalidate a custom sampler. With &lt;strong&gt;.NET 10 ActivitySamplingResult PropagationData&lt;/strong&gt;, a child activity no longer becomes &lt;code&gt;Recorded&lt;/code&gt; just because its parent carries the recorded flag. The trace identity still flows, but the local sampling decision now wins.&lt;/p&gt;

&lt;p&gt;I treat that decision as a contract worth testing. A collector is not required to reproduce it, and an exporter can actually hide the important part behind more configuration. A fixed &lt;code&gt;ActivityContext&lt;/code&gt;, one &lt;code&gt;ActivityListener&lt;/code&gt;, and a few assertions are enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why .NET 10 ActivitySamplingResult PropagationData changed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ActivitySource.StartActivity&lt;/code&gt; only creates an activity when a registered listener asks for one. The listener's sampling result also says how much data the activity should collect.&lt;/p&gt;

&lt;p&gt;The relevant choices are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;None&lt;/code&gt;: do not create the activity.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PropagationData&lt;/code&gt;: create it with propagation state, but do not request enrichment or recording.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AllData&lt;/code&gt;: request tags, links, and events without setting &lt;code&gt;Recorded&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AllDataAndRecorded&lt;/code&gt;: request enrichment and set the recorded flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before .NET 10, &lt;code&gt;PropagationData&lt;/code&gt; had an exception to that table. If the parent was recorded, the child also became recorded. Microsoft changed this because the inherited flag did not match the sampling result or the OpenTelemetry contract. The &lt;a href="https://learn.microsoft.com/dotnet/core/compatibility/core-libraries/10.0/activity-sampling" rel="noopener noreferrer"&gt;.NET 10 compatibility note&lt;/a&gt; now states that a &lt;code&gt;PropagationData&lt;/code&gt; child has both &lt;code&gt;Recorded == false&lt;/code&gt; and &lt;code&gt;IsAllDataRequested == false&lt;/code&gt;, even under a recorded parent.&lt;/p&gt;

&lt;p&gt;That distinction matters in custom listeners. &lt;code&gt;Recorded&lt;/code&gt; controls the W3C recorded bit propagated downstream. &lt;code&gt;IsAllDataRequested&lt;/code&gt; tells instrumentation whether detailed data should be attached. They answer different questions, so forcing one does not automatically enable the other. The &lt;a href="https://learn.microsoft.com/dotnet/api/system.diagnostics.activitysamplingresult?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;ActivitySamplingResult&lt;/code&gt; API documentation&lt;/a&gt; is a useful compact reference for those four choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the Recorded flag in one process
&lt;/h2&gt;

&lt;p&gt;I start with a listener whose decision is explicit and a remote parent whose IDs are fixed test data:&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;decision&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ActivitySamplingResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropagationData&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;source&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;ActivitySource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ActivityPropagationSampling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;listener&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;ActivityListener&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ShouldListenTo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Sample&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;ActivityCreationOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActivityContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;ActivitySource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddActivityListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listener&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;parent&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;ActivityContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ActivityTraceId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"11111111111111111111111111111111"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;ActivitySpanId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2222222222222222"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;ActivityTraceFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recorded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;traceState&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;isRemote&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;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"receive-message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ActivityKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Consumer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important assertions are about the contract, not generated identifiers or wall-clock timing:&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;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TraceId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TraceId&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="nf"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParentSpanId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpanId&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="nf"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recorded&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAllDataRequested&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child exists and continues the trace, which is exactly what &lt;code&gt;PropagationData&lt;/code&gt; requests. It simply does not claim that this process chose to record or enrich it.&lt;/p&gt;

&lt;p&gt;The complete &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/060-activity-propagation-sampling" rel="noopener noreferrer"&gt;sample on &lt;code&gt;main&lt;/code&gt;&lt;/a&gt; turns these checks into a deterministic console verifier. It runs without credentials, network calls, model calls, an OpenTelemetry package, or a collector. The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/50" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; also records the validation commands and results.&lt;/p&gt;

&lt;p&gt;Run it with:&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;restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--verify-no-changes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier ends with &lt;code&gt;PASS: 10/10 checks&lt;/code&gt;. Five repeated runs produced byte-identical output in the sample validation, but I am not presenting that as a performance benchmark. It only proves that the fixture itself is stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the sampling result deliberately
&lt;/h2&gt;

&lt;p&gt;If I only need trace identity and baggage to cross a boundary, &lt;code&gt;PropagationData&lt;/code&gt; is still the right answer. Changing it to &lt;code&gt;AllDataAndRecorded&lt;/code&gt; merely to preserve pre-.NET 10 behavior can increase the amount of telemetry collected.&lt;/p&gt;

&lt;p&gt;If the listener truly intends to record and enrich the activity, I return &lt;code&gt;AllDataAndRecorded&lt;/code&gt; and test both flags. If I need the old recorded bit temporarily, Microsoft's documented compatibility measure is explicit:&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;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActivityTraceFlags&lt;/span&gt; &lt;span class="p"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;ActivityTraceFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recorded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line makes &lt;code&gt;child.Recorded&lt;/code&gt; true and propagates the bit downstream. It does &lt;strong&gt;not&lt;/strong&gt; make &lt;code&gt;IsAllDataRequested&lt;/code&gt; true. Code that sets tags or events based on that property will still skip enrichment, so this is a narrow bridge rather than a replacement sampling policy.&lt;/p&gt;

&lt;p&gt;I also keep the framework baseline visible. The sample was verified on the stable .NET 10.0.11 runtime included with SDK 10.0.303; Microsoft's &lt;a href="https://github.com/dotnet/core/blob/main/release-notes/10.0/10.0.11/10.0.11.md" rel="noopener noreferrer"&gt;10.0.11 release notes&lt;/a&gt; list that supported SDK/runtime pairing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and when not to change anything
&lt;/h2&gt;

&lt;p&gt;This behavior targets code that directly implements &lt;code&gt;ActivityListener.Sample&lt;/code&gt; and returns &lt;code&gt;PropagationData&lt;/code&gt;. Microsoft notes that the default OpenTelemetry .NET parent-based sampler is not affected. If that is your setup, do not add a flag override for a problem you do not have.&lt;/p&gt;

&lt;p&gt;The sample also stops at the in-process contract. It does not prove exporter batching, collector sampling, backend retention, or billing behavior. Those belong in separate integration checks because they depend on the telemetry stack you deploy.&lt;/p&gt;

&lt;p&gt;For custom samplers, though, this small test catches the exact upgrade boundary without external infrastructure. How are you regression-testing sampling decisions in your tracing code?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>.NET 10 BufferedStream WriteByte: Flush Full Buffers Explicitly</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Tue, 25 Aug 2026 23:26:11 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-bufferedstream-writebyte-flush-full-buffers-explicitly-45a2</link>
      <guid>https://dev.to/ssukhpinder/net-10-bufferedstream-writebyte-flush-full-buffers-explicitly-45a2</guid>
      <description>&lt;p&gt;&lt;code&gt;BufferedStream.WriteByte&lt;/code&gt; had a surprising side effect through .NET 9: the call at its internal capacity boundary also called &lt;code&gt;Flush()&lt;/code&gt; on the wrapped stream. The &lt;strong&gt;.NET 10 BufferedStream WriteByte&lt;/strong&gt; behavior removes that implicit flush. Bytes can still move to the underlying stream when the buffer needs room, but that capacity boundary no longer becomes an accidental flush boundary.&lt;/p&gt;

&lt;p&gt;That distinction matters when a custom stream, protocol adapter, compressor, or test double gives &lt;code&gt;Flush()&lt;/code&gt; observable meaning. The application may still deliver the right bytes eventually while missing the side effect it previously got at a particular point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why .NET 10 BufferedStream WriteByte changed
&lt;/h2&gt;

&lt;p&gt;Microsoft documents this as a &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/bufferedstream-writebyte-flush" rel="noopener noreferrer"&gt;.NET 10 behavioral change&lt;/a&gt;. &lt;code&gt;WriteByte&lt;/code&gt; used to differ from the other &lt;code&gt;BufferedStream.Write&lt;/code&gt; methods by flushing the underlying stream when a byte write reached its capacity boundary. .NET 10 removes that inconsistency.&lt;/p&gt;

&lt;p&gt;This is stable behavior in .NET 10 LTS, not a preview feature. I verified the sample with SDK 10.0.303 and the &lt;a href="https://github.com/dotnet/core/blob/main/release-notes/10.0/10.0.11/10.0.11.md" rel="noopener noreferrer"&gt;10.0.11 runtime&lt;/a&gt;, alongside runtime 9.0.18 for the old contract.&lt;/p&gt;

&lt;p&gt;The word “flush” needs care here. &lt;code&gt;BufferedStream&lt;/code&gt; can write buffered bytes to its destination to make room without calling the destination's &lt;code&gt;Flush()&lt;/code&gt; method. The breaking change is about that method call, not a promise that every byte stays inside &lt;code&gt;BufferedStream&lt;/code&gt; until disposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the old and new flush boundary
&lt;/h2&gt;

&lt;p&gt;I prefer an executable contract over guessing from a &lt;code&gt;MemoryStream.Length&lt;/code&gt; check. The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/073-bufferedstream-explicit-flush" rel="noopener noreferrer"&gt;complete multi-target verifier&lt;/a&gt; wraps a memory stream and counts both writes and flushes:&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;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TrackingStream&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MemoryStream&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;FlushCalls&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;private&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;int&lt;/span&gt; &lt;span class="n"&gt;WriteCalls&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;private&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="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FlushCalls&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WriteCalls&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&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;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WriteCalls&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test uses a four-byte buffer and writes exactly four bytes:&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;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&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;TrackingStream&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;BufferedStream&lt;/span&gt; &lt;span class="n"&gt;buffered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bufferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteByte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;$"flushes=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FlushCalls&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, bytes=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToHexString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result before an explicit flush is precise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;net9.0:  flushes=1, writes=1, bytes=010203
net10.0: flushes=0, writes=1, bytes=010203
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both runtimes write the first three bytes to make room and retain the fourth byte in the buffer. Only .NET 9 also calls &lt;code&gt;Flush()&lt;/code&gt; on the destination. That is the regression test worth preserving.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;buffered.Flush()&lt;/code&gt;, both targets contain &lt;code&gt;01020304&lt;/code&gt; in order. The explicit call adds exactly one destination flush and one write for the remaining byte. The sample asserts seven contracts on each framework and repeats with byte-identical output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the boundary explicit
&lt;/h2&gt;

&lt;p&gt;The fix is not to recreate the old buffer-size accident. Put the flush where the application owns a real boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;encodedRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteByte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// The logical record is complete.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an asynchronous pipeline, use &lt;code&gt;FlushAsync(cancellationToken)&lt;/code&gt; at the equivalent record or batch boundary. The &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.io.bufferedstream.flush?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;BufferedStream.Flush&lt;/code&gt; contract&lt;/a&gt; sends buffered data to the underlying stream and clears the buffer. It is clearer than depending on an internal capacity that could change with construction or implementation details.&lt;/p&gt;

&lt;p&gt;I would also add a contract test around the stream that actually matters. A counter-based fake is good for proving call order. A protocol fixture can go further and verify that one complete record becomes observable only after the chosen flush. Neither test needs a network service.&lt;/p&gt;

&lt;p&gt;Avoid flushing after every byte. That discards the batching benefit that justified &lt;code&gt;BufferedStream&lt;/code&gt; in the first place. A record, frame, or bounded batch is usually a better unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and when not to use this
&lt;/h2&gt;

&lt;p&gt;If the code only needs all bytes when the stream is disposed, and the wrapped stream gives &lt;code&gt;Flush()&lt;/code&gt; no special side effect, no change may be required. Disposal still provides the final boundary.&lt;/p&gt;

&lt;p&gt;An explicit managed flush is also not automatically a durability guarantee. A memory stream, network stream, compressor, and file stream can assign different meaning to &lt;code&gt;Flush()&lt;/code&gt;. If the requirement is physical-media durability, test and invoke the storage-specific durability operation after draining &lt;code&gt;BufferedStream&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The main migration rule is small: do not use internal buffer capacity as application control flow. Name the boundary your protocol or storage contract actually needs, flush there, and verify it offline.&lt;/p&gt;

&lt;p&gt;Where does your code need that boundary: after a record, after a batch, or only during shutdown?&lt;/p&gt;

&lt;p&gt;Happy debugging!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>debugging</category>
      <category>testing</category>
    </item>
    <item>
      <title>.NET 10 NU1510 Package Pruning: Fix CI Without Breaking Legacy Targets</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Mon, 24 Aug 2026 22:23:40 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-nu1510-package-pruning-fix-ci-without-breaking-legacy-targets-5ggd</link>
      <guid>https://dev.to/ssukhpinder/net-10-nu1510-package-pruning-fix-ci-without-breaking-legacy-targets-5ggd</guid>
      <description>&lt;p&gt;.NET 10 NU1510 package pruning can turn an SDK upgrade into a failed restore when a repository promotes warnings to errors. The tempting fix is to delete the named &lt;code&gt;PackageReference&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;That is safe for a &lt;code&gt;net10.0&lt;/code&gt;-only application when the framework supplies the assembly, but it can break a library that still targets &lt;code&gt;netstandard2.0&lt;/code&gt;. I treat &lt;code&gt;NU1510&lt;/code&gt; as a target-specific dependency decision: prove where the reference is redundant, retain it where it is required, and verify the package produced for consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why NU1510 becomes a CI failure
&lt;/h2&gt;

&lt;p&gt;Beginning with .NET 10, package pruning is enabled by default for projects targeting .NET 10 or later. NuGet raises &lt;code&gt;NU1510&lt;/code&gt; when a direct reference to a package registered for pruning can be completely removed because the targeted SDK supplies the same or a higher assembly version.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1510" rel="noopener noreferrer"&gt;&lt;code&gt;NU1510&lt;/code&gt; diagnostic reference&lt;/a&gt; is precise about that boundary. This is not a general-purpose unused-package detector, and it does not apply automatically to arbitrary third-party packages.&lt;/p&gt;

&lt;p&gt;The sample applies a CI-style policy globally:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Its intentionally broken &lt;code&gt;net10.0&lt;/code&gt; project then adds this direct reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"System.Text.Json"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"10.0.11"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the stable .NET SDK 10.0.303 used for the sample, restore exits with code 1 because &lt;code&gt;NU1510&lt;/code&gt; is promoted from a warning to an error. Without a warnings-as-errors policy, the same condition is normally a warning rather than a failed restore.&lt;/p&gt;

&lt;p&gt;Microsoft's &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/nu1510-pruned-references" rel="noopener noreferrer"&gt;.NET 10 breaking-change guidance&lt;/a&gt; recommends removing the reference when every target can prune it, or conditioning it when an older target still needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix .NET 10 NU1510 package pruning per target
&lt;/h2&gt;

&lt;p&gt;For the &lt;code&gt;net10.0&lt;/code&gt;-only application, the fix is simply to remove the package reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Exe&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net10.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application still imports &lt;code&gt;System.Text.Json&lt;/code&gt; and serializes the same payload. Its verified output is:&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="nl"&gt;"Message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"framework-provided"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A multi-target library needs a different answer. The sample supports &lt;code&gt;netstandard2.0&lt;/code&gt; and &lt;code&gt;net10.0&lt;/code&gt;, so deleting the reference globally would remove a dependency required by the older target. I make that requirement explicit in MSBuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;TargetFrameworks&amp;gt;&lt;/span&gt;netstandard2.0;net10.0&lt;span class="nt"&gt;&amp;lt;/TargetFrameworks&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;ItemGroup&lt;/span&gt; &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"'$(TargetFramework)' == 'netstandard2.0'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"System.Text.Json"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"10.0.11"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unconditioned reference that remains necessary for one runtime target does not produce &lt;code&gt;NU1510&lt;/code&gt;, because it cannot be removed from every target. Current pack behavior also omits a prunable dependency from the &lt;code&gt;net10.0&lt;/code&gt; group while retaining it for the older target. I use the explicit condition to make the intent reviewable, not because every unconditioned multi-target reference is wrong.&lt;/p&gt;

&lt;p&gt;NuGet restore creates a separate dependency graph for each target framework, and &lt;code&gt;dotnet pack&lt;/code&gt; produces framework-specific dependency metadata. The &lt;a href="https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#targeting-multiple-frameworks" rel="noopener noreferrer"&gt;PackageReference documentation&lt;/a&gt; documents target conditions. For a larger framework matrix, a compatibility-based condition may be easier to maintain, but it still needs verification against every supported target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the dependency graph, not only the warning
&lt;/h2&gt;

&lt;p&gt;I do not consider a disappearing warning sufficient evidence. A library fix must also preserve the dependency contract seen by package consumers.&lt;/p&gt;

&lt;p&gt;The sample runs its checks with:&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;restore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\Verifier\Verifier.csproj&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\Verifier\Verifier.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--configuration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\Verifier\Verifier.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--configuration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;whitespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\Nu1510PackagePruning.slnx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--verify-no-changes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier owns the expected failure, so the broken project is deliberately excluded from the solution. It checks nine behaviors: the broken restore fails with &lt;code&gt;NU1510&lt;/code&gt;; the diagnostic names &lt;code&gt;System.Text.Json&lt;/code&gt;; the fixed application runs; the fixed assets contain no package copy; and the multi-target assets plus packed &lt;code&gt;.nuspec&lt;/code&gt; keep the dependency only for &lt;code&gt;netstandard2.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The completed verifier reports &lt;code&gt;PASS 9/9&lt;/code&gt;. Five repeated runs also produced byte-identical output. You can inspect the &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/079-nu1510-package-pruning" rel="noopener noreferrer"&gt;complete runnable sample&lt;/a&gt; and the &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/69" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This distinction matters: the sample executes the modern application, while the older library target is validated through restore, build, assets metadata, and the packed &lt;code&gt;.nuspec&lt;/code&gt;. It does not claim to execute a &lt;code&gt;netstandard2.0&lt;/code&gt; application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and when not to remove a reference
&lt;/h2&gt;

&lt;p&gt;This approach applies only when the SDK or a referenced framework has registered a package for pruning. A custom package is not made redundant merely because its namespace resembles framework functionality.&lt;/p&gt;

&lt;p&gt;The sample covers the SDK-provided &lt;code&gt;System.Text.Json&lt;/code&gt; case. It does not model the separate &lt;code&gt;FrameworkReference&lt;/code&gt; and transitive &lt;code&gt;ProjectReference&lt;/code&gt; scenario described in the diagnostic documentation. It also makes no benchmark claim about restore size or speed.&lt;/p&gt;

&lt;p&gt;Before removing a reference in a real library, I would restore, build, test, and pack every supported target. I would also inspect the resulting dependency groups, especially when central package management or more complex MSBuild conditions are involved. Suppressing &lt;code&gt;NU1510&lt;/code&gt; may unblock a migration temporarily, but it does not establish whether the published dependency contract is correct.&lt;/p&gt;

&lt;p&gt;Which multi-targeted package reference will you audit before your next .NET 10 upgrade?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>devops</category>
      <category>debugging</category>
    </item>
    <item>
      <title>MCP Pagination Empty `nextCursor`: Don't Stop After Page One</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:51:37 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/mcp-pagination-empty-nextcursor-dont-stop-after-page-one-1006</link>
      <guid>https://dev.to/ssukhpinder/mcp-pagination-empty-nextcursor-dont-stop-after-page-one-1006</guid>
      <description>&lt;p&gt;MCP pagination empty &lt;code&gt;nextCursor&lt;/code&gt; handling looks like a tiny null check, but the wrong predicate can hide most of a server's catalog. In the final 2026-07-28 specification, cursors are opaque strings. An empty string is valid; only a missing &lt;code&gt;nextCursor&lt;/code&gt; ends traversal. In a nullable C# response model, that absence is represented by &lt;code&gt;null&lt;/code&gt;. I built a small .NET 10 verifier because this failure is unusually quiet: the request succeeds, the first page looks reasonable, and no error says that later pages never arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP pagination empty nextCursor breaks quietly
&lt;/h2&gt;

&lt;p&gt;MCP uses cursor pagination for &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;prompts/list&lt;/code&gt;, &lt;code&gt;resources/list&lt;/code&gt;, and &lt;code&gt;resources/templates/list&lt;/code&gt;. The server chooses each page size, so a client cannot infer completion from the number of returned items.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination" rel="noopener noreferrer"&gt;official pagination specification&lt;/a&gt; gives the reliable rule: continue whenever the response supplies a non-null &lt;code&gt;nextCursor&lt;/code&gt;. The client must pass that token back without parsing or changing it. The empty string is still a supplied token.&lt;/p&gt;

&lt;p&gt;Stripped to the fields relevant to this bug, an intermediate response can contain:&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;"result"&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;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"catalog.search"&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;"nextCursor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="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;That JSON is a pagination excerpt, not a complete MCP wire message. A conforming response still needs its JSON-RPC envelope and the other required result metadata, while every 2026-07-28 request needs the required &lt;code&gt;_meta&lt;/code&gt;. None of those fields changes the cursor termination rule.&lt;/p&gt;

&lt;p&gt;A common C# loop accidentally treats that response as the last page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cursor&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;do&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;page&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;pager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ListToolsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&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;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NextCursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="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;cursor&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IsNullOrEmpty&lt;/code&gt; combines two states that have different protocol meanings. A missing field, represented by &lt;code&gt;null&lt;/code&gt; in this DTO, means finished. Empty means make another request with &lt;code&gt;cursor: ""&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The symptom can outlive the original request. If the client caches the partial catalog, later tool selection operates on the same incomplete view until that cache expires. Counting returned tools is not a safe fallback because the server controls page size and a one-item page may be perfectly legitimate. I prefer tests that assert the sequence of requested cursor values, not just the final item count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the page-one bug offline
&lt;/h2&gt;

&lt;p&gt;My fake &lt;code&gt;tools/list&lt;/code&gt; server has three deterministic pages. The first request omits the cursor. Page one returns &lt;code&gt;""&lt;/code&gt;; page two returns &lt;code&gt;opaque:/+==&lt;/code&gt;; page three omits &lt;code&gt;nextCursor&lt;/code&gt;, which the C# page model represents as &lt;code&gt;null&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="k"&gt;switch&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;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ToolPage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"catalog.search"&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="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ToolPage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"catalog.lookup"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s"&gt;"opaque:/+=="&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s"&gt;"opaque:/+=="&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;ToolPage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"catalog.health"&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;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;McpProtocolException&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;32602&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Invalid cursor"&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 broken loop returns only &lt;code&gt;catalog.search&lt;/code&gt;. That is more dangerous than a loud exception because a user may simply believe the server exposes one tool.&lt;/p&gt;

&lt;p&gt;The opaque second token contains punctuation on purpose. A client that decodes, trims, normalizes, or reconstructs it is also violating the contract. An unknown token triggers &lt;code&gt;-32602&lt;/code&gt;, the specification's recommended Invalid params response for a bad cursor.&lt;/p&gt;

&lt;p&gt;The complete &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/080-mcp-empty-cursor-pagination" rel="noopener noreferrer"&gt;runnable sample on &lt;code&gt;main&lt;/code&gt;&lt;/a&gt; performs eight checks without an MCP server, credential, model call, clock, or random input. The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/70" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; records the exact validation commands and results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the loop by testing only for null
&lt;/h2&gt;

&lt;p&gt;The corrected loop makes the protocol distinction explicit:&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;for&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;pageNumber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;pageNumber&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxPages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;pageNumber&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;page&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;pager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ListToolsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&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;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NextCursor&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NextCursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pagination exceeded its page limit."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This version forwards both &lt;code&gt;""&lt;/code&gt; and &lt;code&gt;opaque:/+==&lt;/code&gt; without changing their string values. It also includes cancellation and a page cap so a faulty server cannot keep the client in an unbounded loop. The cap is an application guard, not a conclusion derived from a cursor's contents.&lt;/p&gt;

&lt;p&gt;In production, I would also check the mapping layer, not only the loop. A nullable string can represent the optional field correctly because absence maps to &lt;code&gt;null&lt;/code&gt;, while &lt;code&gt;""&lt;/code&gt; remains non-null. Explicit JSON &lt;code&gt;null&lt;/code&gt; is outside the 2026-07-28 cursor schema, so a strict mapper should reject it rather than treat it as a normal terminator. A custom converter, DTO mapper, or helper such as &lt;code&gt;NullIfEmpty&lt;/code&gt; can erase the valid empty-string distinction before pagination code sees it. A transport-level regression matrix should therefore cover an absent field, malformed explicit &lt;code&gt;null&lt;/code&gt;, an empty string, and a punctuation-heavy token.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;2026-07-28 release announcement&lt;/a&gt; confirms that this is the released specification and that the Tier 1 SDKs were updated for it. Even when an SDK offers a pagination helper, I still want a boundary test with an empty token. It catches wrappers that apply a language's truthiness rules after the SDK returns a page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and when not to write your own pager
&lt;/h2&gt;

&lt;p&gt;This verifier isolates cursor control. It does not implement JSON-RPC framing, transports, authentication, required request &lt;code&gt;_meta&lt;/code&gt;, cache metadata, retries, or a dataset changing between calls. A production client must define whether an error discards partial results, how retries interact with cursors, and what telemetry is safe to record.&lt;/p&gt;

&lt;p&gt;The page cap also needs an explicit failure path. Reaching it should not quietly return an incomplete catalog that looks successful. I would surface a distinct error, retain enough page-count context for diagnosis, and avoid recording the opaque cursor value unless the application's data policy permits it. A retry should resend the exact cursor associated with the failed page rather than restart and combine two potentially different catalog snapshots.&lt;/p&gt;

&lt;p&gt;I would not replace a well-tested SDK paginator just to own more loop code. I would keep the empty-cursor fixture as a regression test around the abstraction my application actually calls. I also would not copy this termination rule into an unrelated REST or GraphQL API without reading that API's contract; some protocols explicitly use empty values as sentinels.&lt;/p&gt;

&lt;p&gt;Does your MCP client test a non-null empty cursor, or would its catalog stop after page one?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <item>
      <title>.NET 10 NU1015: Fix PackageReference Without Version Restore Failures</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:00:04 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-nu1015-fix-packagereference-without-version-restore-failures-4741</link>
      <guid>https://dev.to/ssukhpinder/net-10-nu1015-fix-packagereference-without-version-restore-failures-4741</guid>
      <description>&lt;p&gt;.NET 10 NU1015 turns a &lt;code&gt;PackageReference&lt;/code&gt; without a version into a restore error. I like the stricter default because an unbounded direct dependency can quietly resolve the lowest package version. The catch is that versionless XML is also the correct shape for NuGet Central Package Management (CPM). A mechanical “add &lt;code&gt;Version&lt;/code&gt; everywhere” repair can undo the policy your repository intended to enforce.&lt;/p&gt;

&lt;p&gt;I use a simple split: first decide who owns the version, then make restore prove the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why .NET 10 NU1015 stops the build
&lt;/h2&gt;

&lt;p&gt;Before .NET 10, NuGet reported &lt;code&gt;NU1604&lt;/code&gt; when a direct reference had no inclusive lower bound. Restore could continue and select the lowest version available from the configured sources. Starting with .NET 10, the same mistake produces &lt;code&gt;NU1015&lt;/code&gt; and restore fails. Microsoft documents this as a stable behavioral change in the &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/nu1015-packagereference-version" rel="noopener noreferrer"&gt;.NET 10 compatibility guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is the ambiguous project entry:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If this is a normal direct reference, the project is missing its version. If CPM is active, the project is correct and the version should live elsewhere. The &lt;a href="https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1015" rel="noopener noreferrer"&gt;NU1015 diagnostic reference&lt;/a&gt; calls out a common failure mode: a project that expected CPM was copied into a location where CPM is disabled or its props file is no longer discovered.&lt;/p&gt;

&lt;p&gt;That distinction matters more than silencing the error. It tells me whether the project file or the repository-level package policy is broken.&lt;/p&gt;

&lt;p&gt;The timing can be misleading. An SDK upgrade may expose an old direct reference that had always relied on lowest-version resolution, while a repository move may break a previously valid CPM import. I inspect the failing project's evaluated inputs, nearby props files, and recent path changes before editing package metadata. That keeps a restore migration from turning into an accidental package-management migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the owner, not only the XML
&lt;/h2&gt;

&lt;p&gt;For a direct reference, I add an explicit version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Demo.Greeting"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For CPM, I keep the project reference versionless and put the version in the nearest &lt;code&gt;Directory.Packages.props&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ManagePackageVersionsCentrally&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/ManagePackageVersionsCentrally&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageVersion&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Demo.Greeting"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NuGet automatically imports only the first &lt;code&gt;Directory.Packages.props&lt;/code&gt; it finds while walking up from a project. A nested file can therefore change which policy applies, and a copied project can lose the import completely. The official &lt;a href="https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management" rel="noopener noreferrer"&gt;Central Package Management guide&lt;/a&gt; describes both the enabling property and the nearest-file rule.&lt;/p&gt;

&lt;p&gt;I check the evaluated project context instead of assuming that a props file somewhere in the repository is active. The two valid outcomes are clear: a direct &lt;code&gt;PackageReference&lt;/code&gt; owns its &lt;code&gt;Version&lt;/code&gt;, or an active CPM file owns the matching &lt;code&gt;PackageVersion&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce NU1015 without an external feed
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/066-nu1015-package-versions" rel="noopener noreferrer"&gt;runnable sample&lt;/a&gt; creates a local package feed containing &lt;code&gt;Demo.Greeting&lt;/code&gt; versions &lt;code&gt;1.0.0&lt;/code&gt; and &lt;code&gt;2.0.0&lt;/code&gt;. It then tests three consumers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BrokenDirect&lt;/code&gt; has no version and must fail with &lt;code&gt;NU1015&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FixedDirect&lt;/code&gt; pins &lt;code&gt;2.0.0&lt;/code&gt; on the reference.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CentralManaged&lt;/code&gt; leaves the reference versionless and pins &lt;code&gt;2.0.0&lt;/code&gt; in &lt;code&gt;Directory.Packages.props&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the sample folder, I run:&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;restore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\Verifier\Verifier.csproj&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\Verifier\Verifier.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\Verifier\Verifier.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier packs both fixture versions, runs each restore, reads &lt;code&gt;project.assets.json&lt;/code&gt;, and executes the repaired projects. The important assertions are not just “restore passed.” Both supported repairs must resolve and run with &lt;code&gt;2.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The sample also demonstrates the documented compatibility switch:&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;restore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\BrokenDirect\BrokenDirect.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;SdkAnalysisLevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;9.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;TreatWarningsAsErrors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That brings back &lt;code&gt;NU1604&lt;/code&gt;, and the local feed proves why I do not treat it as the fix: NuGet chooses &lt;code&gt;1.0.0&lt;/code&gt;, the lowest available version. The complete validation and review history is in the &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/56" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and when I avoid the escape hatch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SdkAnalysisLevel=9.0.300&lt;/code&gt; affects every SDK behavior gated by that level, not only this diagnostic. A warnings-as-errors policy can also promote &lt;code&gt;NU1604&lt;/code&gt; back to a failure. I reserve the switch for short migration diagnostics while I determine who should own the version.&lt;/p&gt;

&lt;p&gt;The local-feed sample does not model authenticated sources, source mapping, package lock files, or feed outages. Those controls still matter in a production restore. Its narrower job is to make the ownership decision and resolved version deterministic.&lt;/p&gt;

&lt;p&gt;I also would not add &lt;code&gt;Version="0.0.0"&lt;/code&gt; merely to make &lt;code&gt;NU1015&lt;/code&gt; disappear. Microsoft documents that value for the unusual case where the lowest version is genuinely intended, but it still produces &lt;code&gt;NU1603&lt;/code&gt; when NuGet resolves a higher available version. Most application dependencies need a deliberate version or an active central policy instead.&lt;/p&gt;

&lt;p&gt;For direct references, I pin the intended dependency. For CPM repositories, I verify the import boundary and keep versions centralized. Either way, I leave restore with one explicit owner and a test that checks the resolved graph.&lt;/p&gt;

&lt;p&gt;Have you hit &lt;code&gt;NU1015&lt;/code&gt; because a direct version was missing, or because a project silently lost its CPM context?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>devops</category>
      <category>debugging</category>
    </item>
    <item>
      <title>.NET 10 JSON Console Logging: Stop Parsing State.Message</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Sat, 22 Aug 2026 18:47:25 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-json-console-logging-stop-parsing-statemessage-6p5</link>
      <guid>https://dev.to/ssukhpinder/net-10-json-console-logging-stop-parsing-statemessage-6p5</guid>
      <description>&lt;p&gt;The &lt;strong&gt;.NET 10 JSON console logging&lt;/strong&gt; change is small enough to miss during an upgrade: the formatted message still exists, but a typical record no longer duplicates it at &lt;code&gt;State.Message&lt;/code&gt;. A collector, script, or snapshot test that reads only that nested property can start returning null while the application continues logging normally.&lt;/p&gt;

&lt;p&gt;I treat console JSON as a schema whenever another process parses it. That means a runtime upgrade deserves a contract test, not just a visual check in a terminal. The practical fix is to read the top-level &lt;code&gt;Message&lt;/code&gt;, keep &lt;code&gt;State&lt;/code&gt; for structured values, and retain a narrow fallback for older records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why .NET 10 JSON console logging breaks nested-message parsers
&lt;/h2&gt;

&lt;p&gt;Before .NET 10, a normal &lt;code&gt;AddJsonConsole&lt;/code&gt; record commonly repeated the rendered text:&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;"Message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order 42 moved to ready."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"State"&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;"Message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order 42 moved to ready."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"OrderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"{OriginalFormat}"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order {OrderId} moved to {Status}."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In .NET 10, the typical shape keeps one rendered message at the top level:&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;"Message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order 42 moved to ready."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"State"&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;"OrderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"{OriginalFormat}"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order {OrderId} moved to {Status}."&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;Microsoft documents this as a behavioral breaking change and recommends that parsers use the top-level property. The &lt;a href="https://learn.microsoft.com/dotnet/core/compatibility/extensions/10.0/console-json-logging-duplicate-messages" rel="noopener noreferrer"&gt;official compatibility note&lt;/a&gt; also gives an essential caveat: &lt;code&gt;State.Message&lt;/code&gt; may still appear when its content differs from the top-level value. I therefore do not reject a record merely because both properties exist.&lt;/p&gt;

&lt;p&gt;This is not a loss of structured logging data. &lt;code&gt;OrderId&lt;/code&gt;, &lt;code&gt;Status&lt;/code&gt;, and &lt;code&gt;{OriginalFormat}&lt;/code&gt; remain useful fields inside &lt;code&gt;State&lt;/code&gt;. The part that changed is where a consumer should get the rendered sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer the top-level Message and keep State structured
&lt;/h2&gt;

&lt;p&gt;A legacy-only extractor is brittle because it assumes the duplicate is the contract:&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;static&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;ReadLegacyOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;root&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;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;"State"&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;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="n"&gt;state&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;"Message"&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;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use a top-level-first rule instead:&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;static&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;ReadMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;root&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;ReadTopLevelMessage&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="p"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;ReadStateMessage&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fallback is for stored .NET 9-era records or mixed-version fleets. It is not a reason to keep a new parser anchored to the old nested location. When both values exist and differ, the top-level field stays canonical, matching Microsoft's migration guidance.&lt;/p&gt;

&lt;p&gt;I also parse the structured properties separately. Searching a rendered sentence for an order ID throws away the main benefit of JSON logging. The formatter keeps those values addressable under &lt;code&gt;State&lt;/code&gt;, while &lt;code&gt;{OriginalFormat}&lt;/code&gt; preserves the message template for grouping or diagnostics.&lt;/p&gt;

&lt;p&gt;The built-in formatter can be configured with &lt;code&gt;AddJsonConsole&lt;/code&gt;; Microsoft's &lt;a href="https://learn.microsoft.com/dotnet/core/extensions/console-log-formatter#json" rel="noopener noreferrer"&gt;console formatter documentation&lt;/a&gt; covers timestamps, scopes, and JSON options. Those options can change other parts of a record, so my contract focuses only on fields the consuming pipeline actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the schema into an offline regression test
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/067-json-console-message" rel="noopener noreferrer"&gt;runnable sample&lt;/a&gt; contains a documented legacy fixture, a real .NET 10 emitter, and a verifier. The emitter uses the installed runtime rather than a hand-written “current” fixture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddJsonConsole&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;UseUtcTimestamp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimestampFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"O"&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;orderMoved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Define&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;new&lt;/span&gt; &lt;span class="nf"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OrderMoved"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s"&gt;"Order {OrderId} moved to {Status}."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;orderMoved&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="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ready"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier launches that emitter, captures its single JSON line, and checks semantics rather than timestamp text or property order. It proves that the top-level message exists, the redundant nested message is absent for this ordinary case, and the structured values remain intact. It then runs the same compatibility extractor against the legacy fixture.&lt;/p&gt;

&lt;p&gt;That test is deterministic and offline after restore. It needs no logging backend, account, credential, or paid service. It also catches a more realistic failure than a unit test built from two hand-authored strings: the current side of the contract comes from &lt;code&gt;AddJsonConsole&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;I verified the sample with .NET SDK 10.0.303 and runtime 10.0.11; the &lt;a href="https://github.com/dotnet/core/blob/main/release-notes/10.0/10.0.11/10.0.11.md" rel="noopener noreferrer"&gt;official 10.0.11 release notes&lt;/a&gt; identify that patch as a supported .NET 10 release. The contract change applies to .NET 10 generally, not only that patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and when not to parse console JSON
&lt;/h2&gt;

&lt;p&gt;This approach is for consumers that must accept the built-in console formatter's output. A provider, collector, or OpenTelemetry pipeline may expose a different and better-defined schema. I would test that contract directly instead of translating every provider into this shape.&lt;/p&gt;

&lt;p&gt;Console JSON is also not an ideal application-to-application protocol. If I control both sides, I prefer a structured transport with explicit versioning. When console output is the available boundary, a small compatibility extractor and fixture set make the assumption visible.&lt;/p&gt;

&lt;p&gt;Finally, I would not assert that &lt;code&gt;State.Message&lt;/code&gt; can never exist. The official caveat matters. Prefer top-level &lt;code&gt;Message&lt;/code&gt;, preserve structured &lt;code&gt;State&lt;/code&gt;, and test the specific records your pipeline depends on.&lt;/p&gt;

&lt;p&gt;What console log field is your upgrade test still assuming will always be there?&lt;/p&gt;

&lt;p&gt;Cheers!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>debugging</category>
      <category>testing</category>
    </item>
    <item>
      <title>MCP C# SDK Array Tool Outputs: Stop Looking for a `result` Wrapper</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:23:31 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper-452c</link>
      <guid>https://dev.to/ssukhpinder/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper-452c</guid>
      <description>&lt;p&gt;MCP C# SDK array tool outputs are easy to misread after a protocol upgrade. A client written around the older wire shape may always reach for &lt;code&gt;structuredContent.result&lt;/code&gt;. Once both sides negotiate MCP &lt;code&gt;2026-07-28&lt;/code&gt;, an array is an array and a scalar is a scalar. There is no required wrapper to unwrap.&lt;/p&gt;

&lt;p&gt;I treat that as a contract change worth testing at the transport boundary. A unit test against the C# return type cannot tell me what &lt;code&gt;tools/list&lt;/code&gt; advertised or what &lt;code&gt;tools/call&lt;/code&gt; actually carried. The small verifier below runs both protocol versions offline and makes the difference explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wire contract changed, not just the C# type
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools" rel="noopener noreferrer"&gt;MCP 2026-07-28 tools specification&lt;/a&gt; allows &lt;code&gt;structuredContent&lt;/code&gt; to contain any JSON value: object, array, string, number, Boolean, or null. An &lt;code&gt;outputSchema&lt;/code&gt; may likewise describe an array or primitive at its root. The schema still matters: servers must return content that conforms to it, and clients should validate the result.&lt;/p&gt;

&lt;p&gt;This is different from the older object-only convention. Under &lt;code&gt;2025-11-25&lt;/code&gt;, a tool returning &lt;code&gt;string[]&lt;/code&gt; needs an object envelope such as this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"structuredContent"&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;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"starter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"growth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enterprise"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The modern form is the value itself:&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;"structuredContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"starter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"growth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enterprise"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stable MCP C# SDK handles this negotiation. Its &lt;a href="https://github.com/modelcontextprotocol/csharp-sdk/releases/tag/v2.0.0" rel="noopener noreferrer"&gt;v2.0.0 release notes&lt;/a&gt; call out direct non-object tool results, while the current &lt;a href="https://csharp.sdk.modelcontextprotocol.io/concepts/tools/tools.html" rel="noopener noreferrer"&gt;v2.2.0 tools guide&lt;/a&gt; documents &lt;code&gt;UseStructuredContent&lt;/code&gt;. For a down-level client, the SDK still emits the compatibility envelope. I do not need a second handler or a hand-written version switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build two real in-memory protocol sessions
&lt;/h2&gt;

&lt;p&gt;The sample pins the current stable package and targets .NET 10:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"ModelContextProtocol"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.2.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It registers one array tool and one scalar tool. &lt;code&gt;UseStructuredContent = true&lt;/code&gt; tells the SDK to generate the output schema and serialize the return value into &lt;code&gt;structuredContent&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;toolCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Func&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="k"&gt;]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;ListTiers&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;McpServerToolCreateOptions&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;"list_tiers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UseStructuredContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ReadOnly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="n"&gt;toolCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="n"&gt;CountTiers&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;McpServerToolCreateOptions&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;"count_tiers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UseStructuredContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ReadOnly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;System.IO.Pipelines.Pipe&lt;/code&gt; instances connect an &lt;code&gt;McpClient&lt;/code&gt; and &lt;code&gt;McpServer&lt;/code&gt;. Nothing opens a port and no model is involved, but discovery and calls still cross the SDK's stream transport. I create one pair with &lt;code&gt;McpClientOptions.ProtocolVersion = "2026-07-28"&lt;/code&gt; and a fresh pair with &lt;code&gt;"2025-11-25"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using separate sessions matters. The negotiated protocol belongs to a connection, so changing an option after discovery would not prove the real compatibility path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify MCP C# SDK array tool outputs on the wire
&lt;/h2&gt;

&lt;p&gt;For the modern session, I assert both the advertised schema and the returned value:&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;JsonElement&lt;/span&gt; &lt;span class="n"&gt;arraySchema&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arrayTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProtocolTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputSchema&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;CallToolResult&lt;/span&gt; &lt;span class="n"&gt;arrayResult&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;arrayTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arraySchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arrayResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;ValueKind&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JsonValueKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nf"&gt;TryReadLegacyResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arrayResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredContent&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="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last check is a deliberate negative control. It models the parser I want to remove: one that only succeeds when the root is an object containing &lt;code&gt;result&lt;/code&gt;. If the contract test only checks the values, an accidental wrapper can slip back in unnoticed.&lt;/p&gt;

&lt;p&gt;The legacy session checks the opposite shape:&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="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsLegacyEnvelope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arraySchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;arrayResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredContent&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueKind&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JsonValueKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sessions then verify the three tier values and the text content fallback. That fallback is useful for older or text-oriented consumers and is recommended by the specification when structured content is returned.&lt;/p&gt;

&lt;p&gt;On the client side, I branch from the discovered schema before reading the result. I do not infer the shape from the tool name or from a C# type in my own codebase; a client may be talking to a server implemented in another language. For an array root, I enumerate the value directly. For the down-level object schema, I read the required &lt;code&gt;result&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;Checking discovery and invocation together also catches an asymmetric bug: a server could advertise an array schema but return a wrapped object, or advertise the legacy envelope and emit a bare array. Either response may contain the expected values while still violating the declared contract. The paired assertions fail on that mismatch before application parsing hides it.&lt;/p&gt;

&lt;p&gt;Running the verifier produces a stable summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS 15/15
  modern: array schema has an array root
  modern: array result has no result wrapper
  modern: legacy result-wrapper parser is rejected
  legacy: array schema advertises a result envelope
  legacy: array result keeps the result envelope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/056-mcp-array-tool-outputs" rel="noopener noreferrer"&gt;sample on &lt;code&gt;main&lt;/code&gt;&lt;/a&gt; includes the scalar checks, setup commands, expected output, and vulnerability audit. The associated &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/46" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; records the exact validation commands and results.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this test is not enough
&lt;/h2&gt;

&lt;p&gt;This verifier proves the official MCP C# SDK 2.2.0 behavior over its stream transport. It does not prove that every host accepts every JSON Schema 2020-12 construct. A third-party client may lag the protocol, apply a narrower schema profile, or ignore structured content entirely. I would add an end-to-end test for each real host before relying on array or primitive roots in production.&lt;/p&gt;

&lt;p&gt;An output schema also checks shape, not business meaning. The tool must still validate inputs, authorization, and domain rules. If every consumer already expects an object with named fields, returning a small response record may be clearer than returning a bare array just because the protocol permits it.&lt;/p&gt;

&lt;p&gt;For mixed fleets, I let negotiation select the wire shape and make the client read according to the advertised &lt;code&gt;outputSchema&lt;/code&gt;. I avoid hard-coding either a wrapper or a natural root globally.&lt;/p&gt;

&lt;p&gt;Which client contract would you add to this compatibility matrix first?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <item>
      <title>.NET 10 Numeric String Sorting: Put File 10 After File 9</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:58:53 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-numeric-string-sorting-put-file-10-after-file-9-1nff</link>
      <guid>https://dev.to/ssukhpinder/net-10-numeric-string-sorting-put-file-10-after-file-9-1nff</guid>
      <description>&lt;p&gt;When I display generated filenames, &lt;strong&gt;.NET 10 numeric string sorting&lt;/strong&gt; gives me the order people usually expect: &lt;code&gt;file9.txt&lt;/code&gt; before &lt;code&gt;file10.txt&lt;/code&gt;. The new &lt;code&gt;CompareOptions.NumericOrdering&lt;/code&gt; flag handles digit runs without a handwritten natural-sort parser.&lt;/p&gt;

&lt;p&gt;That sounds like a presentation-only change. It is not. The same comparer also decides whether two strings are equal, which can quietly collapse &lt;code&gt;file2.txt&lt;/code&gt; and &lt;code&gt;file02.txt&lt;/code&gt; inside a set or dictionary. I want both parts of that contract visible before I reuse the comparer across an application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ordinary string order looks wrong
&lt;/h2&gt;

&lt;p&gt;Ordinal comparison reads the characters from left to right. After the shared &lt;code&gt;file&lt;/code&gt; prefix, the first digit in &lt;code&gt;file10.txt&lt;/code&gt; is &lt;code&gt;1&lt;/code&gt;, so it sorts before the &lt;code&gt;9&lt;/code&gt; in &lt;code&gt;file9.txt&lt;/code&gt;. The comparer is behaving correctly; it simply does not interpret a run of digits as a number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;fileNames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"file10.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"file2.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"file02.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"file9.txt"&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;ordinal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file02.txt | file10.txt | file2.txt | file9.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Padding every number can work when I control the naming format, but it is brittle for imported files, user-facing labels, and old data. A regex that splits text and numbers is another option, although it creates parsing, allocation, overflow, and edge-case decisions that a simple sort should not need.&lt;/p&gt;

&lt;h2&gt;
  
  
  .NET 10 numeric string sorting with one comparer
&lt;/h2&gt;

&lt;p&gt;.NET 10 added &lt;code&gt;NumericOrdering&lt;/code&gt; as a stable string-comparison option. The official &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/libraries#numeric-ordering-for-string-comparison" rel="noopener noreferrer"&gt;.NET 10 library notes&lt;/a&gt; show that digit sequences are compared by numeric value, so &lt;code&gt;2&lt;/code&gt; comes before &lt;code&gt;10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I can package the culture and comparison rule in one &lt;code&gt;StringComparer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;StringComparer&lt;/span&gt; &lt;span class="n"&gt;numericComparer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CompareOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumericOrdering&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;numeric&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numericComparer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" | "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file2.txt | file02.txt | file9.txt | file10.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample uses &lt;code&gt;InvariantCulture&lt;/code&gt; for reproducible output across machines. For labels shown directly to a person, &lt;code&gt;CurrentCulture&lt;/code&gt; may be the better choice. The important part is to choose deliberately rather than inherit an implicit comparison rule.&lt;/p&gt;

&lt;p&gt;I also keep this comparer close to the query that needs it. Passing it explicitly to &lt;code&gt;Order&lt;/code&gt; makes the display rule reviewable and prevents a natural-sort policy from leaking into unrelated keys. If case should be ignored for a particular UI, &lt;code&gt;CompareOptions.IgnoreCase&lt;/code&gt; can be combined with &lt;code&gt;NumericOrdering&lt;/code&gt;, but that is another equality decision worth making at the call site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural order also changes equality
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;StringComparer&lt;/code&gt; supplies ordering, equality, and hash codes. With &lt;code&gt;NumericOrdering&lt;/code&gt;, leading zeroes do not change the numeric value of a digit run:&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;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;numericComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file2.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"file02.txt"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// True&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;names&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;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;numericComparer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"file2.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"file02.txt"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That equality is useful when &lt;code&gt;chapter2&lt;/code&gt; and &lt;code&gt;chapter02&lt;/code&gt; are alternate spellings of the same display label. It is destructive when both are real file names that must remain distinct. I use the numeric comparer only at the sorting boundary in that case and keep exact identity under &lt;code&gt;StringComparer.Ordinal&lt;/code&gt; or the platform-appropriate file-name rule.&lt;/p&gt;

&lt;p&gt;Equal items retain their incoming relative order in the sample, which is why &lt;code&gt;file2.txt&lt;/code&gt; stays ahead of &lt;code&gt;file02.txt&lt;/code&gt;. I do not treat input order as a meaningful tie-breaker. If the output must be reproducible after inputs arrive in a different order, I add an explicit ordinal secondary key while leaving identity comparisons separate.&lt;/p&gt;

&lt;p&gt;Punctuation deserves the same care. The &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.globalization.compareoptions?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;CompareOptions&lt;/code&gt; reference&lt;/a&gt; says a decimal point, minus sign, plus sign, or any other non-digit ends the digit sequence. As a result, &lt;code&gt;v1.5&lt;/code&gt; and &lt;code&gt;v1.05&lt;/code&gt; compare as equal: each digit run is compared independently. This is natural collation, not decimal or semantic-version parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the contract, not just the screen
&lt;/h2&gt;

&lt;p&gt;The runnable &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/038-numeric-string-sorting" rel="noopener noreferrer"&gt;sample on main&lt;/a&gt; turns these details into six deterministic checks. It has no package dependencies, credentials, network calls, or generated test data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet restore
dotnet format NumericStringSorting.csproj &lt;span class="nt"&gt;--verify-no-changes&lt;/span&gt; &lt;span class="nt"&gt;--no-restore&lt;/span&gt;
dotnet build NumericStringSorting.csproj &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;--no-restore&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; NumericStringSorting.csproj &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;--no-build&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier checks ordinal and numeric order, leading-zero equality, &lt;code&gt;HashSet&lt;/code&gt; deduplication, punctuation behavior, and the rejection of &lt;code&gt;NumericOrdering&lt;/code&gt; by index-based operations. A successful run ends with &lt;code&gt;PASS 6/6&lt;/code&gt;. The merged &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/26" rel="noopener noreferrer"&gt;sample pull request&lt;/a&gt; also records the exact validation commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would not use NumericOrdering
&lt;/h2&gt;

&lt;p&gt;I would not use this comparer to parse signed numbers, decimals, dates, or semantic versions. Those domains need parsers that understand their grammar. I also would not use it for &lt;code&gt;IndexOf&lt;/code&gt;, &lt;code&gt;StartsWith&lt;/code&gt;, &lt;code&gt;EndsWith&lt;/code&gt;, and related search operations; &lt;code&gt;NumericOrdering&lt;/code&gt; is not valid for those APIs.&lt;/p&gt;

&lt;p&gt;Most importantly, culture-aware collation should not decide authentication, authorization, protocol identifiers, or other security-sensitive equality. Microsoft's &lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings" rel="noopener noreferrer"&gt;string comparison guidance&lt;/a&gt; recommends ordinal rules for non-linguistic and security comparisons.&lt;/p&gt;

&lt;p&gt;For a visible list containing embedded positive integers, though, the new comparer removes a surprising amount of custom code. I just keep its equality semantics close enough that nobody mistakes natural order for exact identity.&lt;/p&gt;

&lt;p&gt;What would you sort with &lt;code&gt;NumericOrdering&lt;/code&gt; first: filenames, build labels, or something else?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>MCP C# SDK Hybrid Sessions: Serve Old and New Clients on One Endpoint</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Wed, 19 Aug 2026 18:47:30 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/mcp-c-sdk-hybrid-sessions-serve-old-and-new-clients-on-one-endpoint-47hp</link>
      <guid>https://dev.to/ssukhpinder/mcp-c-sdk-hybrid-sessions-serve-old-and-new-clients-on-one-endpoint-47hp</guid>
      <description>&lt;p&gt;The MCP C# SDK hybrid sessions option solves an awkward upgrade boundary: some clients still use the &lt;code&gt;2025-11-25&lt;/code&gt; initialize handshake and depend on sessions, while clients on &lt;code&gt;2026-07-28&lt;/code&gt; expect every HTTP request to stand alone. I want both groups to reach one ASP.NET Core endpoint without making modern clients downgrade or stripping useful behavior from legacy clients.&lt;/p&gt;

&lt;p&gt;The stable C# SDK 2.2.0 release added exactly that path with &lt;code&gt;HttpServerSessionMode.StatefulForInitializeClients&lt;/code&gt;. The &lt;a href="https://github.com/modelcontextprotocol/csharp-sdk/releases/tag/v2.2.0" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; describe it as hybrid stateful/stateless serving, and the &lt;a href="https://csharp.sdk.modelcontextprotocol.io/v2/concepts/stateless/stateless.html#hybrid-mode-sessions-for-initialize-clients-only" rel="noopener noreferrer"&gt;official session-mode guide&lt;/a&gt; spells out the per-request behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one global session switch fails
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;2026-07-28&lt;/code&gt; MCP revision removed the initialize handshake and &lt;code&gt;Mcp-Session-Id&lt;/code&gt; from its wire format. Client identity, capabilities, and protocol version travel with each request instead. The &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;final specification announcement&lt;/a&gt; explains why the core moved toward request/response statelessness.&lt;/p&gt;

&lt;p&gt;That creates a migration choice for an existing server.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;HttpServerSessionMode.Stateful&lt;/code&gt;, initialize-era clients receive full sessions. A modern request is refused so a dual-path client can fall back to the older handshake. Compatibility is preserved, but the client does not use the new protocol natively.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;HttpServerSessionMode.Stateless&lt;/code&gt;, every request is independent. That is the right default for servers that do not need session state, unsolicited notifications, resource subscriptions, or older server-to-client flows. It may be too abrupt when deployed clients still rely on those features.&lt;/p&gt;

&lt;p&gt;Hybrid mode makes the decision from the incoming request instead of applying one choice to the endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure MCP C# SDK hybrid sessions
&lt;/h2&gt;

&lt;p&gt;The server configuration is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcpServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHttpTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;HttpServerSessionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatefulForInitializeClients&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;WithTools&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DemoTools&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;An initialize-era client sends an &lt;code&gt;initialize&lt;/code&gt; request with &lt;code&gt;protocolVersion: "2025-11-25"&lt;/code&gt;. The server returns &lt;code&gt;Mcp-Session-Id&lt;/code&gt;, and that client must send the value on its later requests.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;2026-07-28&lt;/code&gt; client sends &lt;code&gt;server/discover&lt;/code&gt; or another operation with its modern metadata. It does not receive a session ID:&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&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;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server/discover"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&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;"_meta"&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;"io.modelcontextprotocol/protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-28"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"io.modelcontextprotocol/clientCapabilities"&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;"io.modelcontextprotocol/clientInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hybrid-probe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="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;For Streamable HTTP, the request also carries &lt;code&gt;MCP-Protocol-Version: 2026-07-28&lt;/code&gt; and the routing header &lt;code&gt;Mcp-Method: server/discover&lt;/code&gt;. Tool calls add &lt;code&gt;Mcp-Name&lt;/code&gt;. Those headers do not create a session; they let the transport route and validate a self-describing request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove both protocol eras with TestServer
&lt;/h2&gt;

&lt;p&gt;Configuration alone is easy to regress. I prefer a transport-level check that exercises the real SDK handler while staying offline.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/036-mcp-hybrid-sessions" rel="noopener noreferrer"&gt;complete sample&lt;/a&gt; uses &lt;code&gt;Microsoft.AspNetCore.TestHost&lt;/code&gt;, so it opens no port and calls no model. Its verifier runs these checks against the same &lt;code&gt;/mcp&lt;/code&gt; route:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Modern discovery succeeds and returns no &lt;code&gt;Mcp-Session-Id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A modern &lt;code&gt;echo&lt;/code&gt; tool call succeeds and remains stateless.&lt;/li&gt;
&lt;li&gt;Legacy initialize succeeds and mints a non-empty session ID.&lt;/li&gt;
&lt;li&gt;The legacy notification and tool call reuse that session.&lt;/li&gt;
&lt;li&gt;Modern &lt;code&gt;DELETE&lt;/code&gt; returns &lt;code&gt;405 Method Not Allowed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Legacy &lt;code&gt;DELETE&lt;/code&gt; closes its session successfully.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The assertion that matters is not just a &lt;code&gt;200&lt;/code&gt; response. Each side must get the correct session semantics:&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="nf"&gt;AssertNoSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modernToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"modern tool call"&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;sessionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetRequiredSessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;legacyInitialize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"legacy initialize"&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;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;GetRequiredSessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;legacyToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"legacy tool call"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the verifier with:&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;restore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\McpHybridSessions.csproj&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\McpHybridSessions.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-restore&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\McpHybridSessions.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The merged implementation and validation record are also in &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/24" rel="noopener noreferrer"&gt;the pull request&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and when not to use hybrid mode
&lt;/h2&gt;

&lt;p&gt;Hybrid mode is a bridge, not a new universal default. If every supported client speaks &lt;code&gt;2026-07-28&lt;/code&gt; and the server needs no session-only behavior, choose &lt;code&gt;Stateless&lt;/code&gt;. It is simpler to scale because requests can land on any instance without affinity.&lt;/p&gt;

&lt;p&gt;The modern half of a hybrid endpoint is still stateless. It cannot receive unsolicited notifications or use resource subscriptions, and it does not gain per-client isolation. Use the newer multi-round-trip mechanism where it fits rather than assuming hybrid mode restores sessions for modern requests.&lt;/p&gt;

&lt;p&gt;The legacy half still has the operational costs of sessions. Session memory lives on the server, restarts discard it, and multiple instances may need affinity or a deliberate migration design. Authentication and authorization are separate concerns; a session ID is not proof of identity.&lt;/p&gt;

&lt;p&gt;I would keep this regression test until the last initialize-era client is retired, then change the server and test together to the explicit stateless mode.&lt;/p&gt;

&lt;p&gt;Are you keeping a legacy session path during your &lt;code&gt;2026-07-28&lt;/code&gt; migration, or can your server go fully stateless?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <item>
      <title>MCP x-mcp-header Validation: Keep Bad Tool Schemas Out of tools/list</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Wed, 19 Aug 2026 18:33:55 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/mcp-x-mcp-header-validation-keep-bad-tool-schemas-out-of-toolslist-3j3d</link>
      <guid>https://dev.to/ssukhpinder/mcp-x-mcp-header-validation-keep-bad-tool-schemas-out-of-toolslist-3j3d</guid>
      <description>&lt;p&gt;MCP &lt;code&gt;x-mcp-header&lt;/code&gt; validation is easy to miss because the annotation looks like ordinary JSON Schema metadata. On the 2026-07-28 Streamable HTTP transport, it is a wire contract: the client copies selected tool arguments into &lt;code&gt;Mcp-Param-*&lt;/code&gt; headers, intermediaries can act on those headers, and the server checks them against the JSON-RPC body.&lt;/p&gt;

&lt;p&gt;I treat that contract as something to test before a tool reaches &lt;code&gt;tools/list&lt;/code&gt;. A bad suffix, an unsupported type, or an unreachable annotation makes the whole tool definition invalid. Silently accepting it only moves the failure to a harder place to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the same value travels twice
&lt;/h2&gt;

&lt;p&gt;The final &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http" rel="noopener noreferrer"&gt;Streamable HTTP specification&lt;/a&gt; mirrors request metadata into HTTP headers so a load balancer, gateway, or WAF does not need to parse JSON-RPC. A server can add &lt;code&gt;x-mcp-header&lt;/code&gt; to a tool property:&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;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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-mcp-header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Region"&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 call with &lt;code&gt;"region": "us-west1"&lt;/code&gt; then carries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Mcp-Param-Region: us-west1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official C# SDK can generate that schema from a parameter attribute:&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;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ExecuteSql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;McpHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Region"&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;region&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;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"Queued for &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Current &lt;a href="https://csharp.sdk.modelcontextprotocol.io/v2/concepts/tools/tools.html" rel="noopener noreferrer"&gt;C# SDK v2 tool documentation&lt;/a&gt; describes both schema generation and automatic header projection. The feature is on the stable v2 line; it is not necessary to pin an earlier preview or release candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP x-mcp-header validation rules
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools#x-mcp-header" rel="noopener noreferrer"&gt;final tool definition rules&lt;/a&gt; are deliberately narrow.&lt;/p&gt;

&lt;p&gt;The annotation value must be a non-empty HTTP field-name token and must be unique without regard to case. &lt;code&gt;Region&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt; therefore collide. Control characters, spaces, and separators such as a colon are not valid suffix characters.&lt;/p&gt;

&lt;p&gt;Only &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, and &lt;code&gt;boolean&lt;/code&gt; properties can be mirrored. JSON Schema &lt;code&gt;number&lt;/code&gt; is excluded, and integer values must stay between &lt;code&gt;-(2^53 - 1)&lt;/code&gt; and &lt;code&gt;2^53 - 1&lt;/code&gt; so every conforming implementation can represent the value exactly.&lt;/p&gt;

&lt;p&gt;Reachability is the rule most likely to surprise me. An annotated property can be nested, but the path from the schema root must pass only through &lt;code&gt;properties&lt;/code&gt;. An annotation below &lt;code&gt;items&lt;/code&gt;, &lt;code&gt;$ref&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;, &lt;code&gt;allOf&lt;/code&gt;, &lt;code&gt;if&lt;/code&gt;, or another composition or conditional keyword is invalid. A Streamable HTTP client must exclude an invalid tool from the returned &lt;code&gt;tools/list&lt;/code&gt; result and should log the reason. A stdio client may ignore these annotations because it has no HTTP headers to project.&lt;/p&gt;

&lt;p&gt;Values have their own encoding rules. Plain visible ASCII can travel as-is. Non-ASCII text, control characters, leading or trailing whitespace, and strings that already look like the &lt;code&gt;=?base64?...?=&lt;/code&gt; sentinel must be UTF-8/Base64 encoded inside that sentinel. Boolean values become lowercase &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;; mathematically integral JSON forms such as &lt;code&gt;42.0&lt;/code&gt; normalize to decimal &lt;code&gt;42&lt;/code&gt;. If an optional argument is absent or explicitly &lt;code&gt;null&lt;/code&gt;, the client omits its header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make schema drift fail offline
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/16" rel="noopener noreferrer"&gt;sample draft PR&lt;/a&gt; turns those requirements into a dependency-free .NET 10 executable. It scans the relevant JSON Schema subschema locations, ignores annotation-shaped literal data under keywords such as &lt;code&gt;default&lt;/code&gt;, records valid property paths, and fails malformed schemas before any network request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;JsonDocument&lt;/span&gt; &lt;span class="n"&gt;schema&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;schemaJson&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;JsonDocument&lt;/span&gt; &lt;span class="n"&gt;arguments&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;argumentJson&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;headers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;McpHeaderProjector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;schema&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="n"&gt;arguments&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deterministic verifier covers twelve cases, including nested primitive properties, absent and &lt;code&gt;null&lt;/code&gt; arguments, non-ASCII and sentinel encoding, case-insensitive duplicates, the forbidden &lt;code&gt;number&lt;/code&gt; type, annotations below &lt;code&gt;items&lt;/code&gt; and &lt;code&gt;oneOf&lt;/code&gt;, literal example data, invalid HTTP tokens, integral exponent notation, and both safe-integer boundaries.&lt;/p&gt;

&lt;p&gt;I like this style of test because it catches two different regressions. A server refactor can accidentally move an annotation behind a &lt;code&gt;$ref&lt;/code&gt;; a client refactor can stop encoding a padded or Unicode value. Both changes compile, but both break the transport contract.&lt;/p&gt;

&lt;p&gt;At runtime, the server has another job. It must decode recognized &lt;code&gt;Mcp-Param-*&lt;/code&gt; values and compare them with the body. A missing, malformed, or different value is HTTP 400 with JSON-RPC error &lt;code&gt;-32020&lt;/code&gt; (&lt;code&gt;HeaderMismatch&lt;/code&gt;). When that mismatch suggests a stale schema, the client should refresh &lt;code&gt;tools/list&lt;/code&gt; before retrying with the new definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits: routing metadata is not authorization
&lt;/h2&gt;

&lt;p&gt;These headers help infrastructure route, meter, and observe requests. They do not prove that a caller may use the region, tenant, or resource named in the value. An attacker who can choose the body can usually choose the matching header too, so the application still needs normal authentication and authorization checks. A gateway enforcing policy on mirrored headers should reject an absent or older protocol version, where header/body validation is not guaranteed.&lt;/p&gt;

&lt;p&gt;I would never mark a password, API key, access token, or personally identifiable value with &lt;code&gt;x-mcp-header&lt;/code&gt;. Base64 is only an encoding, and headers are visible to intermediaries and often copied into logs.&lt;/p&gt;

&lt;p&gt;The sample is also a focused conformance fixture, not a full JSON Schema 2020-12 engine or a replacement for the official SDK. Its value is keeping the sharp transport rules visible in tests. For production, use a current SDK, validate header/body equality on the server, and keep authorization tied to the authenticated principal.&lt;/p&gt;

&lt;p&gt;Which malformed schema or encoding edge case would you add to this regression set?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
