<?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: Ivan Rossouw</title>
    <description>The latest articles on DEV Community by Ivan Rossouw (@iqtechsolutions).</description>
    <link>https://dev.to/iqtechsolutions</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%2F4035800%2F9c84067b-fe9a-46e7-8144-5b05a4ffb504.png</url>
      <title>DEV Community: Ivan Rossouw</title>
      <link>https://dev.to/iqtechsolutions</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iqtechsolutions"/>
    <language>en</language>
    <item>
      <title>A Green Build Is Not a Route Test</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Tue, 01 Sep 2026 06:21:34 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-green-build-is-not-a-route-test-4oec</link>
      <guid>https://dev.to/iqtechsolutions/a-green-build-is-not-a-route-test-4oec</guid>
      <description>&lt;p&gt;A Blazor Hybrid application can compile successfully and still fail as soon as its router starts.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that nothing needs to be wrong with the individual components. The failure can live in the composition of otherwise valid Razor pages: two scanned assemblies declare the same route, and the conflict is only discovered at runtime.&lt;/p&gt;

&lt;p&gt;That makes route ownership an architectural contract, not just an &lt;code&gt;@page&lt;/code&gt; detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invisible route set
&lt;/h2&gt;

&lt;p&gt;Multi-head applications often share UI through a Razor class library. A web host, a progressive web app, and a .NET MAUI host may each scan two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the host's own assembly; and&lt;/li&gt;
&lt;li&gt;a shared UI assembly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is useful. Shared pages can be implemented once while each host keeps pages that depend on its own authentication model, platform services, or startup behaviour.&lt;/p&gt;

&lt;p&gt;It also creates a set that the compiler does not reason about: &lt;strong&gt;all routes visible to one router&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine the shared assembly contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@page "/settings"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, a host-specific page independently declares the same path. Both components are valid. Both assemblies compile. The duplicate becomes visible only when that host's router scans the combined set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a green build is insufficient
&lt;/h2&gt;

&lt;p&gt;Compilation proves that each project is structurally valid. It does not prove that independently compiled route tables are unique when combined.&lt;/p&gt;

&lt;p&gt;This is a general modularity problem. A module can be correct in isolation while the application graph is invalid after composition. Dependency-injection cycles, duplicate HTTP endpoints, conflicting configuration keys, and route collisions all share this shape.&lt;/p&gt;

&lt;p&gt;For Blazor, the timing makes the failure especially awkward. The collision can wait until the router initialises on first load. A pipeline may restore, compile, and test successfully, while the person opening the application becomes the integration test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the MAUI host changes the decision
&lt;/h2&gt;

&lt;p&gt;Web applications can cheaply receive post-deploy smoke coverage: request a page and assert that it renders instead of returning an error.&lt;/p&gt;

&lt;p&gt;A MAUI build is different. Producing an Android or iOS binary does not prove that a BlazorWebView can start, compose its scanned assemblies, and navigate. A real-device or emulator startup test is valuable, but it adds workload installation, boot time, device management, and another source of CI instability.&lt;/p&gt;

&lt;p&gt;In the committed change I reviewed, the web-facing heads had page smoke coverage while the MAUI workflow had no equivalent startup check. That made a fast structural guard a sensible intermediate layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a focused collision guard
&lt;/h2&gt;

&lt;p&gt;The guard does not need to reproduce the whole Razor runtime. It needs to answer one architectural question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Within the assemblies scanned by each host, is any route declared more than once?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A practical implementation can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enumerate &lt;code&gt;.razor&lt;/code&gt; source files for a host and its shared UI project;&lt;/li&gt;
&lt;li&gt;ignore generated &lt;code&gt;bin&lt;/code&gt; and &lt;code&gt;obj&lt;/code&gt; trees;&lt;/li&gt;
&lt;li&gt;remove Razor and HTML comments;&lt;/li&gt;
&lt;li&gt;recognise only complete &lt;code&gt;@page&lt;/code&gt; directive lines;&lt;/li&gt;
&lt;li&gt;normalise paths with an explicit case policy; and&lt;/li&gt;
&lt;li&gt;group the resulting declarations by route.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the same assertion for every head. A collision report should name the generalized host and the two source locations so a developer can resolve ownership quickly.&lt;/p&gt;

&lt;p&gt;This test is intentionally narrow. It does not need a browser, emulator, dependency container, or application server. Its value is that it turns a late composition failure into an early, deterministic signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the guard prove itself
&lt;/h2&gt;

&lt;p&gt;Source guards have a dangerous failure mode: they can pass because the parser found nothing.&lt;/p&gt;

&lt;p&gt;That is why a negative assertion such as “there are no duplicates” is incomplete. Add an anti-vacuous test that requires the parser to find a few routes that must exist. If a future Razor syntax change, path change, or parser bug empties the result, the test fails instead of giving false confidence.&lt;/p&gt;

&lt;p&gt;Test false positives too. Comments often contain route examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@* Do not duplicate @page "/settings" in this host. *@
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A naïve text search counts that as a declaration. A focused parser should ignore it, and a test should pin that behaviour. Excluding generated copies matters for the same reason; otherwise every route can appear to collide with itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off
&lt;/h2&gt;

&lt;p&gt;A source-level guard is fast and portable, but it is coupled to project layout and Razor syntax. It does not exercise the real router, dependency injection, native lifecycle, or device packaging.&lt;/p&gt;

&lt;p&gt;A device startup test provides higher-fidelity evidence, but it is slower and more operationally expensive. The two tests answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;source guard:&lt;/strong&gt; is the declared route set structurally unique for every host?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;device smoke test:&lt;/strong&gt; can the packaged application actually start and navigate in its platform environment?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the source guard for immediate feedback, then add at least one device-startup path when the release risk justifies the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist
&lt;/h2&gt;

&lt;p&gt;Before adding or moving a shared Razor page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write down every assembly each host scans.&lt;/li&gt;
&lt;li&gt;Decide whether the route is shared or host-owned.&lt;/li&gt;
&lt;li&gt;Assert uniqueness across each host's complete scanned set.&lt;/li&gt;
&lt;li&gt;Prove the guard finds known routes.&lt;/li&gt;
&lt;li&gt;Prove comments and generated output do not create false routes.&lt;/li&gt;
&lt;li&gt;Keep a device startup check on the roadmap, because structural tests are not runtime proof.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shared UI is valuable precisely because it removes duplication. The price is making the composition rules explicit. A green build tells you the parts compile. A route test tells you whether those parts can safely meet.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>mobile</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your PowerShell Script Has a .NET Runtime Too</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Mon, 31 Aug 2026 06:25:23 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/your-powershell-script-has-a-net-runtime-too-2md9</link>
      <guid>https://dev.to/iqtechsolutions/your-powershell-script-has-a-net-runtime-too-2md9</guid>
      <description>&lt;p&gt;A PowerShell script can parse cleanly, pass review, and still fail before it performs any useful deployment work.&lt;/p&gt;

