<?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>MCP Python SDK Extension Method Collisions: Fail Before the Server Starts</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Thu, 10 Sep 2026 23:09:06 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts-329l</link>
      <guid>https://dev.to/ssukhpinder/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts-329l</guid>
      <description>&lt;p&gt;MCP Python SDK extension method collisions are configuration defects, not runtime edge cases. If two extensions claim the same method—or one claims a core MCP method—the server should reject that setup before it accepts a request. I prefer making method ownership an executable contract so registration order can never decide which handler wins.&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://py.sdk.modelcontextprotocol.io/advanced/extensions/" rel="noopener noreferrer"&gt;MCP Python SDK extension documentation&lt;/a&gt; defines three useful safeguards: core methods cannot be registered as extension methods, duplicate extension methods are rejected during registration, and every binding must declare at least one supported protocol version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why collisions should fail during startup
&lt;/h2&gt;

&lt;p&gt;An extension adds vendor-specific behavior to the same dispatch table used by the rest of the server. That makes method names part of the server's public contract.&lt;/p&gt;

&lt;p&gt;Consider two independently configured extensions that both expose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;com.example/catalog.search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A last-write-wins registry would make the active handler depend on extension order. Reordering configuration could silently change request behavior without changing the client call.&lt;/p&gt;

&lt;p&gt;The safer contract is one owner per method name. In the sample, a valid extension starts normally, while a second extension claiming the same method causes &lt;code&gt;MCPServer&lt;/code&gt; construction to raise &lt;code&gt;ValueError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Core protocol methods have an even stronger boundary. A vendor extension must not replace methods such as &lt;code&gt;tools/list&lt;/code&gt;. The SDK's MCP extension core method guard rejects that binding when &lt;code&gt;MethodBinding&lt;/code&gt; is constructed.&lt;/p&gt;

&lt;p&gt;The sample targets protocol version &lt;code&gt;2026-07-28&lt;/code&gt;, announced in the project's &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;final MCP release post&lt;/a&gt;, and pins the stable &lt;a href="https://pypi.org/project/mcp/2.1.1/" rel="noopener noreferrer"&gt;&lt;code&gt;mcp==2.1.1&lt;/code&gt; package&lt;/a&gt; so the checks are reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a version-pinned &lt;code&gt;MethodBinding&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I start with a namespaced method and an explicit protocol-version set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PROTOCOL_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-28&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EXTENSION_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.example/catalog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;METHOD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.example/catalog.search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_binding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;METHOD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MethodBinding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;MethodBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SearchParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;protocol_versions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;PROTOCOL_VERSION&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 reverse-domain prefix keeps the vendor method separate from core MCP names. More importantly, &lt;code&gt;protocol_versions&lt;/code&gt; states exactly where the binding is reachable.&lt;/p&gt;

&lt;p&gt;That protocol version validation prevents a subtle configuration mistake. An empty set describes a method that cannot be used under any protocol version, so the SDK rejects it immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_unreachable_binding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MethodBinding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;MethodBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;METHOD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SearchParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;protocol_versions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;frozenset&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 valid extension returns one binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CatalogSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Extension&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXTENSION_ID&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MethodBinding&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;search_binding&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second extension deliberately returns the same method name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShadowSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Extension&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.example/catalog-shadow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MethodBinding&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;search_binding&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither class is inherently invalid in isolation. The collision appears when both are registered with one server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;MCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extension-contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;CatalogSearch&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;ShadowSearch&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;code&gt;MethodBinding&lt;/code&gt; duplicate-method boundary I want to test: the server registry sees two owners and refuses to start.&lt;/p&gt;

&lt;p&gt;Checking this boundary during construction keeps the failure close to the configuration that caused it. A deployment never reaches the point where the first unlucky request discovers an ambiguous handler. It also makes the regression test independent of extension ordering: swapping the two classes cannot turn the failure into success. In a larger server, I would keep these ownership tests beside the composition root where optional packages are assembled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify MCP Python SDK extension method collisions offline
&lt;/h2&gt;

&lt;p&gt;The runnable &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/107-mcp-extension-collisions" rel="noopener noreferrer"&gt;sample&lt;/a&gt; checks one valid path and three invalid configurations. Its valid case uses the SDK's in-memory client, so it does not open a port or require an external MCP host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_valid_server&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;advertise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXTENSION_ID&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SearchRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;SearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SearchResult&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 handler returns deterministic values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;["mcp-0", "mcp-1"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The positive request is as important as the rejection cases. It proves the namespaced method remains callable when it has one owner, the client advertises the extension identifier, and the typed result survives the same registry being guarded. Without that control case, a test could pass simply because every extension path was broken.&lt;/p&gt;

&lt;p&gt;The remaining checks construct a duplicate server, attempt to bind &lt;code&gt;tools/list&lt;/code&gt;, and create a binding with an empty version set. The verifier catches &lt;code&gt;ValueError&lt;/code&gt; only to assert the boundary; production startup should let those errors stop the process.&lt;/p&gt;

&lt;p&gt;Run the full validation with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--all-groups&lt;/span&gt;
uv lock &lt;span class="nt"&gt;--check&lt;/span&gt;
uv run ruff format &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
uv run ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
uv run mypy extension_contract.py verify.py test_extension_contract.py
uv run python &lt;span class="nt"&gt;-m&lt;/span&gt; compileall &lt;span class="nt"&gt;-q&lt;/span&gt; extension_contract.py verify.py test_extension_contract.py
uv run python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest &lt;span class="nt"&gt;-v&lt;/span&gt;
uv run python verify.py
uv run pip-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deterministic verifier reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PASS] unique vendor method starts normally
[PASS] typed request keeps the vendor method
[PASS] duplicate method fails during server construction
[PASS] core MCP method cannot be claimed
[PASS] empty protocol version set is rejected
5/5 checks passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;This sample verifies construction and in-memory request dispatch. It does not test stdio, Streamable HTTP, authentication, extension result claims, or notification bindings.&lt;/p&gt;

&lt;p&gt;It also pins exact exception-message fragments for SDK 2.1.1. If I were supporting several SDK releases, I would make the exception type and offending method the durable assertions, then keep message checks narrow enough to tolerate wording changes.&lt;/p&gt;

&lt;p&gt;These checks are most useful when a server composes extensions from multiple packages or configuration sources. For a small server with no extensions, the extra contract tests may add little value. They also complement integration tests rather than replacing transport and authorization coverage.&lt;/p&gt;

&lt;p&gt;How are you testing extension method ownership before your MCP server accepts traffic?&lt;/p&gt;

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

</description>
      <category>mcp</category>
      <category>python</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>.NET 10 Generic Math Shift Masking: Catch Overshifts That Now Wrap</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Tue, 08 Sep 2026 22:13:40 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-generic-math-shift-masking-catch-overshifts-that-now-wrap-1612</link>
      <guid>https://dev.to/ssukhpinder/net-10-generic-math-shift-masking-catch-overshifts-that-now-wrap-1612</guid>
      <description>&lt;p&gt;.NET 10 generic math shift masking changes the result of some oversized shifts on small integer types. If a helper uses &lt;code&gt;IShiftOperators&amp;lt;T, int, T&amp;gt;&lt;/code&gt;, code such as a generic &lt;code&gt;byte &amp;lt;&amp;lt; 8&lt;/code&gt; can return &lt;code&gt;1&lt;/code&gt; on .NET 10 where it returned &lt;code&gt;0&lt;/code&gt; on .NET 9. That is a narrow behavioral change, but it can matter in bit-field parsing, binary protocols, compact identifiers, and any test fixture that treats an out-of-range count as an implicit zero.&lt;/p&gt;

&lt;p&gt;I treat this as a contract problem. The runtime now behaves consistently; the application still has to decide whether an oversized count is valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why .NET 10 generic math shift masking changes results
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/generic-math" rel="noopener noreferrer"&gt;official breaking-change note&lt;/a&gt; says generic shifts now mask the shift amount as appropriate for all built-in integer types. The affected small types are &lt;code&gt;byte&lt;/code&gt;, &lt;code&gt;char&lt;/code&gt;, &lt;code&gt;sbyte&lt;/code&gt;, &lt;code&gt;short&lt;/code&gt;, and &lt;code&gt;ushort&lt;/code&gt;, and the affected operators are &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is specifically about operators dispatched through generic math. A concrete C# expression involving a &lt;code&gt;byte&lt;/code&gt; can be promoted to &lt;code&gt;int&lt;/code&gt;; a generic method constrained by &lt;code&gt;IShiftOperators&amp;lt;T, int, T&amp;gt;&lt;/code&gt; returns &lt;code&gt;T&lt;/code&gt;. That distinction is why a normal happy-path unit test may not reveal the upgrade boundary.&lt;/p&gt;