&lt;p&gt;The missing part of the compatibility statement is often the runtime underneath the shell.&lt;/p&gt;

&lt;p&gt;A recent CI/CD fix in a shared .NET repository exposed a compact example. A deployment-proof script needed case-aware substring checks, so it called this overload:&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="nv"&gt;$text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$expected&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="n"&gt;System.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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a minimal two-host smoke check, that expression works in modern PowerShell running on current .NET. On Windows PowerShell 5.1, backed by the older .NET Framework, the isolated call fails with a method-resolution error because that overload is not available.&lt;/p&gt;

&lt;p&gt;The script's syntax is valid in both places. The API surface is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  PowerShell is also a .NET API client
&lt;/h2&gt;

&lt;p&gt;PowerShell makes .NET methods feel like part of the language. That convenience can hide an important dependency.&lt;/p&gt;

&lt;p&gt;When a script calls an instance method, overload resolution happens against the runtime loaded by the current host. A method introduced in a newer .NET version does not become available merely because the PowerShell syntax parser accepts the expression.&lt;/p&gt;

&lt;p&gt;This creates a portability stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PowerShell language version;&lt;/li&gt;
&lt;li&gt;underlying .NET runtime and API surface;&lt;/li&gt;
&lt;li&gt;installed modules;&lt;/li&gt;
&lt;li&gt;native tools and their versions; and&lt;/li&gt;
&lt;li&gt;host operating-system behaviour.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;“It is a PowerShell script” names only the first layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the semantics, not just the outcome
&lt;/h2&gt;

&lt;p&gt;The fix was to use an older-compatible API while keeping the comparison rule explicit:&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="nv"&gt;$text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$expected&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="n"&gt;System.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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IndexOf&lt;/code&gt; returning zero or greater means the value was found. More importantly, the explicit &lt;code&gt;StringComparison&lt;/code&gt; preserves the original case and culture semantics.&lt;/p&gt;

&lt;p&gt;A tempting rewrite is:&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="nv"&gt;$text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="s2"&gt;*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That may be acceptable in some scripts, but it changes the contract. Wildcard metacharacters, default case behaviour, and culture expectations can produce different results. Compatibility work should not quietly change meaning.&lt;/p&gt;

&lt;p&gt;When replacing an unavailable API, write down the behaviour being preserved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ordinal or culture-aware comparison;&lt;/li&gt;
&lt;li&gt;case-sensitive or case-insensitive matching;&lt;/li&gt;
&lt;li&gt;literal text or wildcard pattern;&lt;/li&gt;
&lt;li&gt;null and empty-value behaviour; and&lt;/li&gt;
&lt;li&gt;success and failure return conventions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choose a support strategy deliberately
&lt;/h2&gt;

&lt;p&gt;There are two honest approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standardise on a modern host.&lt;/strong&gt; Pin PowerShell 7 and a known .NET runtime in every supported runner. This gives scripts a cleaner API surface and reduces compatibility branches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support the oldest required host.&lt;/strong&gt; If a deployment server still runs Windows PowerShell 5.1, treat that environment as part of the product contract. Use compatible APIs and test there.&lt;/p&gt;

&lt;p&gt;Neither choice is free. Upgrading a runner may involve operating-system constraints, security review, module changes, or a carefully staged deployment. Supporting multiple engines increases the test matrix and can make otherwise simple expressions more verbose.&lt;/p&gt;

&lt;p&gt;The expensive option is pretending the choice has not been made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the real runtime in the test loop
&lt;/h2&gt;

&lt;p&gt;A source guard can stop a known incompatible pattern from returning. For example, a repository test might require the supported &lt;code&gt;IndexOf&lt;/code&gt; form and reject the problematic &lt;code&gt;Contains&lt;/code&gt; overload in a critical script.&lt;/p&gt;

&lt;p&gt;That is useful, but it is structural evidence. It proves that one text pattern is absent; it does not prove the script runs on the target host.&lt;/p&gt;

&lt;p&gt;A stronger CI matrix adds a small runtime smoke test:&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="nv"&gt;$engines&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"legacy"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell.exe"&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="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"modern"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pwsh"&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$engines&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Command&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;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;/verify-script-contract.ps1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$LASTEXITCODE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Runtime contract failed for &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real pipeline, run this on actual runner images rather than assuming both commands on one machine represent production. Keep the smoke test narrow: load the script, exercise compatibility-sensitive helpers, and avoid invoking external side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a runtime manifest
&lt;/h2&gt;

&lt;p&gt;For deployment automation, a small machine-readable manifest can make assumptions reviewable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;shells&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;windows-powershell-5.1&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;powershell-7&lt;/span&gt;
&lt;span class="na"&gt;requiredModules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Example.Module&lt;/span&gt;
    &lt;span class="na"&gt;minimumVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.4.0&lt;/span&gt;
&lt;span class="na"&gt;nativeTools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact format is less important than ownership. Someone must decide when an old engine leaves the support set, who validates runner images, and how a toolchain change is rolled out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical review
&lt;/h2&gt;

&lt;p&gt;When a CI or deployment script changes, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which shell executable will run it?&lt;/li&gt;
&lt;li&gt;Which .NET runtime backs that shell?&lt;/li&gt;
&lt;li&gt;Are called method overloads present there?&lt;/li&gt;
&lt;li&gt;Are comparison semantics explicit?&lt;/li&gt;
&lt;li&gt;Are module and native-tool versions pinned or checked?&lt;/li&gt;
&lt;li&gt;Does at least one test execute on the oldest supported runner?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Portability is not a property of syntax alone. It is an agreement between the script and the complete execution environment.&lt;/p&gt;

&lt;p&gt;Make that environment explicit, test the oldest supported boundary, and choose compatibility shims only when they preserve the behaviour you intended.&lt;/p&gt;

</description>
      <category>shell</category>
      <category>devops</category>
      <category>dotnet</category>
      <category>testing</category>
    </item>
    <item>
      <title>Email Is Not a Web Page</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sun, 30 Aug 2026 08:25:08 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/email-is-not-a-web-page-2bfn</link>
      <guid>https://dev.to/iqtechsolutions/email-is-not-a-web-page-2bfn</guid>
      <description>&lt;p&gt;An HTML email can be generated correctly, accepted by a provider, and delivered successfully—then arrive with its most important image missing or transformed.&lt;/p&gt;

&lt;p&gt;That is not only a design problem. It is an architectural reminder: email is a delivered document, not a miniature web page.&lt;/p&gt;

&lt;p&gt;A web page runs in a renderer you can observe and update. An email leaves your boundary and enters clients that may block remote images, proxy them, cache them, rewrite markup, or alter colours for dark mode. When a visual matters to recognition or comprehension, a normal web asset reference can be a surprisingly weak contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden web-page assumption
&lt;/h2&gt;