&lt;p&gt;Here is the core of the reproducer:&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="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;ShiftLeft&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&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="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IShiftOperators&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;UnsignedShiftRight&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&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="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IShiftOperators&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/generics/math" rel="noopener noreferrer"&gt;generic math guide&lt;/a&gt; explains the static interface-member model behind these constraints. The syntax did not change here. The built-in implementations did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the .NET 9 and .NET 10 boundary
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/101-generic-math-shift-masking" rel="noopener noreferrer"&gt;runnable sample&lt;/a&gt; multi-targets &lt;code&gt;net9.0&lt;/code&gt; and &lt;code&gt;net10.0&lt;/code&gt;, then runs the same cases under both installed runtimes. It checks counts equal to the type width and one greater than the width. It also includes &lt;code&gt;int &amp;lt;&amp;lt; 32&lt;/code&gt; as a control because &lt;code&gt;int&lt;/code&gt; already masks its shift count.&lt;/p&gt;

&lt;p&gt;The relevant output is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET 9:  byte-left-8=0, byte-left-9=0
.NET 10: byte-left-8=1, byte-left-9=2

.NET 9:  byte-unsigned-right-8=0
.NET 10: byte-unsigned-right-8=128

both:    int-left-32-control=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the .NET 10 &lt;code&gt;byte&lt;/code&gt; cases, a count of &lt;code&gt;8&lt;/code&gt; becomes &lt;code&gt;0&lt;/code&gt; after masking, and &lt;code&gt;9&lt;/code&gt; becomes &lt;code&gt;1&lt;/code&gt;. The value is therefore shifted by zero or one position. The .NET 9 result reflected the previous inconsistent small-integer behavior.&lt;/p&gt;

&lt;p&gt;The count is not clamped to the largest legal position. It is reduced according to the operand width, so every additional full width repeats the same positions. That makes &lt;code&gt;8&lt;/code&gt;, &lt;code&gt;16&lt;/code&gt;, and &lt;code&gt;24&lt;/code&gt; equivalent counts for an eight-bit implementation. It also explains why an overshift may produce a nonzero value rather than clearing all bits. Tests should assert the domain rule, not a vague expectation that a “large enough” shift becomes zero.&lt;/p&gt;

&lt;p&gt;For an upgrade audit, I search for &lt;code&gt;IShiftOperators&lt;/code&gt;, &lt;code&gt;IBinaryInteger&lt;/code&gt;, generic &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;/&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; helpers, and methods that accept an unvalidated count. I then prioritize small built-in types. Calls whose count is a compile-time constant below the width are not affected; input-derived counts, sentinels equal to the width, and reusable bit-packers deserve the cross-runtime test. Searching only for &lt;code&gt;byte &amp;lt;&amp;lt;&lt;/code&gt; misses helpers where &lt;code&gt;T&lt;/code&gt; hides the eventual operand type.&lt;/p&gt;

&lt;p&gt;The verifier builds both targets, requires the exact output, and fails on any unexpected exit code. It has no package references and needs no credentials, clock, random input, or network call at runtime. The associated &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/91" rel="noopener noreferrer"&gt;sample pull request&lt;/a&gt; records the complete validation commands and results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the shift-count policy explicit
&lt;/h2&gt;

&lt;p&gt;Runtime consistency is useful, but silent normalization is not always the right application rule. I prefer to name one of two policies at the boundary.&lt;/p&gt;

&lt;p&gt;For a protocol offset or serialized bit index, reject a count outside the value width:&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="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;ShiftLeftReject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IShiftOperators&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&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;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNegative&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="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNegativeOrZero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&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;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;width&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;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&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;value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;lt;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a domain where cyclic counts are intentional, normalize them yourself:&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="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;ShiftLeftModulo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IShiftOperators&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&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;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNegative&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="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNegativeOrZero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;lt;&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="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the matching built-in value width, both versions behave the same on .NET 9 and .NET 10. More importantly, a reviewer can see the policy without knowing a runtime-specific operator rule.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;width&lt;/code&gt; close to the numeric type rather than accepting an unrelated caller-provided value. In a reusable library, a type-specific helper or a small metadata table is clearer than letting every call site guess. The sample passes the width explicitly so the two policies remain visible, testable, and easy to adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use modulo masking
&lt;/h2&gt;

&lt;p&gt;Do not add &lt;code&gt;% width&lt;/code&gt; simply to preserve the new output. If an oversized count signals corrupt input, masking turns an invalid value into a plausible one. Rejection is usually safer for parsers, authorization bitsets, storage formats, and externally supplied offsets.&lt;/p&gt;

&lt;p&gt;This sample also does not define semantics for custom numeric types; their operator implementations remain their own contracts. Review rotations, sign extension, negative counts, and cryptographic code separately. A shift verifier can expose a changed result, but it cannot decide whether that result is correct for the domain.&lt;/p&gt;

&lt;p&gt;.NET 10 is a stable LTS release, and the current &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 the SDK/runtime builds used for this check. If your library targets multiple runtimes, I would keep the cross-target assertion until the oldest affected target leaves support.&lt;/p&gt;

&lt;p&gt;Would your code reject an oversized shift count, or is modulo behavior part of its contract?&lt;/p&gt;

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

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>EF Core 10 Named Default Constraints: Preview the Migration SQL Before Deploying</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Thu, 03 Sep 2026 19:35:34 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/ef-core-10-named-default-constraints-preview-the-migration-sql-before-deploying-3pma</link>
      <guid>https://dev.to/ssukhpinder/ef-core-10-named-default-constraints-preview-the-migration-sql-before-deploying-3pma</guid>
      <description>&lt;p&gt;EF Core 10 named default constraints solve an annoying SQL Server maintenance problem: database-generated names such as &lt;code&gt;DF__Jobs__Status__...&lt;/code&gt; are hard to predict in scripts and incident playbooks. The new convention gives every default a stable name. The catch is easy to miss: enabling it on an existing model makes the next migration change every default constraint in that model.&lt;/p&gt;

&lt;p&gt;That is exactly the kind of small configuration edit that deserves a migration gate. I want to know how many columns will change, what SQL will run, and whether the preview can be checked without pointing a tool at a real database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why EF Core 10 named default constraints can surprise CI
&lt;/h2&gt;

&lt;p&gt;EF Core 10 adds two naming choices for SQL Server defaults. I can pass a name directly to &lt;code&gt;HasDefaultValue&lt;/code&gt; or &lt;code&gt;HasDefaultValueSql&lt;/code&gt;, or enable the global convention:&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;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelBuilder&lt;/span&gt; &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseNamedDefaultConstraints&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&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;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasDefaultValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"queued"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedUtc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasDefaultValueSql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SYSUTCDATETIME()"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasDefaultValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated names are predictable: &lt;code&gt;DF_Jobs_Status&lt;/code&gt;, &lt;code&gt;DF_Jobs_CreatedUtc&lt;/code&gt;, and &lt;code&gt;DF_Jobs_RetryCount&lt;/code&gt;. Predictability helps when a deployment script, DBA, or rollback procedure must refer to a constraint.&lt;/p&gt;

&lt;p&gt;The global call is not metadata-only for an existing schema. Microsoft’s &lt;a href="https://learn.microsoft.com/ef/core/what-is-new/ef-core-10.0/whatsnew#custom-default-constraint-names" rel="noopener noreferrer"&gt;EF Core 10 release notes&lt;/a&gt; warn that the next migration renames every default constraint in the model. A build that only checks whether migrations compile will not show the scope clearly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview the migration SQL without SQL Server
&lt;/h2&gt;

&lt;p&gt;I built the &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/114-efcore-named-default-constraints" rel="noopener noreferrer"&gt;runnable sample&lt;/a&gt; around two committed migrations. &lt;code&gt;InitialSchema&lt;/code&gt; represents the old model with three unnamed defaults. &lt;code&gt;NameDefaultConstraints&lt;/code&gt; is scaffolded after adding the global convention.&lt;/p&gt;