&lt;p&gt;A remote image looks attractive because it keeps the message small and lets many messages share one cached asset. It also assumes several things outside your control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the client permits a network fetch;&lt;/li&gt;
&lt;li&gt;the remote address remains reachable;&lt;/li&gt;
&lt;li&gt;a privacy proxy preserves the response;&lt;/li&gt;
&lt;li&gt;the client supports the markup as expected; and&lt;/li&gt;
&lt;li&gt;colour transformations do not destroy contrast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those assumptions may be acceptable for decoration. They are less comfortable when the visual helps a recipient recognise the sender or understand the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify content by consequence
&lt;/h2&gt;

&lt;p&gt;Before choosing an image strategy, classify the content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical to recognition or understanding:&lt;/strong&gt; consider embedding it in the message, give it useful alternative text, and test the degraded path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decorative:&lt;/strong&gt; a remote image may be sufficient. Its absence should not change the meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Essential information:&lt;/strong&gt; express it as real text. An amount, deadline, action, warning, or status should never exist only inside pixels.&lt;/p&gt;

&lt;p&gt;This classification is more useful than a blanket rule that every image must be embedded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put critical pixels inside the message
&lt;/h2&gt;

&lt;p&gt;Most mail formats can carry an inline attachment with a content identifier. The HTML references that identifier instead of asking the client to fetch a public resource.&lt;/p&gt;

&lt;p&gt;A small value object keeps the template independent of storage details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;EmailVisual&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;Reference&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;MediaType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ReadOnlyMemory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;Content&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;RemoteFallback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;Content&lt;/code&gt; is present, the sender adds one inline attachment and renders the image source as &lt;code&gt;cid:&lt;/code&gt; plus &lt;code&gt;Reference&lt;/code&gt;. If only &lt;code&gt;RemoteFallback&lt;/code&gt; is present, the template retains the remote form.&lt;/p&gt;

&lt;p&gt;The important idea is not the record shape. It is that the template receives a resolved rendering decision rather than opening files or guessing media types itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate resolution from sending
&lt;/h2&gt;

&lt;p&gt;In ASP.NET Core, a narrow resolver can use the host's file abstraction to load a trusted, deployed asset, derive a MIME type from its configured filename, and carry that resolved value with the bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;EmailVisual&lt;/span&gt; &lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalAsset&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;EmailVisualFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FallbackAddress&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;file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFileInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalAsset&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exists&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="s"&gt;"Configured email visual was not found"&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;EmailVisualFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FallbackAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateReadStream&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;buffer&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;MemoryStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CopyTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EmailVisualFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Inline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"message-visual"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mediaType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MediaTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;For&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&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="n"&gt;buffer&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="n"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FallbackAddress&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;IOException&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;"Email visual could not be loaded"&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;EmailVisualFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FallbackAddress&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;This example is deliberately general. In production, validate the configured path, bound the asset size, map only allowed media types, and catch only failures the resolver can safely degrade.&lt;/p&gt;

&lt;p&gt;Keeping resolution separate gives the template, transport adapter, and tests one stable contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Degrade presentation without failing composition
&lt;/h2&gt;

&lt;p&gt;A missing decorative or recognition image should usually not prevent an otherwise useful transactional message from being composed.&lt;/p&gt;

&lt;p&gt;That makes fallback behaviour part of the design. If a configured local asset is missing or unreadable, retain a safe remote reference and emit an operational warning. If no local path is configured, the remote representation can remain the expected path rather than an error. The message still contains its text, links, and actions.&lt;/p&gt;

&lt;p&gt;This is not the same as pretending nothing happened. Composition continues, while telemetry records that presentation degraded. If the image contains essential meaning, the safer fix is to move that meaning into text rather than make message composition depend on image resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for client transformations
&lt;/h2&gt;

&lt;p&gt;Embedding solves the remote-fetch dependency. It does not make every client render identically.&lt;/p&gt;

&lt;p&gt;Transparent artwork that looks good on a web page can lose contrast when a mail client applies dark-mode transformations. An email-specific asset with an explicit light canvas and sufficient internal contrast can make the intended result more stable.&lt;/p&gt;

&lt;p&gt;That is a separate rendering asset, so name and own it as one. Do not silently assume the main web image is suitable for mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the contracts you own
&lt;/h2&gt;

&lt;p&gt;Useful automated tests focus on decisions within the application boundary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A configured local asset resolves to the expected bytes and media type.&lt;/li&gt;
&lt;li&gt;A missing or unreadable asset returns the remote fallback without throwing.&lt;/li&gt;
&lt;li&gt;No configured local asset keeps the remote path.&lt;/li&gt;
&lt;li&gt;Embedded content becomes exactly one inline attachment.&lt;/li&gt;
&lt;li&gt;The HTML references the matching content identifier.&lt;/li&gt;
&lt;li&gt;Remote-only rendering does not create an unnecessary attachment.&lt;/li&gt;
&lt;li&gt;Render snapshots select the intended email-specific asset.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These tests prove composition and degradation behaviour. They do not prove universal compatibility across every mail client. A small real-client review matrix is still valuable for high-volume templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off
&lt;/h2&gt;

&lt;p&gt;Inline assets increase message size and remove shared caching. Rendering-specific variants create another file to maintain. Content identifiers and media types add composition rules that remote images avoid.&lt;/p&gt;

&lt;p&gt;In return, critical pixels travel with the message and do not depend on a public fetch when inline resolution succeeds. A fallback keeps local image resolution non-fatal.&lt;/p&gt;

&lt;p&gt;The practical rule is simple: embed what matters, keep essential meaning in text, and let presentation degrade without stopping the message.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>Catch by Recovery Policy, Not Convenience</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sat, 29 Aug 2026 07:55:14 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/catch-by-recovery-policy-not-convenience-12l6</link>
      <guid>https://dev.to/iqtechsolutions/catch-by-recovery-policy-not-convenience-12l6</guid>
      <description>&lt;p&gt;Liquid syntax error: 'raw' tag was never closed&lt;/p&gt;
</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>sre</category>
      <category>testing</category>
    </item>
    <item>
      <title>Gate New Actions, Keep Recovery Open</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Fri, 28 Aug 2026 06:53:30 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/gate-new-actions-keep-recovery-open-266h</link>
      <guid>https://dev.to/iqtechsolutions/gate-new-actions-keep-recovery-open-266h</guid>
      <description>&lt;p&gt;Liquid syntax error: 'raw' tag was never closed&lt;/p&gt;
</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Gate New Actions, Keep Recovery Open</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Fri, 28 Aug 2026 06:10:57 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/gate-new-actions-keep-recovery-open-pfg</link>
      <guid>https://dev.to/iqtechsolutions/gate-new-actions-keep-recovery-open-pfg</guid>
      <description>&lt;p&gt;Liquid syntax error: 'raw' tag was never closed&lt;/p&gt;
</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Counts Match. The Data Can Still Be Wrong</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:26:55 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/counts-match-the-data-can-still-be-wrong-5cm2</link>
      <guid>https://dev.to/iqtechsolutions/counts-match-the-data-can-still-be-wrong-5cm2</guid>
      <description>&lt;p&gt;Many systems eventually need to repartition one persisted record. A batch becomes several processing groups. A container becomes several parcels. A work queue becomes several assignments.&lt;/p&gt;