&lt;p&gt;The second migration contains three &lt;code&gt;AlterColumn&lt;/code&gt; operations. Each operation keeps the same CLR type and default value, but adds the relational annotation:&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;migrationBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AlterColumn&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Jobs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"nvarchar(32)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"queued"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;oldClrType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;oldType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"nvarchar(32)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;oldMaxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;oldDefaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"queued"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Annotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Relational:DefaultConstraintName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"DF_Jobs_Status"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generating a migration script does not require a live connection. The SQL Server provider can translate the committed operations locally:&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;tool&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;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;tool&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="nx"&gt;dotnet-ef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;migrations&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;script&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="nx"&gt;InitialSchema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;NameDefaultConstraints&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;--no-build&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The preview reveals more than the C# migration name suggests. For every affected column, EF queries &lt;code&gt;sys.default_constraints&lt;/code&gt; to discover the opaque old name, drops that constraint, alters the column, and adds the predictable constraint. In this sample that sequence happens three times. The script does not drop a table or column, but it is still schema work worth reviewing for locks and deployment duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the migration preview into a deterministic gate
&lt;/h2&gt;

&lt;p&gt;Reading a script once is useful; making the expectation executable is better. The sample resolves EF’s migrations services without opening the placeholder connection. It inspects the second migration and then generates the upgrade SQL twice from fresh contexts:&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;alteredDefaults&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;namingMigration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpOperations&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AlterColumnOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;alteredDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"enabling the convention changes all three defaults"&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;CountOccurrences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"DROP CONSTRAINT"&lt;/span&gt;&lt;span class="p"&gt;)&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="s"&gt;"SQL drops all three existing default constraints"&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 also checks the three exact &lt;code&gt;DF_...&lt;/code&gt; names, confirms three lookups in &lt;code&gt;sys.default_constraints&lt;/code&gt;, rejects &lt;code&gt;DROP TABLE&lt;/code&gt; and &lt;code&gt;DROP COLUMN&lt;/code&gt;, and confirms the placeholder connection remains closed. Five repeated runs produce identical output. The complete commands and expected nine checks are in the sample README, and the &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/104" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; preserves the reviewed diff.&lt;/p&gt;

&lt;p&gt;For CI, I prefer these semantic assertions over a snapshot of the entire SQL file. Provider patches may adjust whitespace, batch separators, or local variable names without changing the operation. Counting the affected defaults and checking the exact new constraint names keeps the gate focused. If a fourth default is added later, the test fails deliberately and asks the reviewer to update the expected scope instead of accepting a wider migration by accident.&lt;/p&gt;

&lt;p&gt;This is intentionally a contract test, not an integration test. It answers “what does this migration plan to do?” without requiring SQL Server, credentials, or a disposable database. I would still apply the migration to a representative database before production because an offline script cannot predict lock duration, workload contention, or provider permissions. Microsoft’s &lt;a href="https://learn.microsoft.com/ef/core/managing-schemas/migrations/managing" rel="noopener noreferrer"&gt;migration management guidance&lt;/a&gt; makes the same broader point: generated migrations should be reviewed and customized when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would avoid the global convention
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;UseNamedDefaultConstraints()&lt;/code&gt; is convenient for a new schema because the names exist from the first migration. It is also reasonable for a small existing schema when the generated changes have been reviewed and scheduled.&lt;/p&gt;

&lt;p&gt;For a large or busy database, I would consider a staged rollout. EF Core 10’s &lt;a href="https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.sqlservermodelbuilderextensions.usenameddefaultconstraints?view=efcore-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;UseNamedDefaultConstraints&lt;/code&gt; API&lt;/a&gt; is SQL Server-specific, and the property-level overloads let me name selected defaults first. That keeps an unrelated model edit from producing a broad migration.&lt;/p&gt;

&lt;p&gt;I would also avoid treating the operation count as a performance estimate. Three safe-looking changes in a demo say nothing about hundreds of defaults on hot production tables. The gate protects scope and intent; database rehearsal protects the rollout.&lt;/p&gt;

&lt;p&gt;Would you enable named defaults globally, or introduce explicit names one property at a time?&lt;/p&gt;

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

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>database</category>
      <category>sqlserver</category>
    </item>
    <item>
      <title>OpenAI Responses API previous_response_id Instructions: Repeat Policy on Every Turn</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:30:51 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/openai-responses-api-previousresponseid-instructions-repeat-policy-on-every-turn-1c88</link>
      <guid>https://dev.to/ssukhpinder/openai-responses-api-previousresponseid-instructions-repeat-policy-on-every-turn-1c88</guid>
      <description>&lt;p&gt;OpenAI Responses API previous_response_id instructions have a counterintuitive boundary: the response ID carries conversation state forward, but it does not carry the prior top-level &lt;code&gt;instructions&lt;/code&gt;. If my application uses that field for output format, tool rules, or safety constraints, turn two can quietly run without the policy I expected.&lt;/p&gt;

&lt;p&gt;I prefer to make that boundary visible in request construction. A small guard can require instructions on every policy-bound turn, while still allowing the application to replace them deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenAI Responses API previous_response_id instructions disappear
&lt;/h2&gt;

&lt;p&gt;The Responses API supports several state strategies. With &lt;code&gt;previous_response_id&lt;/code&gt;, a new response can continue from an earlier one without resending the full conversation. OpenAI's &lt;a href="https://developers.openai.com/api/docs/guides/conversation-state" rel="noopener noreferrer"&gt;conversation state guide&lt;/a&gt; shows that chaining pattern directly.&lt;/p&gt;

&lt;p&gt;That convenience does not make every request option persistent. The &lt;a href="https://developers.openai.com/api/reference/cli/resources/responses/methods/create" rel="noopener noreferrer"&gt;create response reference&lt;/a&gt; says that earlier &lt;code&gt;instructions&lt;/code&gt; are not carried into a request that uses &lt;code&gt;previous_response_id&lt;/code&gt;. The new request can supply the same instructions, different instructions, or none.&lt;/p&gt;

&lt;p&gt;This is useful when I want to change behavior mid-thread. It is risky when the omission is accidental.&lt;/p&gt;

&lt;p&gt;Consider an application that expects every answer to follow a JSON 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;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;"Answer in JSON with keys summary and risks."&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;first&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;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpt-5.6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Review the deployment plan."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;store&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A continuation that sends only the new input and the earlier response ID has conversation context, but not that top-level policy. I therefore build the second turn with the policy beside the continuation ID:&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;second&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;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpt-5.6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Now focus on rollback."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;store&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;previous_response_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firstResponseId&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction is simple: response lineage and request instructions solve different problems. I do not treat one as an implied copy of the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the continuation contract explicit
&lt;/h2&gt;

&lt;p&gt;I encode the rule in one request builder instead of relying on every call site to remember it. The builder accepts the model, input, instructions, and one optional state reference:&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;record&lt;/span&gt; &lt;span class="nc"&gt;ResponsesTurn&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;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Instructions&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;PreviousResponseId&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ConversationId&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 guard rejects blank instructions before any transport code runs:&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instructions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an application invariant, not an API requirement. The API's &lt;code&gt;instructions&lt;/code&gt; field is optional. My builder makes it mandatory because this particular application says every turn must carry policy.&lt;/p&gt;

&lt;p&gt;I also reject a payload that combines &lt;code&gt;previous_response_id&lt;/code&gt; with &lt;code&gt;conversation&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PreviousResponseId&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;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConversationId&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="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;"previous_response_id and conversation cannot be sent together."&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;That mirrors the documented request contract and keeps the state strategy unambiguous. If I decide to move from response chaining to a durable Conversation object, I make that a deliberate code change rather than emitting both fields and waiting for an HTTP 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the chained payload offline
&lt;/h2&gt;

&lt;p&gt;The merged &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/096-openai-chained-instructions" rel="noopener noreferrer"&gt;sample on &lt;code&gt;main&lt;/code&gt;&lt;/a&gt; is a dependency-free .NET 10 executable. It builds three fixed payloads: a first turn, a continuation that repeats the policy, and a continuation that intentionally replaces it.&lt;/p&gt;

&lt;p&gt;Its verifier checks seven behaviors. It confirms that the first request carries &lt;code&gt;instructions&lt;/code&gt; and &lt;code&gt;store: true&lt;/code&gt;; the chained request carries both the policy and its fixture response ID; replacement instructions survive serialization; blank instructions and IDs fail locally; the two state mechanisms cannot be combined; and identical inputs produce byte-identical JSON.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/86" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; records the exact validation commands and results. Restore, formatting, Release build, package inventory, and vulnerability audit pass. Five repeated verifier runs produce identical output. No account, API key, paid request, or model call is needed.&lt;/p&gt;

&lt;p&gt;I like this test because it validates the part my code controls. A model call would add latency and output variability without proving that every future call site constructs the payload correctly. The offline fixture makes omission a deterministic build failure.&lt;/p&gt;

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

&lt;p&gt;This sample is not an OpenAI SDK replacement. It writes a narrow JSON shape with string input, sets &lt;code&gt;store: true&lt;/code&gt;, and stops before HTTP. A production integration should use a current supported SDK or a complete HTTP client, handle API errors, and test its actual serialization boundary.&lt;/p&gt;

&lt;p&gt;The guard also does not fit every state strategy. With &lt;code&gt;store: false&lt;/code&gt;, I need to replay the relevant returned items instead of chaining a stored response. A durable Conversation object has its own lifecycle and cannot be combined with &lt;code&gt;previous_response_id&lt;/code&gt;. Stored response objects also have retention and token-billing implications described in the conversation state guide, so state selection deserves an explicit product decision.&lt;/p&gt;

&lt;p&gt;Finally, model instructions are not authorization. I still enforce permissions, tenant boundaries, validation, and destructive-action checks in application code. Repeating policy keeps the model request consistent; it does not turn prose into a security boundary.&lt;/p&gt;

&lt;p&gt;What invariant would you add before a chained request leaves your code?&lt;/p&gt;

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

</description>
      <category>openai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <item>
      <title>.NET 10 InlineArray Explicit Size: Fix TypeLoadException with a Wrapper</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Wed, 02 Sep 2026 02:49:54 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-inlinearray-explicit-size-fix-typeloadexception-with-a-wrapper-2ip0</link>
      <guid>https://dev.to/ssukhpinder/net-10-inlinearray-explicit-size-fix-typeloadexception-with-a-wrapper-2ip0</guid>
      <description>&lt;p&gt;.NET 10 InlineArray explicit Size is a small compatibility change with an unusually late failure mode. A project can compile, start normally, and then throw &lt;code&gt;TypeLoadException&lt;/code&gt; when the runtime first loads a legacy value type that combines &lt;code&gt;InlineArrayAttribute&lt;/code&gt; with &lt;code&gt;StructLayoutAttribute.Size&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I would not fix this by deleting the size blindly. The explicit byte count probably represented an interop assumption, so the safer job is to identify what the size meant and move that layout intent to an unambiguous wrapper.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/112-inlinearray-explicit-size" rel="noopener noreferrer"&gt;complete runnable sample&lt;/a&gt; creates the invalid metadata in memory, proves that .NET 10 rejects it, and verifies two supported replacements without a native library or external service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the loader rejects the old shape
&lt;/h2&gt;

&lt;p&gt;An inline array already defines its storage: the runtime repeats the struct's single field for the length supplied to &lt;code&gt;InlineArrayAttribute&lt;/code&gt;. This declaration therefore describes eight consecutive &lt;code&gt;int&lt;/code&gt; values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Int8InlineArray&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_element0&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;On a normal .NET 10 target, that type occupies 32 bytes. Adding a second size declaration to the same type creates two competing descriptions of its layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;StructLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LayoutKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;LegacyInt8InlineArray&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_element0&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;Microsoft documents this as a &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/inlinearray-explicit-size-disallowed" rel="noopener noreferrer"&gt;.NET 10 binary compatibility change&lt;/a&gt;. Earlier runtimes allowed implementation-specific behavior. .NET 10 rejects the combination when the type is loaded because any interpretation of the duplicate size information would be ambiguous.&lt;/p&gt;

&lt;p&gt;That timing matters. This is not necessarily a compiler diagnostic in source you control. The shape can already exist in a referenced assembly, and a cold path involving reflection, interop registration, or serialization might be the first code that forces the type to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the TypeLoadException safely
&lt;/h2&gt;

&lt;p&gt;I wanted the sample to demonstrate the loader rule without committing an intentionally broken assembly. It uses &lt;code&gt;Reflection.Emit&lt;/code&gt; to create the old metadata at runtime:&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;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DefineType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"LegacyInt8InlineArray"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TypeAttributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotPublic&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;TypeAttributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sealed&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;TypeAttributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SequentialLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ValueType&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;PackingSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unspecified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;typesize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32&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;constructor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InlineArrayAttribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConstructor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)])!;&lt;/span&gt;

&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetCustomAttribute&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;CustomAttributeBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DefineField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_element0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;FieldAttributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Private&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateTypeInfo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// TypeLoadException on .NET 10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier catches the exception and checks its type. It then uses &lt;code&gt;Unsafe.SizeOf&amp;lt;T&amp;gt;()&lt;/code&gt; and fixed values to prove that each replacement is 32 bytes and still supports all eight indexed values. Running the check repeatedly produces identical output, so it works well as an upgrade regression test.&lt;/p&gt;

&lt;p&gt;There is a useful testing detail here: keep the invalid shape behind an isolated loader boundary. If I referenced a broken type directly throughout the test executable, the runtime could force it while compiling a method, before the assertion reached its intended &lt;code&gt;try&lt;/code&gt; block. Emitting one fixture keeps the failure local and makes the expected exception part of the test result instead of a process-level surprise.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.inlinearrayattribute?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;InlineArrayAttribute&lt;/code&gt; documentation&lt;/a&gt; is useful context here: the attribute represents sequentially replicated storage. It should be the only mechanism defining the inline array's total repeated shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix .NET 10 InlineArray explicit Size with a wrapper
&lt;/h2&gt;

&lt;p&gt;The correct replacement depends on the original intent.&lt;/p&gt;

&lt;p&gt;If 32 bytes describes the whole native buffer, wrap the inline array and put &lt;code&gt;Size&lt;/code&gt; on the outer type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Int8InlineArray&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_element0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;StructLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LayoutKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;WholeArrayWrapper&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Int8InlineArray&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the explicit size describes one native element, move it to the element and let the inline array repeat that type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;StructLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LayoutKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;SizedElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&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;get&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;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;SizedElementArray&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;SizedElement&lt;/span&gt; &lt;span class="n"&gt;_element0&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 separation makes the contract reviewable: &lt;code&gt;InlineArray(8)&lt;/code&gt; owns repetition, while &lt;code&gt;StructLayout.Size&lt;/code&gt; owns either the wrapper boundary or an individual element. The &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.size?view=net-10.0" rel="noopener noreferrer"&gt;StructLayout size reference&lt;/a&gt; also warns that an explicit size must be at least as large as the type's fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would gate during an upgrade
&lt;/h2&gt;

&lt;p&gt;I would start by scanning source and generated interop code for types that contain both attributes. Then I would load every relevant plugin or interop assembly in a .NET 10 test process, because a successful build does not prove that every value type can be loaded.&lt;/p&gt;

&lt;p&gt;That load test should use the same deployment shape as production. Trimming, ahead-of-time compilation, plugin discovery, and architecture-specific assemblies can force types in a different order from a normal developer run. I would also make the test enumerate known boundary types explicitly; waiting for incidental application coverage can leave a rarely used native message or device structure unchecked.&lt;/p&gt;

&lt;p&gt;For each hit, I would record whether the byte count belongs to the element, the repeated buffer, or an enclosing native record. After moving the attribute, I would keep three checks close to the boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the managed size is the expected fixed value;&lt;/li&gt;
&lt;li&gt;representative values survive indexed reads and writes; and&lt;/li&gt;
&lt;li&gt;the real native declaration agrees on field order, packing, and alignment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample and its &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/102" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; cover the first two checks. They do not prove a platform-specific ABI, and &lt;code&gt;Reflection.Emit&lt;/code&gt; is only a controlled way to exercise the loader rule. If the type crosses a P/Invoke boundary, test against the actual native library and every supported architecture. If no native or serialized contract depends on an explicit byte count, a plain inline array may be all you need.&lt;/p&gt;

&lt;p&gt;How are you auditing layout-sensitive types before your .NET 10 rollout?&lt;/p&gt;

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

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>.NET 10 LDAP VlvRequestControl Validation: Stop Sending Replacement Bytes</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Tue, 01 Sep 2026 17:59:10 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/net-10-ldap-vlvrequestcontrol-validation-stop-sending-replacement-bytes-4ilf</link>
      <guid>https://dev.to/ssukhpinder/net-10-ldap-vlvrequestcontrol-validation-stop-sending-replacement-bytes-4ilf</guid>
      <description>&lt;p&gt;When I audit an LDAP migration, &lt;code&gt;.NET 10 LDAP VlvRequestControl validation&lt;/code&gt; is the kind of small compatibility change I want covered by a focused test. On .NET 9, an unpaired UTF-16 surrogate in a virtual-list-view target can be encoded as the UTF-8 replacement sequence &lt;code&gt;EF BF BD&lt;/code&gt;. On .NET 10, the same malformed target is rejected with &lt;code&gt;EncoderFallbackException&lt;/code&gt; before LDAP I/O.&lt;/p&gt;