&lt;p&gt;The domain changes, but the risk is consistent: every child must move from one source into exactly one result without being invented, duplicated, or lost.&lt;/p&gt;

&lt;p&gt;The tempting safeguard is a count comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The source contains twelve items.&lt;/li&gt;
&lt;li&gt;The proposed results contain twelve items altogether.&lt;/li&gt;
&lt;li&gt;Therefore, the split must be valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unfortunately, equal counts prove only cardinality. They do not prove identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind spot in a passing count
&lt;/h2&gt;

&lt;p&gt;Imagine that a source contains the item IDs &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, and &lt;code&gt;C&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A proposed split contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Result one: &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;A&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Result two: &lt;code&gt;C&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source and outputs both contain three entries. A count check passes, even though &lt;code&gt;A&lt;/code&gt; was duplicated and &lt;code&gt;B&lt;/code&gt; disappeared.&lt;/p&gt;

&lt;p&gt;Another invalid proposal might contain &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, and &lt;code&gt;D&lt;/code&gt;. Again, the count matches, but an unknown item has replaced a valid one.&lt;/p&gt;

&lt;p&gt;These are not arithmetic failures. They are failures of identity conservation.&lt;/p&gt;

&lt;h2&gt;
  
  
  State the stronger invariant
&lt;/h2&gt;

&lt;p&gt;For a valid repartition, the original identity set must equal the disjoint union of the output identity sets.&lt;/p&gt;

&lt;p&gt;That gives us three useful requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every original ID appears in the outputs.&lt;/li&gt;
&lt;li&gt;No unknown ID appears in the outputs.&lt;/li&gt;
&lt;li&gt;No ID appears more than once.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Set equality proves membership. A duplicate check proves disjointness. Cardinality remains useful, but only as one part of the invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before mutation
&lt;/h2&gt;

&lt;p&gt;A generalized C# guard can make that rule explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EnsureExactPartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;originalIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;partitions&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;original&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;originalIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToHashSet&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;output&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ids&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sourceHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;original&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;originalIds&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;outputHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&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="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;output&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;outputHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;output&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="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&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;"The requested split is not an exact partition."&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;This example is deliberately small. In a real application, authorization, lifecycle state, concurrency, and ownership rules may add further checks. The important ordering is that the complete proposal is validated before tracked entities or database state are changed.&lt;/p&gt;

&lt;p&gt;That ordering makes failure cheap. An invalid request becomes a rejected request, not a cleanup exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move tracked children instead of copying them
&lt;/h2&gt;

&lt;p&gt;When the children already exist as tracked entities, repartitioning should preserve their identities.&lt;/p&gt;

&lt;p&gt;Creating new child objects by copying fields can accidentally turn a move into duplication. It may also lose historical references, introduce new primary keys, or leave the originals attached to the source.&lt;/p&gt;

&lt;p&gt;Instead, load the authorized source and its children, resolve each requested ID to the existing tracked instance, and re-parent that instance to its destination. With EF Core, this usually means updating the relationship through the navigation property or foreign key and allowing relationship fix-up to track the move.&lt;/p&gt;

&lt;p&gt;The output records are new. The children are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the whole transition atomic
&lt;/h2&gt;

&lt;p&gt;A valid split still should not become partially visible.&lt;/p&gt;

&lt;p&gt;Creating two results, failing on the third, and leaving the source marked as processed produces a state that is difficult to reason about and harder to retry safely.&lt;/p&gt;

&lt;p&gt;Treat these changes as one unit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create every result.&lt;/li&gt;
&lt;li&gt;Move every child.&lt;/li&gt;
&lt;li&gt;Transition the source record.&lt;/li&gt;
&lt;li&gt;Save the complete state atomically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single &lt;code&gt;SaveChanges&lt;/code&gt; call is transactional for supported relational providers. If the workflow requires multiple saves or coordinates additional durable work, use an explicit transaction or an outbox-style boundary.&lt;/p&gt;

&lt;p&gt;The desired outcome is simple: observers see either the original state or the complete repartitioned state, never a mixture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every result an idempotency scope
&lt;/h2&gt;

&lt;p&gt;Operation-level idempotency is helpful, but a split produces several distinct results. Each result therefore needs a stable scope of its own.&lt;/p&gt;

&lt;p&gt;A compound uniqueness boundary such as &lt;code&gt;(operationId, stableResultKey)&lt;/code&gt; lets a retry identify each intended result independently. The result key should come from stable input or deterministic content, not an array position whose meaning may change when ordering changes.&lt;/p&gt;

&lt;p&gt;A database uniqueness constraint should enforce this promise. Application checks improve error messages; the constraint protects correctness under concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off is intentional friction
&lt;/h2&gt;

&lt;p&gt;Exact set validation allocates collections. Transactions hold resources. Unique indexes add storage and write cost. The implementation is more involved than comparing two integers.&lt;/p&gt;

&lt;p&gt;For very large partitions, validation may need to move closer to the database or use streaming and batching. That changes the mechanism, not the invariant.&lt;/p&gt;

&lt;p&gt;The additional friction buys something valuable: failures occur before mutation, concurrent retries converge, and the persisted model remains explainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that expose the boundary
&lt;/h2&gt;

&lt;p&gt;A compact test suite should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A valid partition in a different order&lt;/li&gt;
&lt;li&gt;One duplicated ID&lt;/li&gt;
&lt;li&gt;One omitted ID&lt;/li&gt;
&lt;li&gt;One foreign ID&lt;/li&gt;
&lt;li&gt;A retry using the same per-result scopes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The negative cases should fail before any entity is re-parented or source state is changed.&lt;/p&gt;

&lt;p&gt;The practical lesson is modest: whenever one persisted record becomes many, ask more than, “Did the counts match?”&lt;/p&gt;

&lt;p&gt;Ask, “Did every identity move exactly once?”&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>testing</category>
    </item>
    <item>
      <title>A Secret Store Is Not a Boundary Until It Is the Only One</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:29:16 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-secret-store-is-not-a-boundary-until-it-is-the-only-one-7c3</link>
      <guid>https://dev.to/iqtechsolutions/a-secret-store-is-not-a-boundary-until-it-is-the-only-one-7c3</guid>
      <description>&lt;p&gt;Moving long-lived credentials into a managed secret store is a good security change. It is also easy to stop that migration too early.&lt;/p&gt;

&lt;p&gt;The uncomfortable question is not, “Do we have a secure store?” It is, “Can this credential still travel through any other path?”&lt;/p&gt;

&lt;p&gt;A recent C# and ASP.NET Core hardening change brought that distinction into focus. The intended store was secure, but older disk-backed helpers—including an SDK-backed file cache—still existed. The destination had improved; the authority model had not yet become singular.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration illusion
&lt;/h2&gt;

&lt;p&gt;OAuth integrations often accumulate several generations of code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a first version that writes a token file;&lt;/li&gt;
&lt;li&gt;a later service that saves a reference to a protected or managed secret;&lt;/li&gt;
&lt;li&gt;a legacy SDK helper configured with its own file-backed cache;&lt;/li&gt;
&lt;li&gt;a compatibility controller or helper that can still read the old files; and&lt;/li&gt;
&lt;li&gt;an environment fallback added to keep development convenient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each part can look reasonable in isolation. Together, they create multiple credential authorities.&lt;/p&gt;

&lt;p&gt;That matters because an attacker, operator, or future maintainer does not care which path the architecture diagram calls canonical. If an older reader can authenticate with a disk file, or a library can write a fresh cache beside the managed store, that alternate path is part of the real boundary.&lt;/p&gt;

&lt;p&gt;Security posture follows the weakest active route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define one credential authority
&lt;/h2&gt;

&lt;p&gt;A useful target state is closed-world: every credential read and write is accounted for, and anything outside the approved store is rejected.&lt;/p&gt;

&lt;p&gt;For a server-side integration, that might mean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolve client configuration directly from the configuration providers and parse it in memory.&lt;/li&gt;
&lt;li&gt;Exchange authorization codes without allowing the SDK to persist tokens.&lt;/li&gt;
&lt;li&gt;Store refresh tokens only through the application-owned credential service.&lt;/li&gt;
&lt;li&gt;Persist an opaque reference and provider metadata, never the plaintext token.&lt;/li&gt;
&lt;li&gt;Retrieve and retire the token through that same service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key phrase is “only through”. A secure store is not merely another destination. It is the sole authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close SDK persistence explicitly
&lt;/h2&gt;

&lt;p&gt;Third-party SDKs often provide convenient persistence helpers. Those are useful in a desktop sample and risky in a multi-user server process.&lt;/p&gt;

&lt;p&gt;Make the decision visible in every active flow:&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;flow&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;AuthorizationFlow&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;AuthorizationFlowOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ClientConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ResolveInMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;CredentialStore&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;NoOpCredentialStore&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 names here are deliberately generic, but the principle is concrete: do not leave persistence semantics implicit or assume another layer owns them. Supply the no-op implementation where the SDK accepts its persistence dependency.&lt;/p&gt;

&lt;p&gt;Then enumerate all flows. Initial connection, token refresh, and API-client construction may each build their own authorization object. Fixing two out of three still leaves a second authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete the old path
&lt;/h2&gt;

&lt;p&gt;Deprecating a credential path in a comment is not the same as removing it.&lt;/p&gt;

&lt;p&gt;Delete obsolete file readers, writers, controllers, helper services, configuration keys, and package references. Remove their dependency-injection registrations. Stop copying old credential assets into build output. If a compatibility period is unavoidable, give it an owner, an expiry condition, and telemetry that proves whether it is still used.&lt;/p&gt;

&lt;p&gt;Deletion reduces the proof surface. Reviewers can reason about what exists instead of relying on a promise that reachable code “should no longer run”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test that the forbidden path stays absent
&lt;/h2&gt;

&lt;p&gt;Behaviour tests are essential, but some architecture regressions are difficult to trigger naturally. A convenience helper could be restored with file-backed caching while happy-path authorization tests remain green.&lt;/p&gt;

&lt;p&gt;This is one of the rare cases where a focused structural guard is useful. It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;take a non-empty census of the relevant production sources;&lt;/li&gt;
&lt;li&gt;require every known authorization flow to opt into the no-op store;&lt;/li&gt;
&lt;li&gt;reject known disk-store types, filenames, and helper calls; and&lt;/li&gt;
&lt;li&gt;assert that retired endpoints and directories remain absent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The non-empty census matters. A source scan that finds zero files can pass while proving nothing.&lt;/p&gt;

&lt;p&gt;Pair that guard with behavioural tests. Place a plausible legacy credential file where old code would have found it and prove the application ignores it. Simulate a managed-store write failure and prove the operation fails without persisting plaintext. For a development-only protected fallback, prove stored material differs from the original token and can be recovered only through the protector.&lt;/p&gt;

&lt;p&gt;Structural tests protect the shape; behavioural tests protect the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail closed by environment
&lt;/h2&gt;

&lt;p&gt;Development and production do not need identical storage mechanisms, but both need explicit policy.&lt;/p&gt;

&lt;p&gt;A local environment can use a protected, developer-owned store when a managed service would make ordinary work impractical. That fallback should be named and enabled deliberately. In non-development environments, the default should be no fallback: if the managed store is unavailable, the connection attempt fails.&lt;/p&gt;

&lt;p&gt;That is an operational trade-off. Availability decreases during a store outage. The alternative is worse: a silent downgrade that creates a credential copy in a less controlled location precisely when the primary control is unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering trade-off
&lt;/h2&gt;

&lt;p&gt;One authority means stricter configuration, fewer emergency shortcuts, more failure-path tests, and a migration that touches more than the storage class. It may expose outages that a permissive fallback previously hid.&lt;/p&gt;

&lt;p&gt;In return, incident response becomes clearer. There is one place to rotate, audit, revoke, and monitor. A failure cannot quietly change the protection level. Future maintainers have a smaller state space to understand.&lt;/p&gt;

&lt;p&gt;Before calling a credential migration complete, draw every reader and writer—including SDK helpers. If you cannot point to the single authority for each environment, the boundary is still open.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>security</category>
      <category>aspnetcore</category>
      <category>oauth</category>
    </item>
    <item>
      <title>Idempotence Must Survive the Clock</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:24:23 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/idempotence-must-survive-the-clock-bii</link>
      <guid>https://dev.to/iqtechsolutions/idempotence-must-survive-the-clock-bii</guid>
      <description>&lt;p&gt;Calling an operation twice with the same input is a useful idempotence check. For scheduled automation, it is not always enough.&lt;/p&gt;

&lt;p&gt;The second real invocation does not happen at the same instant. If current time influences classification, rendering, or persistence, then time is another input to the contract—even when the business state has not changed.&lt;/p&gt;

&lt;p&gt;That distinction matters because a test can prove same-instant repeatability while missing repeated side effects on every future run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comfortable test that was incomplete
&lt;/h2&gt;

&lt;p&gt;Consider a monitor that periodically evaluates a condition and maintains one notification. Its write plan is sensible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classify the current state.&lt;/li&gt;
&lt;li&gt;Render the notification that should exist.&lt;/li&gt;
&lt;li&gt;Compare it with the notification already stored.&lt;/li&gt;
&lt;li&gt;Perform no write when the two are equal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first regression test might call the planner twice, pass the first output back as the stored value, and assert that the second call returns &lt;code&gt;NoWrite&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is valuable. It catches an implementation that always updates. But if both calls share a frozen clock, they exercise only one moment. They do not simulate two scheduler runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How time turns stable state into different output
&lt;/h2&gt;

&lt;p&gt;The failure appears when the rendered body includes relative age:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The condition has been active for 2.0 hours.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several minutes later, the underlying condition is identical, but the renderer produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The condition has been active for 2.1 hours.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A byte comparison is doing exactly what it was asked to do: the bodies differ, so it plans an update. The monitor writes again, subscribers may be notified again, and the audit trail gains another event even though nothing meaningful happened.&lt;/p&gt;