&lt;p&gt;That stricter behavior is safer because a client no longer sends bytes that differ from the string it was given. It is still a breaking change if an application has been allowing malformed strings to reach &lt;code&gt;VlvRequestControl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why .NET 10 LDAP VlvRequestControl validation matters
&lt;/h2&gt;

&lt;p&gt;LDAP virtual list view controls let a client request a window around a target in a sorted result set. The &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.protocols.vlvrequestcontrol?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;VlvRequestControl&lt;/code&gt; API&lt;/a&gt; accepts that target as a .NET string, which must be converted from UTF-16 to UTF-8 when the control value is serialized.&lt;/p&gt;

&lt;p&gt;A valid surrogate pair represents one Unicode scalar value and should encode normally. A lone high surrogate such as &lt;code&gt;\uD800&lt;/code&gt;, or a lone low surrogate such as &lt;code&gt;\uDC00&lt;/code&gt;, is not valid Unicode text. The older fallback behavior substitutes &lt;code&gt;U+FFFD&lt;/code&gt;, whose UTF-8 bytes are &lt;code&gt;EF BF BD&lt;/code&gt;. That keeps serialization moving, but it silently changes the VLV target and may seek to an unintended position.&lt;/p&gt;

&lt;p&gt;Microsoft documents the strict encoding behavior in the &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/ldap-directorycontrol-parsing" rel="noopener noreferrer"&gt;.NET 10 LDAP compatibility note&lt;/a&gt;. I treat the resulting exception as input validation surfacing at the correct boundary, not as an LDAP server failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a deterministic multi-target probe
&lt;/h2&gt;

&lt;p&gt;I use one project targeting both runtimes, with package versions pinned per target. That keeps the comparison explicit: &lt;code&gt;System.DirectoryServices.Protocols&lt;/code&gt; 9.0.19 for &lt;code&gt;net9.0&lt;/code&gt;, and the stable &lt;a href="https://www.nuget.org/packages/System.DirectoryServices.Protocols/10.0.11" rel="noopener noreferrer"&gt;&lt;code&gt;System.DirectoryServices.Protocols&lt;/code&gt; 10.0.11 package&lt;/a&gt; for &lt;code&gt;net10.0&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&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;TargetFrameworks&amp;gt;&lt;/span&gt;net9.0;net10.0&lt;span class="nt"&gt;&amp;lt;/TargetFrameworks&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;ItemGroup&lt;/span&gt; &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"'$(TargetFramework)' == 'net9.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.DirectoryServices.Protocols"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.0.19"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&lt;/span&gt; &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"'$(TargetFramework)' == 'net10.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.DirectoryServices.Protocols"&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;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 probe covers four inputs: plain ASCII, a valid surrogate pair, an unpaired high surrogate, and an unpaired low surrogate. It calls &lt;code&gt;GetValue()&lt;/code&gt; directly, so it exercises the control's BER serialization without opening an LDAP connection. For a successful encoding, it also checks the serialized bytes for &lt;code&gt;EF BF BD&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.DirectoryServices.Protocols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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;"ASCII"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"smith"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Valid pair"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"\uD83D\uDE80"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Unpaired high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"\uD800"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Unpaired low"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"\uDC00"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;control&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;VlvRequestControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&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;encoded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValue&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;hasReplacement&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;encoded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EFBFBD"&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="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&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;: encoded; replacement=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hasReplacement&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;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EncoderFallbackException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&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;: EncoderFallbackException"&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 repository sample turns those observations into six assertions per target rather than relying on visual inspection. Its verifier runs both targets twice and also checks that repeated output is byte-for-byte identical:&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;\LdapVlvValidation.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--nologo&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;\LdapVlvValidation.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="nt"&gt;--nologo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;powershell.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoProfile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NonInteractive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Bypass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".\verify.ps1"&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 finishes with &lt;code&gt;Summary: 5/5 passed&lt;/code&gt;. No directory, credentials, or runtime network access is required. The complete runnable sample is on &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/109-ldap-vlv-validation" rel="noopener noreferrer"&gt;&lt;code&gt;main&lt;/code&gt;&lt;/a&gt;, and the focused change is recorded in the &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/99" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the result into a migration guardrail
&lt;/h2&gt;

&lt;p&gt;The expected split is precise. Both targets accept ASCII and the valid surrogate pair without replacement bytes. The .NET 9 target accepts each unpaired surrogate and the serialized control contains &lt;code&gt;EF BF BD&lt;/code&gt;. The .NET 10 target rejects each malformed value with &lt;code&gt;EncoderFallbackException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That makes this test useful during a .NET 9 to .NET 10 LDAP migration. If the modern target fails, the application has exposed malformed UTF-16 that used to be rewritten silently. I would trace the value back to its source and validate there, while retaining the boundary test so a future refactor cannot restore permissive behavior accidentally.&lt;/p&gt;

&lt;p&gt;I would not "fix" the exception by replacing invalid code units myself. Replacement still changes the requested target. If the input comes from an API, queue, file, or user interface, rejecting it with a clear validation error preserves the distinction between invalid text and an LDAP connectivity problem.&lt;/p&gt;

&lt;p&gt;Where I own the caller, I catch &lt;code&gt;EncoderFallbackException&lt;/code&gt; at the validation boundary, translate it into a domain-level input error, and keep it out of generic retry logic. Retrying cannot repair malformed text, and treating the exception as transient can hide which record or request produced the value. I also avoid logging the malformed value itself; a safe source identifier and the validation category are enough to diagnose the path.&lt;/p&gt;

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

&lt;p&gt;This sample verifies local control serialization only. It does not prove that a directory server supports virtual list view, that the server's sort rules match application expectations, or that paging and authorization are configured correctly. Those concerns need integration tests against the actual directory environment.&lt;/p&gt;

&lt;p&gt;It also targets a narrow compatibility boundary. If an application never constructs &lt;code&gt;VlvRequestControl&lt;/code&gt;, this specific regression test adds little value. A broader Unicode-validation policy may be a better investment for systems accepting text from many sources.&lt;/p&gt;

&lt;p&gt;For applications that do use VLV targets, though, the offline test is fast, deterministic, and credential-free. It identifies the exact runtime change and keeps malformed input from being mistaken for a server-side LDAP issue.&lt;/p&gt;

&lt;p&gt;What edge-case strings are you adding to your .NET 10 migration tests?&lt;/p&gt;

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

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
      <category>debugging</category>
    </item>
    <item>
      <title>OpenAI Usage API api_key_id: Reconcile Tokens and Costs by Key</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Mon, 31 Aug 2026 15:38:10 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/openai-usage-api-apikeyid-reconcile-tokens-and-costs-by-key-4jmo</link>
      <guid>https://dev.to/ssukhpinder/openai-usage-api-apikeyid-reconcile-tokens-and-costs-by-key-4jmo</guid>
      <description>&lt;p&gt;OpenAI Usage API &lt;code&gt;api_key_id&lt;/code&gt; grouping solves a practical reporting gap: I can see which API key produced completion-token activity and which key accumulated cost. The tricky part is not making the two requests. It is joining their daily buckets without dropping unattributed or unmatched data.&lt;/p&gt;

&lt;p&gt;I want a reconciliation report to expose gaps, not smooth them over. A missing cost row, a cost-only row, or a null key ID can each be useful evidence. This pattern keeps those cases visible with a deterministic .NET sample that needs no credentials or paid calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenAI Usage API &lt;code&gt;api_key_id&lt;/code&gt; needs a full-outer join
&lt;/h2&gt;

&lt;p&gt;OpenAI's &lt;a href="https://developers.openai.com/api/docs/changelog" rel="noopener noreferrer"&gt;August 4, 2026 API changelog&lt;/a&gt; added API-key filtering and grouping to the usage and cost APIs. That gives both responses a shared operational dimension, but it does not make them identical datasets.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.openai.com/api/reference/python/resources/admin/subresources/organization/subresources/usage/methods/completions" rel="noopener noreferrer"&gt;completions usage endpoint&lt;/a&gt; reports measures such as input tokens, output tokens, and model requests. Its &lt;code&gt;api_key_id&lt;/code&gt; can be null. The &lt;a href="https://developers.openai.com/api/reference/python/resources/admin/subresources/organization/subresources/usage/methods/costs" rel="noopener noreferrer"&gt;costs endpoint&lt;/a&gt; returns monetary amounts and currency, also with a nullable API-key dimension.&lt;/p&gt;

&lt;p&gt;An inner join would retain only rows present in both responses. That is attractive for a tidy chart, but unsafe for reconciliation. It can hide a key that has token usage but no matching cost row, a key with cost but no completion row, or an unattributed bucket.&lt;/p&gt;