&lt;p&gt;The defect is not in the equality check. It is in the definition of desired output. A volatile presentation value has accidentally become part of message identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate decision time from message identity
&lt;/h2&gt;

&lt;p&gt;Elapsed time may still be essential to the decision. A monitor often needs to ask whether a condition has lasted long enough to deserve attention. Removing the clock entirely would weaken the behaviour.&lt;/p&gt;

&lt;p&gt;The cleaner design is to separate classification from representation:&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;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Classify&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;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;desired&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&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;StartedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;Normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoWrite&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desired&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, current time helps decide whether the state has crossed a threshold. The persisted message uses an immutable event timestamp such as “active since 09:30 UTC”. As long as the underlying event is the same, the desired body remains stable.&lt;/p&gt;

&lt;p&gt;The exact language and types will differ between systems. The useful boundary is consistent: use volatile time for decisions; use stable facts for identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the next scheduled run
&lt;/h2&gt;

&lt;p&gt;The regression test should advance the clock while holding business state fixed:&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;first&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Plan&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;previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t0&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;afterFourHours&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Plan&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;previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHours&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;afterOneDay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Plan&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;previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;afterFourHours&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="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoWrite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;afterOneDay&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="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoWrite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the complementary test: change the meaningful state and assert one update. Idempotence does not mean “never write again”. It means “repeat the same side effect only when the contract says the desired state changed”.&lt;/p&gt;

&lt;p&gt;For a renderer whose output is intended to be stable, an even sharper assertion compares bodies across widely separated times. This catches any newly introduced current-time field, not just the one that caused the original defect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering trade-off
&lt;/h2&gt;

&lt;p&gt;Relative wording is pleasant. “Active for two hours” saves the reader from doing mental arithmetic. An immutable timestamp is slightly less immediate.&lt;/p&gt;

&lt;p&gt;Stable output, however, avoids redundant writes, rate-limit pressure, noisy audit history, repeated notifications, and eventually alert fatigue. Once people learn that a monitor repeats itself without new information, they stop trusting a signal that may later matter.&lt;/p&gt;

&lt;p&gt;There are reasonable middle paths. A client can calculate relative age at display time without rewriting the stored message. A monitor can update only at meaningful threshold crossings, such as warning and critical. Or the volatile age can live in a dashboard while the notification remains a stable pointer to the underlying event.&lt;/p&gt;

&lt;p&gt;The right choice depends on who reads the output and what an update triggers. The important part is to choose deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;p&gt;When reviewing scheduled or retryable automation, I now ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the clock injected so tests can move it?&lt;/li&gt;
&lt;li&gt;Which decisions legitimately depend on current time?&lt;/li&gt;
&lt;li&gt;Does any persisted or compared output contain a drifting value?&lt;/li&gt;
&lt;li&gt;Can two runs with unchanged business state produce different bytes?&lt;/li&gt;
&lt;li&gt;Does a no-op plan truly perform zero external writes?&lt;/li&gt;
&lt;li&gt;Do tests cover both advanced time and changed state?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The small mental shift is this: do not test only “run it twice”. Test “run it now, then run it later with nothing meaningful changed”.&lt;/p&gt;

&lt;p&gt;Where could a friendly relative timestamp be quietly turning your idempotent automation into a recurring side effect?&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>sre</category>
      <category>automation</category>
    </item>
    <item>
      <title>When Helper Text Becomes the Field Name: A Blazor Accessibility Lesson</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Mon, 24 Aug 2026 12:14:27 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/when-helper-text-becomes-the-field-name-a-blazor-accessibility-lesson-2n57</link>
      <guid>https://dev.to/iqtechsolutions/when-helper-text-becomes-the-field-name-a-blazor-accessibility-lesson-2n57</guid>
      <description>&lt;p&gt;A form can look correct, submit correctly, and contain valid HTML while still giving assistive technology the wrong name for a control.&lt;/p&gt;

&lt;p&gt;I recently inspected a committed Blazor accessibility fix with exactly that shape. Several fields placed their helper prose inside the field's &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;. Visually, the layout was tidy. Semantically, every helpful sentence had joined the control's accessible name.&lt;/p&gt;

&lt;p&gt;The focused lesson is simple: &lt;strong&gt;a label names a control; helper text describes it&lt;/strong&gt;. Those are different contracts, and the markup should preserve the distinction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that pixels do not reveal
&lt;/h2&gt;

&lt;p&gt;Consider this generalized Razor markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;label for="reference"&amp;gt;
    Reference
    &amp;lt;input id="reference" /&amp;gt;
    &amp;lt;span class="hint"&amp;gt;
        Use the reference shown on the original document.
    &amp;lt;/span&amp;gt;
&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sighted user sees a short label, a field, and a quieter line of guidance. That visual hierarchy does not determine the accessibility tree.&lt;/p&gt;

&lt;p&gt;Text associated through the label contributes to the control's accessible name. A screen reader may therefore announce something closer to “Reference, use the reference shown on the original document” as the field name. Repeat that pattern with a longer warning or policy note and routine form navigation becomes noisy.&lt;/p&gt;

&lt;p&gt;The HTML can still validate. A component test can still find the input. A screenshot can remain pixel-for-pixel unchanged. The defect lives in semantics, not appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Name and description are separate contracts
&lt;/h2&gt;

&lt;p&gt;The safer structure keeps the label concise and connects the explanatory text separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="field"&amp;gt;
    &amp;lt;label for="reference"&amp;gt;Reference&amp;lt;/label&amp;gt;
    &amp;lt;input id="reference"
           aria-describedby="reference-hint" /&amp;gt;
    &amp;lt;span class="hint" id="reference-hint"&amp;gt;
        Use the reference shown on the original document.
    &amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the relationships are explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;for&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; associate the short name with the control;&lt;/li&gt;
&lt;li&gt;the hint has its own unique identifier; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby&lt;/code&gt; asks assistive technology to announce the hint as supporting context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The expected experience is a concise name first, then the explanation. Users who navigate by form control can scan efficiently without losing the useful guidance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is more than an ARIA edit
&lt;/h2&gt;

&lt;p&gt;Moving the hint changes the document structure. The wrapper may now need to carry layout and typography that the old label supplied through inheritance. Without that CSS work, the semantic fix can cause inputs to change size, weight, spacing, or alignment even though the intended visual design has not changed.&lt;/p&gt;

&lt;p&gt;Interactive content deserves another look too. A link nested inside a label can compete with the label's activation behaviour. Moving helper content outside the label makes the click targets honest again: the label activates the control; the link behaves as a link.&lt;/p&gt;

&lt;p&gt;That is the engineering trade-off. The corrected markup needs a wrapper, stable IDs, deliberate CSS, and a little more review effort. In return, the field has a predictable name, the guidance remains available, and interactive descendants no longer share a misleading click target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the semantics, not only the markup
&lt;/h2&gt;

&lt;p&gt;The strongest check inspects the browser's computed accessibility tree. It should assert that the control has the expected accessible name and a separate description. That tests the behaviour users actually receive.&lt;/p&gt;

&lt;p&gt;Not every test environment exposes that tree. A source guard can still be a useful backstop if its limits are explicit. For example, it can reject a hint nested inside a label, require every hint to have an ID, and require a matching &lt;code&gt;aria-describedby&lt;/code&gt; reference.&lt;/p&gt;

&lt;p&gt;An absence-only source test has a familiar risk: a wrong directory or broken pattern can find nothing and pass. Give the guard a non-vacuous census so it fails when it has not inspected a credible amount of markup. Then mutation-check it: deliberately move one hint back into a label, remove one description reference, and point the scan at an empty-but-valid directory. Each mutation should make the test fail for the intended reason.&lt;/p&gt;

&lt;p&gt;The committed test I inspected follows that defensive shape. I did not rerun it, and it is not equivalent to a live screen-reader or browser accessibility-tree test.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;p&gt;For each field with helper text, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the accessible name short and specific?&lt;/li&gt;
&lt;li&gt;Is explanatory prose outside the label?&lt;/li&gt;
&lt;li&gt;Does the hint have a unique ID referenced by the control?&lt;/li&gt;
&lt;li&gt;Did the wrapper preserve the intended layout and font context?&lt;/li&gt;
&lt;li&gt;Are any links or other interactive elements trapped inside a label?&lt;/li&gt;
&lt;li&gt;Does at least one test inspect computed accessibility semantics, or clearly document the limits of its structural proxy?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Accessibility bugs often survive because the pixels look reassuring. Treat the accessibility tree as a first-class output of the component, alongside the rendered page and submitted model.&lt;/p&gt;

&lt;p&gt;Where in your forms might helpful prose be quietly promoted into a field name?&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>blazor</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Completion Is an Ownership Boundary</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sun, 23 Aug 2026 06:31:57 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/completion-is-an-ownership-boundary-540o</link>
      <guid>https://dev.to/iqtechsolutions/completion-is-an-ownership-boundary-540o</guid>
      <description>&lt;p&gt;An async method can finish its useful work and still be holding lifecycle state that tells the rest of the system it is active.&lt;/p&gt;

&lt;p&gt;That sounds like a tiny implementation detail. Under concurrency, it becomes a contract bug.&lt;/p&gt;

&lt;p&gt;I recently inspected a committed C# fix where an attempt could settle while two paths still believed they owned the same cancellation source. One path was normal attempt teardown. The other was component disposal. A narrow scheduling window could let one path dispose the source while the other still intended to cancel it.&lt;/p&gt;

&lt;p&gt;The lesson is broader than cancellation: &lt;strong&gt;completion is an ownership boundary&lt;/strong&gt;. Before an operation becomes observably complete, its shared lifecycle resources and active-state markers should already have reached their next valid owner or terminal state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dangerous gap after the work is done
&lt;/h2&gt;

&lt;p&gt;Consider a reusable client-side workflow that starts one attempt at a time. It stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the current task;&lt;/li&gt;
&lt;li&gt;a cancellation source for that attempt;&lt;/li&gt;
&lt;li&gt;an active-attempt identifier; and&lt;/li&gt;
&lt;li&gt;a snapshot published to observers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The network work finishes. The code builds a settled snapshot, publishes it, completes a &lt;code&gt;TaskCompletionSource&lt;/code&gt;, and only then clears the active task and disposes the cancellation source.&lt;/p&gt;

&lt;p&gt;For a moment, callers can observe “finished” while internal state still says “active.” A retry may join the stale completed task. Concurrent disposal may capture a source that the attempt is about to dispose. These failures are rare because the state is individually valid on both sides of the gap; only the ordering is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the resource exactly one owner
&lt;/h2&gt;

&lt;p&gt;A cancellation source should not be “shared until somebody disposes it.” It should have one owner at each point in the lifecycle.&lt;/p&gt;

&lt;p&gt;Here is deliberately invented C# to show the handoff pattern:&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;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;TakeAttemptCancellation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_stateLock&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;owned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_attemptCancellation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_attemptCancellation&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="n"&gt;owned&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;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&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;owned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;TakeAttemptCancellation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;owned&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;DrainActiveAttemptWithinBudgetAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;owned&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important operation is not &lt;code&gt;Cancel&lt;/code&gt; or &lt;code&gt;Dispose&lt;/code&gt;. It is the atomic transfer: read the shared field and clear it while holding the state lock. After that, the local variable is the sole owner. Another path entering the same lock finds &lt;code&gt;null&lt;/code&gt; and knows it owns nothing.&lt;/p&gt;

&lt;p&gt;Cancellation and disposal can then happen outside the lock, avoiding callbacks or slow work while shared state is protected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release before publishing completion
&lt;/h2&gt;

&lt;p&gt;The attempt path needs the complementary rule. Clear the active task and detach or dispose its cancellation source before any observer can see the settled result.&lt;/p&gt;

&lt;p&gt;In generalized form:&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;Result&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_stateLock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_activeAttempt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_attemptCancellation&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_attemptCancellation&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settled&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="nf"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrySetResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settled&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;This ordering gives completion a useful meaning: if an observer has seen the result or awaited the task, the object is already idle. The next operation cannot accidentally coalesce onto stale work.&lt;/p&gt;

&lt;p&gt;This is not a universal instruction to dispose every resource inside a lock. The point is to make the ownership transition atomic. What happens after the transition depends on whether disposal can block, invoke callbacks, or acquire other locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the ordering, not the odds
&lt;/h2&gt;

&lt;p&gt;A stress loop is useful as a backstop, but it may never hit an extremely narrow scheduling window. A deterministic regression test should control the boundary.&lt;/p&gt;

&lt;p&gt;One approach is to block inside the observer that receives the settled snapshot. While publication is paused, inspect the object from another thread. The invariant should already hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the previous attempt is no longer active;&lt;/li&gt;
&lt;li&gt;a new start cannot receive the stale completed task;&lt;/li&gt;
&lt;li&gt;disposal can run without competing for the same source; and&lt;/li&gt;
&lt;li&gt;repeated disposal remains harmless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also cover disposal while work is in flight. Those tests describe ownership much better than “run this race many times and hope.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering trade-off
&lt;/h2&gt;

&lt;p&gt;Explicit ownership adds code. The order of lock-protected mutations becomes part of the design, and reviewers must reason about publication, task completion, cancellation, disposal, and retries together.&lt;/p&gt;

&lt;p&gt;The return is a lifecycle contract people can explain: one owner at a time; observable completion implies released attempt state; disposal is idempotent; immediate retry starts new work.&lt;/p&gt;