&lt;p&gt;I use a full-outer join keyed by &lt;code&gt;(start_time, end_time, api_key_id)&lt;/code&gt; instead. Null or blank IDs become an explicit display value such as &lt;code&gt;&amp;lt;unattributed&amp;gt;&lt;/code&gt;; they do not disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query both APIs at the same daily grain
&lt;/h2&gt;

&lt;p&gt;The Costs API supports daily buckets, so I request &lt;code&gt;bucket_width=1d&lt;/code&gt; from both endpoints. I also group by the same single dimension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /v1/organization/usage/completions
    ?start_time=...
    &amp;amp;end_time=...
    &amp;amp;bucket_width=1d
    &amp;amp;group_by=api_key_id

GET /v1/organization/costs
    ?start_time=...
    &amp;amp;end_time=...
    &amp;amp;bucket_width=1d
    &amp;amp;group_by=api_key_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both resources paginate with &lt;code&gt;has_more&lt;/code&gt; and &lt;code&gt;next_page&lt;/code&gt;. I keep requesting pages until &lt;code&gt;has_more&lt;/code&gt; is false. If a response says more data exists but omits its cursor, I fail the report rather than accepting a partial period. I also reject a repeated cursor to prevent a stuck pagination loop.&lt;/p&gt;

&lt;p&gt;This alignment matters. Joining hourly usage against daily cost would manufacture mismatches. Adding &lt;code&gt;model&lt;/code&gt; to only the usage grouping would make its row grain incompatible with the cost side. For this report, both sources must resolve to one row per UTC day and API-key ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile tokens and costs without inventing a price
&lt;/h2&gt;

&lt;p&gt;After loading every page, the implementation creates separate indexes for usage and cost. It then unions their keys and assigns a status to every row:&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;keys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;costs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distinct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiKeyId&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;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasUsage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasCost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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;true&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;ReconciliationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Matched&lt;/span&gt;&lt;span class="p"&gt;,&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;false&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;ReconciliationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UsageOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&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;ReconciliationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CostOnly&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The indexes reject duplicate day/key pairs. That catches accidental extra grouping dimensions or repeated pages before they inflate totals. The cost index also uses &lt;code&gt;decimal&lt;/code&gt;, retains the returned currency, and refuses to combine multiple currencies in one report.&lt;/p&gt;

&lt;p&gt;The important boundary is that token counts and billed amount remain separate measures. I do not multiply tokens by a model price and compare that estimate with the Costs API. A cost bucket may contain line items beyond completion tokens, so equality would be an unsupported assumption.&lt;/p&gt;

&lt;p&gt;The runnable &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/113-openai-api-key-usage-costs" rel="noopener noreferrer"&gt;sample on &lt;code&gt;main&lt;/code&gt;&lt;/a&gt; uses two synthetic pages from each API. Its output contains four matched rows, one usage-only row, and one cost-only row. It also preserves a null key bucket as &lt;code&gt;&amp;lt;unattributed&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nine offline checks verify pagination, the six-row full-outer result, status counts, null handling, separate token and cost measures, duplicate rejection, currency validation, and daily bucket alignment. The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/103" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; records the exact restore, format, build, run, and dependency checks.&lt;/p&gt;

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

&lt;p&gt;API-key grouping is useful for operational ownership, migration tracking, and coarse chargeback. It is not request-level attribution. If one key serves several products or customers, add your own request metadata and internal ledger at call time. Do not expect the organization usage report to reconstruct that boundary later.&lt;/p&gt;

&lt;p&gt;A live collector also needs an organization Admin API key. Keep it in a secret manager or environment variable, never in source, logs, or fixtures. The sample avoids that risk by making no network requests; its identifiers and amounts are synthetic.&lt;/p&gt;

&lt;p&gt;For a quick manual check, the dashboard may be enough. For repeatable reporting, I prefer the API plus explicit failure states: incomplete pagination, unmatched rows, unattributed activity, duplicate dimensions, and mixed currency should all be visible before anyone trusts a total.&lt;/p&gt;

&lt;p&gt;Which gap would you alert on first: usage-only, cost-only, or unattributed?&lt;/p&gt;

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

</description>
      <category>openai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <item>
      <title>MCP C# Task Polling: Stop Infinite input_required Loops</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Mon, 31 Aug 2026 02:15:50 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/mcp-c-task-polling-stop-infinite-inputrequired-loops-3nnk</link>
      <guid>https://dev.to/ssukhpinder/mcp-c-task-polling-stop-infinite-inputrequired-loops-3nnk</guid>
      <description>&lt;p&gt;MCP Tasks let a tool finish asynchronously, but they also introduce a failure mode that a normal request timeout does not describe well: the task is alive, yet every poll returns the same &lt;code&gt;input_required&lt;/code&gt; request. For MCP C# task polling, I want a bounded definition of “no progress,” not an endless loop or a user prompt that appears again and again.&lt;/p&gt;

&lt;p&gt;The stable C# Tasks extension already provides that guard. The key is to set &lt;code&gt;maxConsecutiveStuckPolls&lt;/code&gt; deliberately and test what happens when a server never advances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP C# task polling can stall
&lt;/h2&gt;

&lt;p&gt;In the MCP &lt;code&gt;2026-07-28&lt;/code&gt; Tasks extension, a tool call can return a task instead of its final tool result. The client then uses &lt;code&gt;tasks/get&lt;/code&gt; until the task completes, fails, is cancelled, or asks for input.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;input_required&lt;/code&gt; result contains keyed requests. A simplified response 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;"taskId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"task-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;"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;"input_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pollInterval"&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;"inputRequests"&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;"approval"&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;"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;"elicitation/create"&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;The key matters. If the next poll returns &lt;code&gt;approval&lt;/code&gt; again, it is not a new question. Presenting it twice can produce duplicate confirmations or conflicting responses. Polling forever is not better; it hides a server that has stopped making useful progress.&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://csharp.sdk.modelcontextprotocol.io/v2/concepts/tasks/tasks.html" rel="noopener noreferrer"&gt;C# SDK Tasks guide&lt;/a&gt; says &lt;code&gt;CallToolWithPollingAsync&lt;/code&gt; deduplicates input-request keys. It also detects repeated &lt;code&gt;input_required&lt;/code&gt; polls that contain no new keys, makes a best-effort &lt;code&gt;tasks/cancel&lt;/code&gt; call, and throws &lt;code&gt;McpException&lt;/code&gt;. The default stuck-poll threshold is 60.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a bound on MCP C# task polling
&lt;/h2&gt;

&lt;p&gt;The extension method keeps the policy close to the call:&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;try&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;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallToolWithPollingAsync&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;CallToolRequestParams&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;"long-running-tool"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;maxConsecutiveStuckPolls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Consume the completed tool result.&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;McpException&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MCP task stopped making progress"&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;I use &lt;code&gt;3&lt;/code&gt; in a fast verifier, not as a universal production value. The practical time bound is approximately the threshold multiplied by the server's poll interval. A task polled every second and a task polled every 30 seconds should not automatically share the same threshold.&lt;/p&gt;

&lt;p&gt;This guard complements the caller's &lt;code&gt;CancellationToken&lt;/code&gt;. Caller cancellation answers “does my operation still need this result?” The stuck-poll guard answers “is the server returning any new work or state?” Those are different decisions, and keeping both makes the failure easier to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the repeated input_required loop offline
&lt;/h2&gt;

&lt;p&gt;My &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/111-mcp-stuck-task-polling" rel="noopener noreferrer"&gt;complete sample on &lt;code&gt;main&lt;/code&gt;&lt;/a&gt; uses &lt;code&gt;ModelContextProtocol.Extensions.Tasks&lt;/code&gt; 2.2.0 and an in-memory transport. It advertises MCP &lt;code&gt;2026-07-28&lt;/code&gt;, returns a task from &lt;code&gt;tools/call&lt;/code&gt;, and then returns the same &lt;code&gt;approval&lt;/code&gt; request key on every &lt;code&gt;tasks/get&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;The elicitation handler declines the request and counts how often it runs:&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;Handlers&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;McpClientHandlers&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ElicitationHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;elicitationCalls&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;ValueTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&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;ElicitResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"decline"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a stuck limit of three, the observed contract is precise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The handler runs once for the &lt;code&gt;approval&lt;/code&gt; key.&lt;/li&gt;
&lt;li&gt;The client sends one &lt;code&gt;tasks/update&lt;/code&gt; containing &lt;code&gt;approval: decline&lt;/code&gt; for &lt;code&gt;task-1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;There are four &lt;code&gt;tasks/get&lt;/code&gt; calls: one that introduces the key, then three with no new key.&lt;/li&gt;
&lt;li&gt;The client sends one best-effort &lt;code&gt;tasks/cancel&lt;/code&gt; for &lt;code&gt;task-1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The call throws &lt;code&gt;McpException&lt;/code&gt; instead of polling again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The verifier runs twice and compares the output byte for byte. It requires no MCP host, model account, credentials, clock, random values, or runtime network access. The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/101" rel="noopener noreferrer"&gt;merged pull request&lt;/a&gt; also records the exact restore, format, build, run, package, and vulnerability-audit commands.&lt;/p&gt;