&lt;p&gt;The committed change I inspected applied singular cancellation-source ownership in two similar async workflows. In the detailed workflow behind this example, active state is also released before snapshot publication; the sibling workflow releases ownership before completing its returned task while retaining its existing publication path. The committed tests include deterministic settlement-boundary coverage for the detailed workflow plus repeated- and in-flight-disposal coverage across both. I inspected the implementation and test assertions, but I did not rerun the suite or verify runtime behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;p&gt;For every async operation with reusable state, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What exactly does “complete” promise to observers?&lt;/li&gt;
&lt;li&gt;Who owns each cancellation source before, during, and after settlement?&lt;/li&gt;
&lt;li&gt;Can retry, disposal, or a callback enter between publication and teardown?&lt;/li&gt;
&lt;li&gt;Can the ownership transfer be made atomic while expensive work stays outside the lock?&lt;/li&gt;
&lt;li&gt;Is there a deterministic test that pauses at the boundary?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rare races often survive because each line looks reasonable in isolation. Naming the ownership boundary makes the ordering reviewable.&lt;/p&gt;

&lt;p&gt;Where does your API announce completion before it has finished releasing the attempt?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>A Valid Lookup Can Still Be Historically Wrong</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sat, 22 Aug 2026 07:20:20 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-valid-lookup-can-still-be-historically-wrong-2gmp</link>
      <guid>https://dev.to/iqtechsolutions/a-valid-lookup-can-still-be-historically-wrong-2gmp</guid>
      <description>&lt;p&gt;A configuration lookup can succeed, return a perfectly valid object, and still give the wrong answer.&lt;/p&gt;

&lt;p&gt;The problem appears when we ask a present-day source to explain a past decision. Configuration answers, “What is true now?” Historical records often need to answer, “What was the user told then?” Those questions look similar in code, but they have different integrity requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug hiding inside a successful lookup
&lt;/h2&gt;

&lt;p&gt;Imagine a C# service that creates user-visible hand-off instructions. At creation time it resolves a destination from configuration, generates a reference, saves an instruction record, and returns both values to the user.&lt;/p&gt;

&lt;p&gt;Later, the destination changes. Perhaps a fulfilment route changes or an external service is replaced. When the user reopens the old instruction, the application resolves the destination again.&lt;/p&gt;

&lt;p&gt;The lookup succeeds. The new destination is valid. The old reference is valid.&lt;/p&gt;

&lt;p&gt;Together, however, they describe an instruction that never existed.&lt;/p&gt;

&lt;p&gt;No exception is thrown. Monitoring stays quiet. Every component may satisfy its local contract while the system rewrites its own history.&lt;/p&gt;

&lt;p&gt;A valid lookup can still be historically wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist the issued output
&lt;/h2&gt;

&lt;p&gt;The safer design is to store the minimum output that the user or another system acted on when the record is created.&lt;/p&gt;

&lt;p&gt;Here is a deliberately invented example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InstructionRecord&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;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Reference&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;IssuedDestinationName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;IssuedDestinationCode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&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;bool&lt;/span&gt; &lt;span class="n"&gt;HasIssuedSnapshot&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IssuedDestinationName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IssuedDestinationCode&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 write path resolves configuration once, then saves the resulting values with the record:&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;destination&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;destinationProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResolveAsync&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;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InstructionRecord&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;Reference&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;referenceFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;IssuedDestinationName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IssuedDestinationCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;db&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="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is not the syntax. It is the source of truth. A later read reconstructs the old instruction from the stored snapshot, not from the current provider.&lt;/p&gt;

&lt;p&gt;Persisting only the route or configuration key is often insufficient. That key tells us which branch was chosen, but the branch can still return different values later. If exact re-display matters, save the issued result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat legacy nulls as honest uncertainty
&lt;/h2&gt;

&lt;p&gt;Adding snapshots to an established table creates a difficult question: what should happen to existing rows?&lt;/p&gt;

&lt;p&gt;An additive EF Core migration with nullable columns is often a practical deployment choice. New application code can write snapshots without requiring a risky backfill or a lockstep release. Old records remain readable.&lt;/p&gt;

&lt;p&gt;But a null snapshot has meaning: the system cannot prove what was issued.&lt;/p&gt;

&lt;p&gt;Backfilling those rows from current configuration would make the database look complete while inventing historical facts. That is worse than leaving the uncertainty visible.&lt;/p&gt;

&lt;p&gt;A safer read model makes the distinction explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;DestinationSnapshot&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HasIssuedSnapshot&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IssuedDestinationName&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IssuedDestinationCode&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user experience can then behave conservatively. For a snapshotted record, offer exact re-display. For a legacy row, withhold that action and ask the user to create a fresh instruction. This is not a temporary fallback to delete after rollout. Unless trustworthy historical evidence becomes available, it is the permanent safe behaviour.&lt;/p&gt;

&lt;p&gt;In data modelling, “unknown” is often a valuable state. Hiding it with a guessed value does not improve integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off is real
&lt;/h2&gt;

&lt;p&gt;Snapshots duplicate data, and duplicated data demands discipline. You need bounded field lengths, appropriate access controls, retention rules, and a clear decision about which values are necessary. If the generated output includes secrets or sensitive personal data, copying it casually may create a larger problem than the one being solved.&lt;/p&gt;

&lt;p&gt;Snapshots are also stale by design. That is their purpose. Current configuration remains authoritative for new instructions; the stored snapshot is authoritative for explaining an old one.&lt;/p&gt;

&lt;p&gt;The benefit is a self-contained historical record. Support teams do not need to search messages or logs to reconstruct what happened. Users see the same instruction again. Audits can distinguish two records created under different configuration, even if both are inspected after the configuration changed.&lt;/p&gt;

&lt;p&gt;The cost is extra schema and a permanent compatibility path. The return is historical correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundary, not only the calculation
&lt;/h2&gt;

&lt;p&gt;Useful tests for this pattern focus on the seam between mutable configuration and durable history:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact resolved values are persisted at creation;&lt;/li&gt;
&lt;li&gt;different configuration branches produce distinguishable stored snapshots;&lt;/li&gt;
&lt;li&gt;reads use the snapshot rather than resolving current configuration;&lt;/li&gt;
&lt;li&gt;a legacy row without a snapshot does not enable exact re-display;&lt;/li&gt;
&lt;li&gt;changing configuration after creation does not change the old instruction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson here came from read-only inspection of a recent committed change and its focused tests. I inspected what those tests assert, but I did not rerun them. That supports a design observation, not a claim about the current runtime or build state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical decision rule
&lt;/h2&gt;

&lt;p&gt;Before adding a snapshot, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did a person or external system act on this output?&lt;/li&gt;
&lt;li&gt;Can the source values change independently?&lt;/li&gt;
&lt;li&gt;Must we reproduce the old output later?&lt;/li&gt;
&lt;li&gt;Would reconstructing it incorrectly cause harm or confusion?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answers point toward history, persist the minimum safe output at write time. Keep current configuration for new decisions, stored snapshots for past decisions, and null for history you genuinely cannot prove.&lt;/p&gt;

&lt;p&gt;That separation is small in code, but it prevents a successful lookup from quietly changing the past.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>efcore</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