&lt;p&gt;The package used here is the stable &lt;a href="https://www.nuget.org/packages/ModelContextProtocol.Extensions.Tasks/2.2.0" rel="noopener noreferrer"&gt;ModelContextProtocol.Extensions.Tasks 2.2.0 release&lt;/a&gt;. Tasks are part of the final MCP &lt;code&gt;2026-07-28&lt;/code&gt; extension model; the &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;official release notes&lt;/a&gt; explain the poll-based lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the threshold—and know its limits
&lt;/h2&gt;

&lt;p&gt;A low threshold is useful in a unit test because it turns a potential hang into a quick deterministic failure. In production, the same value could cancel a healthy task while a person is considering an approval prompt. I would choose it from the expected poll interval, normal response latency, and the cost of leaving remote work active.&lt;/p&gt;

&lt;p&gt;Cancellation is cooperative and eventually consistent. A successful &lt;code&gt;tasks/cancel&lt;/code&gt; response does not prove that remote work stopped at that exact instant, so callers must tolerate a late state transition and avoid assuming rollback.&lt;/p&gt;

&lt;p&gt;This pattern is also not a replacement for an overall deadline, retry policy, or server-side task expiry. It specifically protects the polling loop when &lt;code&gt;input_required&lt;/code&gt; repeats without a new key. Log task IDs and state transitions for diagnosis, but keep elicitation answers and credentials out of logs.&lt;/p&gt;

&lt;p&gt;What stuck-poll threshold fits your server's poll interval and expected human response time?&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>OpenAI Responses API `user` Migration: Split Safety from Prompt Caching</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:15:42 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/openai-responses-api-user-migration-split-safety-from-prompt-caching-32io</link>
      <guid>https://dev.to/ssukhpinder/openai-responses-api-user-migration-split-safety-from-prompt-caching-32io</guid>
      <description>&lt;p&gt;The OpenAI Responses API &lt;code&gt;user&lt;/code&gt; migration is easy to misread as a one-field rename. It is actually a split. The deprecated field mixed end-user safety attribution with prompt-cache routing, while the current request contract provides &lt;code&gt;safety_identifier&lt;/code&gt; and &lt;code&gt;prompt_cache_key&lt;/code&gt; for those separate jobs.&lt;/p&gt;

&lt;p&gt;I would rather make that distinction explicit in one request builder than scatter it across call sites. The result is easier to review, keeps raw identity out of the payload, and can be verified without sending a model request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenAI Responses API &lt;code&gt;user&lt;/code&gt; migration is a split
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developers.openai.com/api/reference/cli/resources/responses/methods/create" rel="noopener noreferrer"&gt;Responses API create reference&lt;/a&gt; marks &lt;code&gt;user&lt;/code&gt; as deprecated and says it is being replaced by both fields. It describes &lt;code&gt;safety_identifier&lt;/code&gt; as a stable end-user identifier used to help detect policy abuse, with a maximum length of 64 characters. It describes &lt;code&gt;prompt_cache_key&lt;/code&gt; as a key that helps route requests with similar reusable prefixes.&lt;/p&gt;

&lt;p&gt;Those are different lifecycles.&lt;/p&gt;

&lt;p&gt;A safety identifier should remain stable for one account across prompts. In this scheme, I change the cache key when the reusable prompt contract changes, and I can share it across users whose requests have the same prefix. Blindly copying the old value into both fields misses the chance to separate the policies; it may still be valid when the old value is privacy-preserving and per-user cache grouping is intentional.&lt;/p&gt;

&lt;p&gt;I model the split with two inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a canonical internal subject for identity;&lt;/li&gt;
&lt;li&gt;a cache group plus prompt-contract version for routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes a review question concrete: does this value identify a user, or does it identify reusable prompt structure?&lt;/p&gt;

&lt;h2&gt;
  
  
  Build two values from two policies
&lt;/h2&gt;

&lt;p&gt;OpenAI's &lt;a href="https://developers.openai.com/api/docs/guides/safety-best-practices" rel="noopener noreferrer"&gt;safety guidance&lt;/a&gt; recommends hashing a username or email instead of sending identifying information. In production I prefer an opaque internal account ID when one exists. I also use HMAC-SHA-256 with a secret pepper, rather than an unkeyed hash, so a copied digest is less useful for guessing common identifiers.&lt;/p&gt;

&lt;p&gt;HMAC is an application choice here, not an API requirement. The pepper belongs in a secret manager and should be at least 32 random bytes. The committed sample uses an obvious fixed fixture solely to keep its output reproducible.&lt;/p&gt;

&lt;p&gt;The derived value remains a stable, linkable pseudonym—not anonymous—and its privacy depends on protecting the pepper.&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;subjectBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&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;safetyIdentifier&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;HMACSHA256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HashData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privacyPepper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subjectBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&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;promptCacheKey&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;cacheGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cacheGroup&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;cacheVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cacheVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promptCacheKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;64&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Prompt cache key is too long."&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;request&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;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;safety_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safetyIdentifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt_cache_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;promptCacheKey&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lowercase hexadecimal digest is exactly 64 characters, which fits the documented safety-identifier maximum. The sample also caps the cache key at 64 characters and restricts its components to a conservative ASCII subset. Length prefixes keep pairs such as &lt;code&gt;a-b&lt;/code&gt; plus &lt;code&gt;c&lt;/code&gt; distinct from &lt;code&gt;a&lt;/code&gt; plus &lt;code&gt;b-c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cache key says nothing about the person. In the sample, &lt;code&gt;12_support-flow_2_v3&lt;/code&gt; identifies one reusable prompt contract; changing the prompt contract to &lt;code&gt;v4&lt;/code&gt; changes the key.&lt;/p&gt;

&lt;p&gt;That separation also makes rotation decisions visible. Rotating the HMAC pepper changes safety identifiers, so a production rollout may need a deliberate overlap plan. In this sample's policy, a deliberate prompt-contract revision advances the cache version even when the identity policy stays unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the payload before transport
&lt;/h2&gt;

&lt;p&gt;The runnable &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/098-openai-user-field-migration" rel="noopener noreferrer"&gt;sample on main&lt;/a&gt; uses only the .NET 10 shared framework. It builds JSON locally and performs fourteen checks, including these invariants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user is absent
safety_identifier is stable for the same subject
different subjects produce different identifiers
raw identity is absent from JSON
prompt_cache_key is shared across matching prompt contracts
a prompt-version change produces a different cache key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also rejects a short pepper before a transport boundary and serializes the same request twice to prove the bytes are identical. The executable makes no OpenAI request and requires no runtime network connection; restore and vulnerability-audit commands may contact configured NuGet sources.&lt;/p&gt;

&lt;p&gt;This is useful because a successful HTTP response would not prove the migration is correct. Both replacement fields are optional. A request can be accepted while omitting the per-user safety signal, the application-supplied cache-routing key, or both. A local contract test catches the omission where the request is assembled.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.openai.com/api/docs/guides/prompt-caching" rel="noopener noreferrer"&gt;prompt-caching guide&lt;/a&gt; adds one important boundary: &lt;code&gt;prompt_cache_key&lt;/code&gt; influences routing, but it does not pin traffic to a machine or guarantee a cache hit. Prefix content still has to match, and cache eligibility depends on the model and prompt shape. I treat the key as a routing hint, not a response cache or correctness mechanism.&lt;/p&gt;

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

&lt;p&gt;This sample does not prove that OpenAI accepts a chosen model, that a cache read occurs, or that any safety action will result. Those belong to integration tests and production telemetry. It also does not prescribe one universal cache-key scheme; high-volume applications should tune grouping against real prefix reuse and overflow behavior.&lt;/p&gt;

&lt;p&gt;For anonymous previews, OpenAI's guidance allows a session ID as the safety identifier. For trusted internal batch jobs with no individual end user, forcing a fictional per-user identity would be misleading. I would document that boundary instead of inventing one.&lt;/p&gt;

&lt;p&gt;The practical migration rule is small: remove &lt;code&gt;user&lt;/code&gt;, derive &lt;code&gt;safety_identifier&lt;/code&gt; from a stable privacy-preserving identity policy, derive &lt;code&gt;prompt_cache_key&lt;/code&gt; from a reusable prompt policy, and test both independently.&lt;/p&gt;

&lt;p&gt;What deprecated request field are you turning into an explicit contract test next?&lt;/p&gt;

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

</description>
      <category>openai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>security</category>
    </item>
    <item>
      <title>MCP C# Per-Request Client Capabilities: Read the Request, Not the Server</title>
      <dc:creator>Sukhpinder Singh</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:07:15 +0000</pubDate>
      <link>https://dev.to/ssukhpinder/mcp-c-per-request-client-capabilities-read-the-request-not-the-server-566m</link>
      <guid>https://dev.to/ssukhpinder/mcp-c-per-request-client-capabilities-read-the-request-not-the-server-566m</guid>
      <description>&lt;p&gt;With &lt;strong&gt;MCP C# per-request client capabilities&lt;/strong&gt;, the &lt;code&gt;2026-07-28&lt;/code&gt; protocol changes where a server must read what the caller supports. Modern stateless requests carry their own &lt;code&gt;clientCapabilities&lt;/code&gt; inside &lt;code&gt;_meta&lt;/code&gt;; there is no initialization handshake whose values can be retained for the connection.&lt;/p&gt;

&lt;p&gt;That creates a small but important C# trap. Inside a stateless HTTP handler, &lt;code&gt;request.Server.ClientCapabilities&lt;/code&gt; is intentionally &lt;code&gt;null&lt;/code&gt;. The authoritative value lives on the current JSON-RPC request. If I cache a previous value, or treat the server property as the modern source, I can make the wrong decision for the next call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP C# per-request client capabilities moved
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;MCP 2026-07-28 release&lt;/a&gt; removed protocol-level sessions and the &lt;code&gt;initialize&lt;/code&gt; exchange from the modern path. Every request is self-contained, which lets separate server instances handle calls without sticky routing or a shared MCP session store.&lt;/p&gt;

&lt;p&gt;The request now carries reserved metadata such as:&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;"_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/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;"report-client"&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="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;"extensions"&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;"com.example/report-export"&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;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 key word is &lt;em&gt;request&lt;/em&gt;. A server must not infer capabilities from an earlier message. Two calls reaching the same process may legitimately declare different extension support, and concurrent handlers must stay isolated.&lt;/p&gt;

&lt;p&gt;The older handshake model encouraged a connection-level mental model: negotiate once, then consult the negotiated server object later. That assumption does not survive a sessionless request that can land on any instance. There is no durable "current client" whose feature set safely belongs in a singleton, static field, or process-wide cache. Even when one client normally sends the same declaration each time, the protocol boundary still has to treat the envelope it received as the source for that call.&lt;/p&gt;

&lt;p&gt;This also matters during staged migrations. A load test, proxy, or compatibility client can mix modern requests with different extension maps in one server process. Code that appears correct with one client may fail only under overlap, which is why I prefer a concurrency regression instead of a single serialized example.&lt;/p&gt;

&lt;p&gt;I used the stable &lt;a href="https://www.nuget.org/packages/ModelContextProtocol.AspNetCore/2.2.0" rel="noopener noreferrer"&gt;&lt;code&gt;ModelContextProtocol.AspNetCore&lt;/code&gt; 2.2.0 package&lt;/a&gt; for the sample. This is released functionality, not a preview API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the request, not the stateless server
&lt;/h2&gt;

&lt;p&gt;The C# SDK exposes the parsed value through &lt;code&gt;JsonRpcMessageContext.ClientCapabilities&lt;/code&gt;. A tool can reach it from its injected &lt;code&gt;RequestContext&amp;lt;CallToolRequestParams&amp;gt;&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;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InspectClientCapabilityAsync&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;requestName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CallToolRequestParams&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CapabilityBarrier&lt;/span&gt; &lt;span class="n"&gt;barrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;barrier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForBothAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JsonRpcRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;ClientCapabilities&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;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Extensions&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.example/report-export"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;true&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;serverValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientCapabilities&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="s"&gt;"null"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"set"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;requestName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:request=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"enabled"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;,server=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;serverValue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://csharp.sdk.modelcontextprotocol.io/api/ModelContextProtocol.Protocol.JsonRpcMessageContext.html" rel="noopener noreferrer"&gt;official &lt;code&gt;JsonRpcMessageContext&lt;/code&gt; reference&lt;/a&gt; calls the current request authoritative. That is the contract I want the code to make visible.&lt;/p&gt;

&lt;p&gt;Keeping the lookup beside the tool decision makes the &lt;code&gt;null&lt;/code&gt; cases easier to reason about. A missing request context means I do not have an affirmative declaration. An empty extensions dictionary means the client sent a valid capability object without this extension. Neither should silently fall back to whatever the previous handler observed.&lt;/p&gt;

&lt;p&gt;Notice that I am checking an extension declaration, not granting access. Client capabilities are self-reported protocol features. They are useful for deciding whether a response shape or interaction is supported, but they are not authentication, authorization, or trusted identity. A protected report export still needs validated claims and application policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce overlapping calls offline
&lt;/h2&gt;

&lt;p&gt;A sequential happy-path test can miss cross-request state. The &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/tree/main/090-mcp-request-capabilities" rel="noopener noreferrer"&gt;complete sample&lt;/a&gt; starts an ASP.NET Core &lt;code&gt;TestServer&lt;/code&gt;, so it exercises the real Streamable HTTP endpoint without opening a port.&lt;/p&gt;

&lt;p&gt;It sends two &lt;code&gt;tools/call&lt;/code&gt; requests concurrently. One advertises &lt;code&gt;com.example/report-export&lt;/code&gt;; the other sends an empty extensions map. A one-shot barrier pauses both tool handlers until they have arrived, then lets each read its request context. The verifier expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enabled-request:request=enabled,server=null
disabled-request:request=disabled,server=null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assertion checks two boundaries at once: each call retains its own capability value, and the stateless server property is not mistaken for the request metadata source.&lt;/p&gt;

&lt;p&gt;The barrier is deliberate. Both handlers arrive before either reads the capability, so the test does not pass merely because the requests happened to run one after another. If application code moved the declaration into shared mutable state, this arrangement would expose the last-writer-wins mistake. The five-second guard turns a broken dispatch path into a failure instead of leaving the test process blocked.&lt;/p&gt;

&lt;p&gt;The verifier also sends a modern request that omits &lt;code&gt;io.modelcontextprotocol/clientCapabilities&lt;/code&gt;. The SDK rejects it before the tool runs with HTTP 400 and JSON-RPC code &lt;code&gt;-32602&lt;/code&gt;. That is different from a well-formed request that declares capabilities but lacks a feature a particular operation requires.&lt;/p&gt;

&lt;p&gt;Run the checks with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet restore
dotnet format &lt;span class="nt"&gt;--verify-no-changes&lt;/span&gt; &lt;span class="nt"&gt;--no-restore&lt;/span&gt;
dotnet build &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release &lt;span class="nt"&gt;--no-restore&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release &lt;span class="nt"&gt;--no-build&lt;/span&gt;
dotnet list package &lt;span class="nt"&gt;--vulnerable&lt;/span&gt; &lt;span class="nt"&gt;--include-transitive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pull request and validation record are available &lt;a href="https://github.com/ssukhpinder/dev-to-code-samples/pull/80" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The sample uses fixed JSON, an in-memory server, and no MCP host, credential, model call, database, or runtime network request.&lt;/p&gt;

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

&lt;p&gt;This pattern targets the modern stateless HTTP path. Initialize-era clients establish capabilities during the legacy handshake, so migration code that supports both eras should follow the SDK's version-specific behavior rather than manually copying &lt;code&gt;_meta&lt;/code&gt; into a global cache.&lt;/p&gt;

&lt;p&gt;The sample also checks one custom extension, not every built-in capability or extension schema. In production I would centralize the extension name and validate any settings object before consuming it. I would still pass the current request into that policy rather than storing a mutable capability snapshot. If several tools need the same decision, a request-scoped service is a better home than a singleton.&lt;/p&gt;

&lt;p&gt;I also would not add a capability check to ordinary tools that never depend on an optional client feature. More gates create more failure modes. Read the metadata where a response or interaction genuinely requires it, keep the check request-scoped, and test the missing declaration separately from an unauthorized caller.&lt;/p&gt;

&lt;p&gt;How are you testing request-scoped metadata before moving an MCP server to the &lt;code&gt;2026-07-28&lt;/code&gt; path?&lt;/p&gt;

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

</description>
      <category>mcp</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <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>
  </channel>
</rss>
