<?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 (@ivan_rossouw_63dff0f4084e).</description>
    <link>https://dev.to/ivan_rossouw_63dff0f4084e</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%2F4181139%2Ff4929535-a44a-4ac4-a466-474ebb2a7ceb.jpg</url>
      <title>DEV Community: Ivan Rossouw</title>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ivan_rossouw_63dff0f4084e"/>
    <language>en</language>
    <item>
      <title>A Stopwatch Needs an Isolated Lane</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Wed, 28 Oct 2026 11:22:18 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/a-stopwatch-needs-an-isolated-lane-291j</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/a-stopwatch-needs-an-isolated-lane-291j</guid>
      <description>&lt;p&gt;Timing tests look wonderfully objective. Start a stopwatch, call the component, stop the watch, and compare the result with a budget.&lt;/p&gt;

&lt;p&gt;The number is precise. The experiment may not be.&lt;/p&gt;

&lt;p&gt;That distinction matters in continuous integration, where many tests share CPU time, thread pools, network stacks, containers, and database capacity. A wall-clock assertion can fail because the product became slower. It can also fail because the test runner became busier. If we cannot distinguish those two explanations, the test is producing urgency without producing useful evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract was reasonable
&lt;/h2&gt;

&lt;p&gt;Consider a service with separate liveness and readiness checks. Liveness says the process is still running. Readiness says the instance can safely accept work.&lt;/p&gt;

&lt;p&gt;One integration test deliberately points the service at an unreachable dependency. The expected behavior is not success. The service should return an unavailable response, and it should do so within a fixed upper bound. That is a meaningful reliability contract: an orchestrator needs a quick, honest answer rather than a request that hangs behind retry policies intended for normal user operations.&lt;/p&gt;

&lt;p&gt;The test boots the real host, makes the request, verifies the unavailable result, and checks elapsed time.&lt;/p&gt;

&lt;p&gt;So far, so good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure blamed the wrong interval
&lt;/h2&gt;

&lt;p&gt;The timing assertion began failing repeatedly in CI. Raising the limit would have made the build green. Adding an automatic retry might also have hidden the symptom.&lt;/p&gt;

&lt;p&gt;Neither action would have explained the result.&lt;/p&gt;

&lt;p&gt;The endpoint implementation had not changed during the relevant window. More importantly, the surrounding timing evidence showed that host startup in the same test took roughly as long as the delay attributed to the readiness request. The test also ran beside many parallel integration classes doing heavier work.&lt;/p&gt;

&lt;p&gt;The stopwatch was attached to one request, but that request still depended on threads scheduled in a shared process. Under contention, continuation work can wait. Startup can wait. Connection failure can be observed later than expected. The test reports wall-clock time, not pure execution time.&lt;/p&gt;

&lt;p&gt;This does not prove the service is fast. It proves the original experiment was unable to tell product latency from runner contention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the budget before changing it
&lt;/h2&gt;

&lt;p&gt;When a timing test turns red, I prefer to keep the bound intact while investigating. The bound represents a real operational expectation. Relaxing it before understanding the measurement destroys evidence that may still be valuable.&lt;/p&gt;

&lt;p&gt;A useful first pass is to separate three intervals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Setup&lt;/strong&gt; — host boot, fixture creation, migrations, container startup, and test data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contention&lt;/strong&gt; — time spent waiting for CPU, threads, sockets, database slots, or another shared resource.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior&lt;/strong&gt; — the operation whose latency the assertion is meant to protect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instrumenting those boundaries does not require a production profiler. Even a few carefully placed timestamps can reveal that the supposedly slow operation accounts for only a small part of the elapsed test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolate the claim, not the whole suite
&lt;/h2&gt;

&lt;p&gt;The narrow repair was to move only the timing proof into a non-parallel test collection.&lt;/p&gt;

&lt;p&gt;The response status did not change. The time budget did not change. The endpoint code did not change. Functional readiness tests stayed in the normal parallel suite. Only the test making a wall-clock claim received an isolated measurement lane.&lt;/p&gt;

&lt;p&gt;That scope matters. Serializing an entire integration project can turn a useful diagnosis into an expensive habit. Parallel tests are valuable because they shorten feedback cycles and expose accidental shared state. The aim is not to remove parallelism. It is to stop using an uncontrolled parallel environment as a precision instrument.&lt;/p&gt;

&lt;p&gt;I like to keep separate tests for separate questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the check return the correct status when the dependency is unavailable?&lt;/li&gt;
&lt;li&gt;Does the response avoid leaking infrastructure details?&lt;/li&gt;
&lt;li&gt;Does the host remain alive while readiness is false?&lt;/li&gt;
&lt;li&gt;Under controlled contention, does the readiness answer stay within its operational budget?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the last question necessarily needs timing isolation.&lt;/p&gt;

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

&lt;p&gt;An isolated collection adds a serial tail to the build. It also creates another test boundary that future maintainers must understand. If many tests demand exclusive execution, the suite can become slow and the isolation can conceal real capacity problems.&lt;/p&gt;

&lt;p&gt;That is why this technique should be applied to the measurement contract, not used as a blanket cure for flakiness.&lt;/p&gt;

&lt;p&gt;In return, failures become more actionable. A red timing check is less likely to mean “the runner happened to be crowded” and more likely to mean “the protected latency contract changed.” The team spends less time rerunning the same experiment and less time normalizing noise by widening budgets.&lt;/p&gt;

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

&lt;p&gt;Before changing a failed CI time limit, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the measured implementation change?&lt;/li&gt;
&lt;li&gt;Is setup included in the timed region?&lt;/li&gt;
&lt;li&gt;Which resources are shared with parallel tests?&lt;/li&gt;
&lt;li&gt;Does the test need fixtures it is currently competing for?&lt;/li&gt;
&lt;li&gt;Can functional correctness remain parallel while the timing proof runs alone?&lt;/li&gt;
&lt;li&gt;Would the same isolation exist in the production condition we care about?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last question prevents overfitting. An isolated test should remove irrelevant test-runner noise, not erase meaningful load. If the production contract is specifically about behavior under concurrency, create a controlled load test with explicit pressure instead of relying on incidental CI contention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Precision is a property of the experiment
&lt;/h2&gt;

&lt;p&gt;A stopwatch does not make a test scientific by itself. The environment, boundaries, and competing work determine what the number means.&lt;/p&gt;

&lt;p&gt;Keep meaningful budgets. Investigate before retrying. Isolate the smallest timing claim that needs a controlled lane, and leave the rest of the suite parallel.&lt;/p&gt;

&lt;p&gt;The result is not merely a greener pipeline. It is a test whose failure tells you something you can act on.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>devops</category>
      <category>reliability</category>
      <category>testing</category>
    </item>
    <item>
      <title>A Query Window Is Not a Calendar Day</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Mon, 26 Oct 2026 20:12:14 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/a-query-window-is-not-a-calendar-day-kji</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/a-query-window-is-not-a-calendar-day-kji</guid>
      <description>&lt;p&gt;Software often uses broad time windows for good reasons. A dashboard may need recent history, work that is actionable now, and the next scheduled item. Fetching all of that in one bounded query can be efficient and easy to reason about.&lt;/p&gt;

&lt;p&gt;The trouble begins when the retrieval window quietly becomes the business definition of “today.”&lt;/p&gt;

&lt;p&gt;This is a small distinction with a very visible failure mode: shortly after midnight, yesterday’s missed item appears under today’s heading. The query is returning exactly what it was asked to return. The selection rule is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval and meaning are different jobs
&lt;/h2&gt;

&lt;p&gt;Imagine a “Today” screen that loads items from several hours in the past through the next day. The look-back allows the screen to include work that recently closed or may still be open. The look-ahead allows it to show what comes next.&lt;/p&gt;

&lt;p&gt;That range is a retrieval concern. It answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which records could be relevant enough to inspect?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which record belongs to the user’s current calendar day?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If selection simply chooses the first item whose state is due or missed, a previous day’s missed item can win after midnight. If display text assumes every future item is later today, tomorrow morning can be described as though it were later tonight.&lt;/p&gt;

&lt;p&gt;Both errors come from using temporal proximity as a substitute for calendar meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define “today” in the relevant time zone
&lt;/h2&gt;

&lt;p&gt;Server time is rarely the correct authority for a human-facing day. UTC is excellent for storing instants and comparing elapsed time. It is not a user’s calendar.&lt;/p&gt;

&lt;p&gt;The safer sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the current instant from an injected clock.&lt;/li&gt;
&lt;li&gt;Convert it once into the time zone that owns the workflow.&lt;/li&gt;
&lt;li&gt;Derive a local date from that value.&lt;/li&gt;
&lt;li&gt;Use that date when classifying calendar-bound states.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Computing the value once matters. It keeps every decision in one request anchored to the same instant, including the unlikely case where execution crosses midnight.&lt;/p&gt;

&lt;p&gt;The relevant time zone also needs to be explicit. It may belong to a person, site, account, or workflow. The correct choice is a domain decision, not an infrastructure default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the exception that actually matters
&lt;/h2&gt;

&lt;p&gt;A tempting fix is to discard every record whose local date is not today. That is neat and wrong when an actionable window crosses midnight.&lt;/p&gt;

&lt;p&gt;Suppose a task opened before midnight and remains valid until shortly afterwards. A strict date filter would hide it at the exact moment it still needs attention.&lt;/p&gt;

&lt;p&gt;This is where explicit domain rules beat blanket filtering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missed and completed items count as current only when their local date is today;&lt;/li&gt;
&lt;li&gt;a still-open item remains current while it is actionable, regardless of its start date;&lt;/li&gt;
&lt;li&gt;a scheduled item on another day can still appear as next, but its wording names that day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule is slightly longer because the product meaning is slightly richer. That extra branch is not accidental complexity. It records why two superficially similar records are treated differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make wording part of correctness
&lt;/h2&gt;

&lt;p&gt;Time bugs are not confined to sorting and filtering. Labels can encode false claims too.&lt;/p&gt;

&lt;p&gt;“Later today” is only correct if the next item is on the same local day. “Between 08:00 and 09:00” may be ambiguous when viewed the night before. Adding “tomorrow” or a concrete day makes the model visible to the user.&lt;/p&gt;

&lt;p&gt;That wording also gives tests a useful public outcome. A test can verify that the screen says tomorrow without reaching into a private helper or asserting a particular LINQ expression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundary as a sequence
&lt;/h2&gt;

&lt;p&gt;A single test at noon will not find this class of defect. The useful test moves a controllable clock through the state transition.&lt;/p&gt;

&lt;p&gt;One focused integration scenario can prove three behaviours:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the item’s own evening, the missed item is still classified as belonging to today.&lt;/li&gt;
&lt;li&gt;The next day’s scheduled item is labelled explicitly as tomorrow.&lt;/li&gt;
&lt;li&gt;After midnight, the old missed item no longer becomes the current card.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using the real persistence provider is valuable when stored dates, generated occurrences, or query translation participate in the behaviour. The clock is the external boundary worth controlling; internal services do not need to be mocked if the public result proves the rule.&lt;/p&gt;

&lt;p&gt;Also test a window that genuinely spans midnight. Without that case, a cleanup that filters everything by date can look correct while introducing a different regression.&lt;/p&gt;

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

&lt;p&gt;Explicit local-date classification costs a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more branches than a simple state check;&lt;/li&gt;
&lt;li&gt;a time-zone conversion at the application boundary;&lt;/li&gt;
&lt;li&gt;boundary-focused tests and more deliberate copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In return, the system becomes predictable at the moment when implicit assumptions fail most often. Retrieval can remain broad for performance and completeness. Domain selection becomes precise. User-facing language reflects the same model.&lt;/p&gt;

&lt;p&gt;The broader leadership lesson is to ask two questions whenever a time range appears in code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is this range finding candidates, or defining business membership?&lt;/li&gt;
&lt;li&gt;Whose calendar gives words such as today, tomorrow, and missed their meaning?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keeping those answers separate prevents an implementation convenience from becoming an accidental policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;p&gt;When building a day-oriented view:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store and compare instants consistently;&lt;/li&gt;
&lt;li&gt;identify the domain-owned time zone;&lt;/li&gt;
&lt;li&gt;derive the local date once per operation;&lt;/li&gt;
&lt;li&gt;keep retrieval windows separate from classification rules;&lt;/li&gt;
&lt;li&gt;preserve intentionally actionable cross-midnight windows;&lt;/li&gt;
&lt;li&gt;make off-day wording explicit;&lt;/li&gt;
&lt;li&gt;test before midnight, after midnight, and a spanning window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code may still query eighteen or thirty hours. The user should experience a calendar day.&lt;/p&gt;

</description>
      <category>reliability</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <item>
      <title>Idempotency Should Resume Partial Work, Not Hide It</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:34:31 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/idempotency-should-resume-partial-work-not-hide-it-35i6</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/idempotency-should-resume-partial-work-not-hide-it-35i6</guid>
      <description>&lt;p&gt;Idempotency is usually introduced with a simple promise: run the operation again and it will not create a duplicate.&lt;/p&gt;

&lt;p&gt;That promise is useful, but it can become dangerous when we apply it to an entire workflow instead of the specific durable step it protects.&lt;/p&gt;

&lt;p&gt;I recently reviewed a recurring integration job with two independent writes. First, it appended a status note to an external record. Second, it moved that record into a terminal state. The note contained a unique marker, so retries could see that the note had already been written.&lt;/p&gt;

&lt;p&gt;The marker worked. The workflow did not.&lt;/p&gt;

&lt;p&gt;When the state transition failed once, the note remained. On the next sweep, the job saw the marker and skipped the record completely. A local sign of success for step one had accidentally become a global completion flag for steps one and two.&lt;/p&gt;

&lt;p&gt;That is a subtle reliability bug because the automation appears idempotent while the real process is only half finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checkpoint is not the whole journey
&lt;/h2&gt;

&lt;p&gt;Consider a generalized background job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a durable note with a unique operation marker.&lt;/li&gt;
&lt;li&gt;Move the remote record into a terminal workflow state.&lt;/li&gt;
&lt;li&gt;Repeat the scan for a bounded period so temporary failures can heal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The marker proves exactly one fact: the note exists.&lt;/p&gt;

&lt;p&gt;It does not prove that the state move succeeded. It does not prove that a notification was delivered, a file was copied, or a later API call completed. Yet it is tempting to write an early return such as “marker found, therefore this item is done.”&lt;/p&gt;

&lt;p&gt;The safer model gives each durable step its own completion evidence. In this case, the marker suppresses only another note. Later sweeps still inspect the remote record and decide whether the second step is pending.&lt;/p&gt;

&lt;p&gt;This sounds like a small distinction. In production, it is the difference between duplicate prevention and recovery prevention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation makes partial completion explicit
&lt;/h2&gt;

&lt;p&gt;A resumable job expects to encounter intermediate states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no note and no terminal state;&lt;/li&gt;
&lt;li&gt;note present but transition still pending;&lt;/li&gt;
&lt;li&gt;note present and record already terminal;&lt;/li&gt;
&lt;li&gt;note present but a person changed the record afterwards;&lt;/li&gt;
&lt;li&gt;remote workflow metadata that no longer matches our assumptions;&lt;/li&gt;
&lt;li&gt;a temporary failure that should be tried again;&lt;/li&gt;
&lt;li&gt;a permanent incompatibility that needs attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not exotic edge cases. They are the normal state space of a multi-step integration.&lt;/p&gt;

&lt;p&gt;The repaired design runs the state decision on every bounded reconciliation sweep. If the note is missing, it writes the note once. Whether the note was just written or already existed, it then evaluates the transition separately.&lt;/p&gt;

&lt;p&gt;That separation makes the control flow honest. We can retry the unfinished part without repeating the finished part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask the external system what its workflow means
&lt;/h2&gt;

&lt;p&gt;The original failure was also a reminder that external state names are rarely universal. One workflow may call its terminal state “Done”; another may use a different label. Hard-coding one friendly name works until a customized process rejects it.&lt;/p&gt;

&lt;p&gt;The stronger approach reads the record's type and the available states for that type. It prefers an explicitly configured state when that state exists. Otherwise, it can select a state by semantic category, such as completed or removed.&lt;/p&gt;

&lt;p&gt;This adds metadata reads and some branching. It also moves compatibility knowledge to the boundary where it belongs. The integration stops pretending that its vocabulary is the external system's vocabulary.&lt;/p&gt;

&lt;p&gt;That fallback must remain bounded. If no suitable category exists, the job should surface the incompatibility rather than inventing a transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human intent outranks automation
&lt;/h2&gt;

&lt;p&gt;Retrying forever is not the same as reconciling safely.&lt;/p&gt;

&lt;p&gt;Suppose the automation wrote its note yesterday, then a person deliberately reopened or reclassified the record today. A later sweep should not force the record back into the automation's preferred state.&lt;/p&gt;

&lt;p&gt;The reconciliation rule therefore needs ordering evidence. If the external state changed after our durable note, leave it alone. If ordering is unknown, prefer the conservative choice and do not overwrite. If the record is already in any terminal category, there is nothing to repair.&lt;/p&gt;

&lt;p&gt;This is an important boundary: the job owns completion of its own intent, not the external workflow itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry by failure class
&lt;/h2&gt;

&lt;p&gt;Not every failure deserves the same response.&lt;/p&gt;

&lt;p&gt;A timeout or temporary service error can be retried by the next sweep. A missing permission or incompatible workflow definition will not improve through repetition. That condition should be reported, with noise controlled, while unrelated records continue.&lt;/p&gt;

&lt;p&gt;This classification keeps the job useful during partial outages. One bad record does not starve the batch, and operators are not flooded with alerts for failures that the normal reconciliation loop can heal.&lt;/p&gt;

&lt;p&gt;Caching workflow metadata for the duration of one sweep can also limit API traffic without letting old assumptions live indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the half-finished states
&lt;/h2&gt;

&lt;p&gt;Happy-path tests are not enough for resumable automation. The valuable tests begin after something durable has already happened.&lt;/p&gt;

&lt;p&gt;I would want focused coverage proving that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an existing marker prevents a duplicate note but not a pending transition;&lt;/li&gt;
&lt;li&gt;a previously failed transition is attempted on a later sweep;&lt;/li&gt;
&lt;li&gt;different state names can resolve through semantic metadata;&lt;/li&gt;
&lt;li&gt;terminal records are left untouched;&lt;/li&gt;
&lt;li&gt;a newer human change is preserved;&lt;/li&gt;
&lt;li&gt;unknown ordering is handled conservatively;&lt;/li&gt;
&lt;li&gt;one record's failure does not block the others;&lt;/li&gt;
&lt;li&gt;temporary and permanent failures take different paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests describe the recovery contract more clearly than a mock verifying one ideal call sequence.&lt;/p&gt;

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

&lt;p&gt;For every multi-step job, list the durable evidence produced by each step. Then ask what the next run should do when execution stops after any one of them.&lt;/p&gt;

&lt;p&gt;Idempotency should let a workflow resume safely. If an “already processed” check prevents the system from finishing later steps, it is not protecting reliability. It is hiding incomplete work.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Let the Router Own the Next Paint</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:06:23 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/let-the-router-own-the-next-paint-500n</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/let-the-router-own-the-next-paint-500n</guid>
      <description>&lt;p&gt;Navigation bugs are often judged by their final state. Did the correct page appear? Did the authorization check pass? Did the URL settle on the expected address?&lt;/p&gt;

&lt;p&gt;Those questions matter, but they can miss the frame that users actually notice.&lt;/p&gt;

&lt;p&gt;In a recent component-based web fix, the destination page was correct. The address was correct. The authorization result was correct. Yet, between the address change and the destination render, the departing page painted once more. Because the framework also applied its scroll reset during that interval, users briefly saw stale content at the top of the viewport before the new page arrived.&lt;/p&gt;

&lt;p&gt;The repair produced a useful general lesson: after an address change, the router should own the next visible paint.&lt;/p&gt;

&lt;h2&gt;
  
  
  A correct destination can still have a wrong journey
&lt;/h2&gt;

&lt;p&gt;An end-state test sees a simple sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;start on page A;&lt;/li&gt;
&lt;li&gt;navigate to page B;&lt;/li&gt;
&lt;li&gt;assert that page B is visible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The browser may have seen something richer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;page A is visible at the user’s current scroll position;&lt;/li&gt;
&lt;li&gt;the address changes to B;&lt;/li&gt;
&lt;li&gt;page A renders again under B’s address;&lt;/li&gt;
&lt;li&gt;a framework scroll reset moves that stale page to the top;&lt;/li&gt;
&lt;li&gt;page B finally renders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps three and four may last only one frame. They are still part of the product.&lt;/p&gt;

&lt;p&gt;This is why navigation correctness is not only about eventual state. It also includes the ordered render sequence between the user action and the destination becoming authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden effect of an event receiver
&lt;/h2&gt;

&lt;p&gt;The source of the extra paint was subtle. An asynchronous navigation callback was registered with the component that hosted the route tree as its receiver.&lt;/p&gt;

&lt;p&gt;In many component frameworks, completing an event callback schedules a render of the receiving component. That behaviour is normally helpful: event handlers change state, so the framework redraws the owner automatically.&lt;/p&gt;

&lt;p&gt;Navigation is different. The callback prepared the transition, but the router—not the departing component—owned the destination. When the receiver-bound callback completed, the framework rendered the old route tree once more. The router rendered the new page immediately afterward, but it was no longer responsible for the first batch after the address changed.&lt;/p&gt;

&lt;p&gt;The fix did not add a delay, loading overlay, or animation. It removed the accidental receiver. The callback still ran, but its completion no longer implied that the departing route component needed to render. The router’s destination batch became the first visible paint after the address change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the interval, not just the answer
&lt;/h2&gt;

&lt;p&gt;The strongest part of the repair was the test shape.&lt;/p&gt;

&lt;p&gt;Instead of sleeping and hoping to catch a flicker, the test marked the instant the address changed. It then counted every component render until the destination page entered the render tree.&lt;/p&gt;

&lt;p&gt;Before the change, that interval contained twelve render notifications in the exercised path. After the change, it contained zero.&lt;/p&gt;

&lt;p&gt;The specific number is less important than the invariant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nothing else paints after the address changes and before the destination appears.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assertion is deterministic. It does not depend on monitor refresh rate, screenshot timing, or a human noticing a flash. It also fails for the causal behaviour rather than a visual approximation of the symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the old page has a boundary
&lt;/h2&gt;

&lt;p&gt;There is a reasonable usability goal behind retaining the current page: an established screen is often a better placeholder than a blank shell while the destination prepares.&lt;/p&gt;

&lt;p&gt;But retained content needs a strict boundary. The old page belongs to the source route and its source authorization decision. It must not start making decisions for the new address, consume state prepared for the destination, or imply that access to the destination has been granted.&lt;/p&gt;

&lt;p&gt;Security invalidation is the important exception. If the user signs out or loses a role, the retained page must stop being authoritative immediately. Smoothness must never extend stale permission.&lt;/p&gt;

&lt;p&gt;That gives us two complementary rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retain a certified source view only until its replacement is ready;&lt;/li&gt;
&lt;li&gt;invalidate it immediately when the identity or authorization basis changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is continuity without authority leakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the negative space
&lt;/h2&gt;

&lt;p&gt;A good transition test suite should cover more than the successful move between two ordinary pages.&lt;/p&gt;

&lt;p&gt;Useful cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an allowed destination;&lt;/li&gt;
&lt;li&gt;a refused destination;&lt;/li&gt;
&lt;li&gt;a layout or shell change;&lt;/li&gt;
&lt;li&gt;sign-out during preparation;&lt;/li&gt;
&lt;li&gt;role loss during preparation;&lt;/li&gt;
&lt;li&gt;an older asynchronous result completing after a newer transition;&lt;/li&gt;
&lt;li&gt;a callback that finishes on a different scheduling context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cases reveal who truly owns the transition. If several components can repaint or decide access for the same interval, the design is probably carrying more than one authority.&lt;/p&gt;

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

&lt;p&gt;When reviewing client-side navigation, draw a short timeline: user action, address change, preparation, first destination render, and interaction-ready state.&lt;/p&gt;

&lt;p&gt;For each gap, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which component may render?&lt;/li&gt;
&lt;li&gt;Which route does that render belong to?&lt;/li&gt;
&lt;li&gt;Which authorization decision supports it?&lt;/li&gt;
&lt;li&gt;Can an event callback schedule an incidental redraw?&lt;/li&gt;
&lt;li&gt;What happens if identity changes or an older task completes late?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then write at least one sequence assertion. Final-state tests remain valuable, but they should be paired with a guard over the transition when a single wrong frame matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leadership lesson
&lt;/h2&gt;

&lt;p&gt;Small visual defects often expose unclear ownership rather than missing polish. A loading spinner might hide the flash, but it would not explain why the departing component could repaint under the destination address.&lt;/p&gt;

&lt;p&gt;The more durable repair is to assign one owner to the critical interval and test that ownership directly.&lt;/p&gt;

&lt;p&gt;Users experience every frame, not only the last one. Our tests should be willing to do the same.&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Tolerant Decoding, Strict Release Gates</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Fri, 23 Oct 2026 06:38:40 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/tolerant-decoding-strict-release-gates-28e</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/tolerant-decoding-strict-release-gates-28e</guid>
      <description>&lt;p&gt;A release verifier has two jobs that are easy to blur together. It must understand the representation produced by the build chain, and it must enforce the policy that decides whether an artifact may leave that chain.&lt;/p&gt;

&lt;p&gt;Those jobs need different attitudes.&lt;/p&gt;

&lt;p&gt;Decoding can be narrowly tolerant of documented, meaning-preserving variation. Release policy should remain exact and fail closed.&lt;/p&gt;

&lt;p&gt;That distinction surfaced in a recent mobile build repair. A generated JSON settings file could begin with the optional UTF-8 byte-order marker. The JSON content was valid, but the verifier decoded it as plain UTF-8 and failed before it could evaluate the values that actually governed release safety.&lt;/p&gt;

&lt;p&gt;The fix was not to “clean up” the input generally. It was to accept one specific encoding variation, then run the same strict semantic checks as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Representation is not policy
&lt;/h2&gt;

&lt;p&gt;A byte-order marker sits at the representation boundary. In this case, accepting UTF-8 with or without that marker did not change the JSON values.&lt;/p&gt;

&lt;p&gt;An endpoint or feature-handoff setting is different. It changes what the package will do after installation. That is policy-bearing data.&lt;/p&gt;

&lt;p&gt;Treating both categories as raw bytes creates a brittle gate: a harmless encoding detail can block a valid package. Treating both categories as broadly normalisable creates a permissive gate: a meaningful difference may be trimmed, defaulted, coerced, or discarded before verification sees it.&lt;/p&gt;

&lt;p&gt;The useful middle ground is narrow tolerance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode only the representation variants the producer is documented to emit.&lt;/li&gt;
&lt;li&gt;Parse into an explicit data shape.&lt;/li&gt;
&lt;li&gt;Compare policy-bearing values exactly.&lt;/li&gt;
&lt;li&gt;Publish nothing until every check has passed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a small design choice, but it keeps compatibility and authority in their proper layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why broad normalisation is risky
&lt;/h2&gt;

&lt;p&gt;“Normalise before comparing” sounds sensible. Sometimes it is. The danger is that normalisation is often written without naming which differences are harmless.&lt;/p&gt;

&lt;p&gt;Should surrounding whitespace be trimmed? Should an empty string become null? Should a missing value receive a default? Should a hostname lose its trailing separator? Should key casing be ignored?&lt;/p&gt;

&lt;p&gt;Each transformation might be correct for one contract and wrong for another. Once a release gate performs all-purpose cleanup, reviewers can no longer see which deviations the gate intentionally accepts.&lt;/p&gt;

&lt;p&gt;A better question is: who owns this variation?&lt;/p&gt;

&lt;p&gt;The decoder owns permitted encoding markers. The JSON parser owns JSON syntax. The release policy owns the approved endpoint and capability settings. Package signing owns artifact identity. Device acceptance owns behaviour on the target platform.&lt;/p&gt;

&lt;p&gt;Let each boundary make only the decision it has enough information to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test tolerance and strictness together
&lt;/h2&gt;

&lt;p&gt;A positive test for the newly accepted variant is necessary, but it is not enough. It proves compatibility; it does not prove that policy remained intact.&lt;/p&gt;

&lt;p&gt;The stronger test set combines the representation variant with both approved and disallowed semantics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a marker-bearing file with approved values passes;&lt;/li&gt;
&lt;li&gt;the verifier emits a clean, parseable receipt;&lt;/li&gt;
&lt;li&gt;a marker-bearing file with an unapproved endpoint fails;&lt;/li&gt;
&lt;li&gt;a marker-bearing file with an unapproved handoff configuration fails;&lt;/li&gt;
&lt;li&gt;either failure produces no distributable package and no success receipt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those negative cases are the important counterweight. They show that the change expanded the accepted representation, not the accepted deployment policy.&lt;/p&gt;

&lt;p&gt;The no-output assertions also matter. A verifier that reports failure after copying the package has already lost the race. Publication must happen after verification, not alongside it. The absence of a receipt is useful too: downstream automation must not mistake a partially completed run for approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the artifact boundary transactional
&lt;/h2&gt;

&lt;p&gt;File-based pipelines rarely have a database transaction around the whole release. We can still design them with transactional thinking.&lt;/p&gt;

&lt;p&gt;Build into a private staging location. Verify the package, its embedded settings, its signature, and any required metadata there. Only after the full decision succeeds should the pipeline copy or promote the artifact and write the corresponding receipt.&lt;/p&gt;

&lt;p&gt;On failure, the public output location should remain unchanged.&lt;/p&gt;

&lt;p&gt;This ordering turns the output directory into a simple commit boundary. Before promotion, the candidate is merely work in progress. After promotion, the package and its evidence agree.&lt;/p&gt;

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

&lt;p&gt;When one tool generates a file and another tool gates a release from it, review four things:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enumerate representation variants
&lt;/h3&gt;

&lt;p&gt;Record the encodings, markers, line endings, ordering guarantees, and optional fields the producer may legitimately emit. Avoid accepting variants merely because a parser can.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Identify policy-bearing fields
&lt;/h3&gt;

&lt;p&gt;List the values that change destination, authority, enabled capabilities, signing identity, or runtime behaviour. Compare them deliberately rather than through a generic object cleanup step.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Add crossed tests
&lt;/h3&gt;

&lt;p&gt;Test each permitted representation with approved values and with at least one disallowed value. This catches the accidental move from narrow compatibility to broad permissiveness.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Prove fail-closed publication
&lt;/h3&gt;

&lt;p&gt;Assert not only that verification returns failure, but also that no package, receipt, or other success signal appears in the publish location.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leadership lesson
&lt;/h2&gt;

&lt;p&gt;Strictness is not the same as rejecting every variation. Good gates are precise about what they tolerate and equally precise about what they refuse.&lt;/p&gt;

&lt;p&gt;That precision makes a pipeline easier to operate. Engineers can fix harmless toolchain differences without weakening release policy. Reviewers can see the exact compatibility allowance. Operators can trust that a failed gate did not leave an ambiguous artifact behind.&lt;/p&gt;

&lt;p&gt;The goal is not a gate that never bends. It is a gate that bends only where meaning is unchanged—and stays immovable where deployment meaning begins.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>dotnet</category>
      <category>mobile</category>
      <category>devops</category>
    </item>
    <item>
      <title>An Empty Field Is Still Signed Data</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:10:10 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/an-empty-field-is-still-signed-data-oop</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/an-empty-field-is-still-signed-data-oop</guid>
      <description>&lt;p&gt;An empty form field looks unimportant. In ordinary application logic, treating an empty value like a missing value is often reasonable. In signature verification, that assumption can reject a legitimate message—or, in a different protocol, verify a representation the sender never signed.&lt;/p&gt;

&lt;p&gt;The important lesson is simple: canonicalization is not generic data cleanup. It is part of the cryptographic protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tidy abstraction that hid a real difference
&lt;/h2&gt;

&lt;p&gt;Consider an integration with two flows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application builds an outbound form and signs it.&lt;/li&gt;
&lt;li&gt;A provider later sends an inbound form notification that the application verifies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both flows need URL encoding, ordered &lt;code&gt;key=value&lt;/code&gt; pairs, a digest, and a comparison. A shared helper therefore seems sensible.&lt;/p&gt;

&lt;p&gt;But suppose the protocol defines different empty-field rules. The outbound form omits empty values. The inbound notification includes every posted field, even an empty one represented as &lt;code&gt;description=&lt;/code&gt;. Reusing the outbound rule during verification silently removes part of the inbound canonical representation.&lt;/p&gt;

&lt;p&gt;Nothing is wrong with the network message. Nothing is wrong with the secret. The application simply hashes different bytes from the sender.&lt;/p&gt;

&lt;p&gt;That is the dangerous kind of bug: the code is deterministic, the crypto primitive behaves correctly, and the abstraction looks clean. The failure lives in the representation contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empty and absent are different inputs
&lt;/h2&gt;

&lt;p&gt;Application code tends to reason semantically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;note=&lt;/code&gt; means there is no useful note.&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;note&lt;/code&gt; field also means there is no useful note.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A signing algorithm reasons syntactically. It may see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reference=abc&amp;amp;note=&amp;amp;status=complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as a different message from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reference=abc&amp;amp;status=complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies to field order, repeated keys, character encoding, spaces, escaping, decimal formatting, and line endings. Two representations can produce the same business meaning while producing different signatures.&lt;/p&gt;

&lt;p&gt;That is why “normalize before verifying” is unsafe advice unless the protocol defines the normalization precisely. The verifier’s job is not to improve the input. Its job is to reconstruct exactly what the other side signed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Share mechanics, not contracts
&lt;/h2&gt;

&lt;p&gt;The repair pattern I prefer keeps the truly common pieces together while making direction-specific rules visible.&lt;/p&gt;

&lt;p&gt;Shared mechanics may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact form-encoding routine;&lt;/li&gt;
&lt;li&gt;digest calculation;&lt;/li&gt;
&lt;li&gt;secret or passphrase handling;&lt;/li&gt;
&lt;li&gt;fixed-time signature comparison;&lt;/li&gt;
&lt;li&gt;consistent fail-closed behaviour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The canonicalization policies remain separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;outbound signing can omit empty fields when the outbound specification says so;&lt;/li&gt;
&lt;li&gt;inbound verification can retain empty fields when the inbound specification says so;&lt;/li&gt;
&lt;li&gt;both can exclude the signature field itself;&lt;/li&gt;
&lt;li&gt;both can preserve their required ordering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This introduces a small amount of repetition. That is a healthy trade when the alternative is a boolean option buried inside a generic helper whose callers must remember invisible protocol rules. If a shared primitive is retained, give the public operations names and documentation that make the distinction impossible to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the representation, not your implementation
&lt;/h2&gt;

&lt;p&gt;Signature tests often contain a subtle circularity: the test calls the production signing helper to create the expected signature, then asks the same helper to verify it. The test proves internal consistency, not compatibility with the protocol.&lt;/p&gt;

&lt;p&gt;A stronger test builds the canonical input independently. For the inbound case, start from a literal ordered field sequence containing several &lt;code&gt;key=&lt;/code&gt; entries. Calculate the expected digest with a small test-only oracle or a published fixture. Then submit the body to the real verification path.&lt;/p&gt;

&lt;p&gt;Useful negative cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove one empty field and expect rejection;&lt;/li&gt;
&lt;li&gt;change field order and expect rejection when order is significant;&lt;/li&gt;
&lt;li&gt;replace an empty field with an absent field;&lt;/li&gt;
&lt;li&gt;vary spaces and reserved characters;&lt;/li&gt;
&lt;li&gt;include a blank expected signature and fail closed;&lt;/li&gt;
&lt;li&gt;change only the signature field and confirm it is excluded from canonical input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests describe the protocol boundary directly. They are harder for a refactor to accidentally “update” into agreeing with the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise the framework pipeline too
&lt;/h2&gt;

&lt;p&gt;A pure canonicalization test is necessary, but it is not sufficient for a web endpoint.&lt;/p&gt;

&lt;p&gt;Framework model binding and form parsing can consume a request stream, decode characters, merge values, or present fields through an API with different ordering behaviour. Middleware can also affect buffering or body access. A verifier that works against a handcrafted dictionary may still fail when the real HTTP pipeline handles the request first.&lt;/p&gt;

&lt;p&gt;Add one focused integration test that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;creates the literal encoded body;&lt;/li&gt;
&lt;li&gt;independently calculates the signature over that exact representation;&lt;/li&gt;
&lt;li&gt;sends an actual form POST through the application’s HTTP stack;&lt;/li&gt;
&lt;li&gt;verifies that the original body remains available where required;&lt;/li&gt;
&lt;li&gt;proves the request reaches the normal accepted outcome.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not an argument for turning every edge case into a slow end-to-end test. Keep the combinatorial matrix in fast unit tests. Use the HTTP-level case as a boundary witness that the framework and your canonicalization contract still agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification is necessary, not complete trust
&lt;/h2&gt;

&lt;p&gt;Correct signature verification answers a narrow question: does this message match the shared signing contract and secret?&lt;/p&gt;

&lt;p&gt;It does not automatically prove that the event is fresh, unique, authorized for the referenced resource, or safe to apply twice. Production handlers may still need replay protection, timestamp or nonce validation, amount and ownership checks, idempotency, audit evidence, and careful failure handling.&lt;/p&gt;

&lt;p&gt;Keeping that claim bounded matters. Strong security engineering is usually a chain of explicit checks, not one impressive cryptographic step.&lt;/p&gt;

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

&lt;p&gt;When reviewing a signed integration, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What exact bytes or characters are signed in each direction?&lt;/li&gt;
&lt;li&gt;Are empty and missing values distinct?&lt;/li&gt;
&lt;li&gt;Is field order defined and preserved?&lt;/li&gt;
&lt;li&gt;When does decoding or encoding happen?&lt;/li&gt;
&lt;li&gt;Are repeated fields possible?&lt;/li&gt;
&lt;li&gt;Does the comparison avoid timing leaks?&lt;/li&gt;
&lt;li&gt;Do invalid inputs fail closed?&lt;/li&gt;
&lt;li&gt;Is at least one expected signature calculated independently?&lt;/li&gt;
&lt;li&gt;Does a real HTTP test cover framework parsing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The broader engineering principle is worth carrying elsewhere: an abstraction should share sameness, not erase difference. At a cryptographic boundary, the awkward representational details are often the contract itself.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>csharp</category>
      <category>security</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Put the Concurrency Gate Before the Side Effect</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:40:05 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/put-the-concurrency-gate-before-the-side-effect-227</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/put-the-concurrency-gate-before-the-side-effect-227</guid>
      <description>&lt;p&gt;Many concurrency bugs begin with code that looks entirely reasonable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the current row.&lt;/li&gt;
&lt;li&gt;Check that the operation is still allowed.&lt;/li&gt;
&lt;li&gt;Create something durable elsewhere.&lt;/li&gt;
&lt;li&gt;Save the original row as complete.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run once, the workflow is correct. Run twice at almost the same time, and both callers can pass the check before either caller records completion. If step three is irreversible, the system can create duplicate work even though every individual condition was true when evaluated.&lt;/p&gt;

&lt;p&gt;The core lesson is simple: when a state transition guards a side effect, make the database choose the winner before the side effect begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why read-then-check is not ownership
&lt;/h2&gt;

&lt;p&gt;An application-level check answers, “What did I observe?” It does not answer, “Do I exclusively own this transition?”&lt;/p&gt;

&lt;p&gt;Imagine a parent record representing an open request. Accepting one option creates a downstream record and closes the parent. Two callers can follow this timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Caller A reads the parent as open.&lt;/li&gt;
&lt;li&gt;Caller B reads the same parent as open.&lt;/li&gt;
&lt;li&gt;Caller A creates a downstream record.&lt;/li&gt;
&lt;li&gt;Caller B creates another downstream record.&lt;/li&gt;
&lt;li&gt;Both callers try to close the parent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimistic concurrency on the final save may reject one update, but by then the rejected caller may already have created the duplicate side effect. Detecting a conflict after the irreversible action is too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move the decision forward
&lt;/h2&gt;

&lt;p&gt;A stronger shape puts a conditional write immediately before downstream creation. In generalized SQL, the intention is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;WorkItems&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;ClaimToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClaimedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Open'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ClaimToken&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The affected-row count becomes the decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One affected row means this caller owns the transition.&lt;/li&gt;
&lt;li&gt;Zero affected rows means another caller won, or the operation is no longer valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That losing result is not exceptional. It is a normal concurrency outcome and should stop before any downstream creation call.&lt;/p&gt;

&lt;p&gt;This is stronger than reading again because the predicate and the update are evaluated atomically by the database. There is no application-level gap between “still open” and “now mine.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim is a state, not a boolean trick
&lt;/h2&gt;

&lt;p&gt;Once a claim exists, the domain has an in-progress state whether or not the schema calls it that. Treating it explicitly improves the design.&lt;/p&gt;

&lt;p&gt;The workflow needs to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can a user retry while a claim is fresh?&lt;/li&gt;
&lt;li&gt;Which failures can safely release the claim?&lt;/li&gt;
&lt;li&gt;When is a claim considered abandoned?&lt;/li&gt;
&lt;li&gt;Can another caller take over an abandoned claim?&lt;/li&gt;
&lt;li&gt;What happens if the process fails after downstream creation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical rule is to release ownership only when the system knows that no durable side effect exists. If downstream creation fails before producing a record, clearing the claim can make a retry safe. Once a durable result may exist, blindly clearing the claim risks creating another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout is recovery policy, not exactly once
&lt;/h2&gt;

&lt;p&gt;A bounded stale-claim timeout prevents an interrupted process from blocking the workflow forever. A new caller may take over only if the stored claim and timestamp still match what it read, so competing recovery attempts cannot both win.&lt;/p&gt;

&lt;p&gt;That solves abandoned ownership before the side effect. It does not solve every crash window.&lt;/p&gt;

&lt;p&gt;Suppose the downstream service successfully creates a record and the process dies before the parent records completion. After the timeout, replaying the operation could create a second record. The conditional claim narrowed the risk, but it did not create a distributed transaction.&lt;/p&gt;

&lt;p&gt;For that boundary, choose an additional mechanism according to the cost of duplication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass a stable idempotency key to the downstream operation.&lt;/li&gt;
&lt;li&gt;Record an outbox message in the same transaction as the claim.&lt;/li&gt;
&lt;li&gt;Make downstream creation upsert by the originating command identity.&lt;/li&gt;
&lt;li&gt;Reconcile incomplete claims against downstream records before replay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest claim is “one winner during concurrent admission,” not “exactly once under every failure.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the overlap, not two sequential calls
&lt;/h2&gt;

&lt;p&gt;A concurrency fix deserves a test that opens the original race window deliberately.&lt;/p&gt;

&lt;p&gt;Hold the first caller inside downstream creation. While it is paused, submit the same command again and also submit a competing choice. Then release the first caller.&lt;/p&gt;

&lt;p&gt;The useful assertions are concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The downstream creation service was called once.&lt;/li&gt;
&lt;li&gt;One winner completed.&lt;/li&gt;
&lt;li&gt;Both competing calls received a clear losing result.&lt;/li&gt;
&lt;li&gt;The parent stored one selected result.&lt;/li&gt;
&lt;li&gt;Other candidates moved to their expected terminal state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add two recovery tests as well. First, make downstream creation fail before producing anything and prove that a retry can acquire the released claim. Second, seed a fresh claim and an expired claim, proving that the fresh claim blocks while the expired one can be reclaimed.&lt;/p&gt;

&lt;p&gt;This test shape is more valuable than sending two tasks and hoping their timing overlaps. A controlled pause makes the race deterministic and keeps the assertion focused on the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact design checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a command that crosses a data boundary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the first irreversible side effect.&lt;/li&gt;
&lt;li&gt;Put the ownership decision before it.&lt;/li&gt;
&lt;li&gt;Use one conditional database write, not a read-then-write promise.&lt;/li&gt;
&lt;li&gt;Treat zero affected rows as a normal loser.&lt;/li&gt;
&lt;li&gt;Release only when no durable effect exists.&lt;/li&gt;
&lt;li&gt;Define stale ownership and takeover semantics.&lt;/li&gt;
&lt;li&gt;Add idempotency or reconciliation for the post-side-effect crash window.&lt;/li&gt;
&lt;li&gt;Test with a deliberately paused winner and active losers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The broader principle is not limited to any framework. Checks describe observations. Conditional writes grant ownership. When a workflow can create something you cannot casually undo, that distinction is part of correctness.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Test the Owner, Not the Teardown</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:40:53 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/test-the-owner-not-the-teardown-3p31</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/test-the-owner-not-the-teardown-3p31</guid>
      <description>&lt;p&gt;Flaky lifecycle tests often produce a tempting diagnosis: the cleanup code must be unreliable. Sometimes that is true. Sometimes the test is asking a framework harness to prove a narrower ownership contract, and the harness is free to finish work on a different schedule.&lt;/p&gt;

&lt;p&gt;That distinction matters in interactive server-side UI systems. A component may subscribe to live updates, start a timer, register an observer, or attach a callback. The resource needs a clear owner and a reliable release path. But the test for that contract should not depend entirely on whether a whole rendered tree has completed teardown at one particular instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the failure
&lt;/h2&gt;

&lt;p&gt;Consider a header component that subscribes to a process-local notification service. A child pop-up also owns a separate subscription. The component test renders the tree, publishes an update, observes the visual change, disposes the test context, and immediately expects the global subscription count to be zero.&lt;/p&gt;

&lt;p&gt;That sounds reasonable. It is also broader than the claim we really want to prove.&lt;/p&gt;

&lt;p&gt;Under a full test-suite load, the notification can overlap a render. The harness teardown call may return while the renderer still has a queued batch. At that instant the header might not yet have received its disposal callback. The non-zero count is therefore ambiguous: either the header leaked, or the harness has not disposed it yet.&lt;/p&gt;

&lt;p&gt;A test with two possible explanations is a weak regression guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Waiting is not the same as proving
&lt;/h2&gt;

&lt;p&gt;The first instinct is usually to add a retry window: wait for the subscription count to reach zero, perhaps for a few seconds.&lt;/p&gt;

&lt;p&gt;That is appropriate when the product contract is genuinely eventual. It is not a universal cure for lifecycle tests. If the harness has left the component alive, waiting merely spends more time observing the wrong boundary. The test remains dependent on incidental scheduling and can still fail without a cleanup defect.&lt;/p&gt;

&lt;p&gt;The key question is not, “How long should we wait?” It is, “Which object owns the resource, and what exact behaviour are we trying to prove?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the assertion at the ownership boundary
&lt;/h2&gt;

&lt;p&gt;The stronger test calls the owning component's disposal contract directly, on the correct renderer context, and checks only the resource that component owns.&lt;/p&gt;

&lt;p&gt;If the header owns one subscription and a still-rendered child owns another, disposing the header should reduce the count by one. Expecting zero would incorrectly make the header test responsible for the child's lifetime as well.&lt;/p&gt;

&lt;p&gt;That narrower assertion is not weaker. It is more precise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the owner releases its resource;&lt;/li&gt;
&lt;li&gt;an independently owned resource is not silently claimed by the parent test;&lt;/li&gt;
&lt;li&gt;the result does not depend on whole-tree teardown timing; and&lt;/li&gt;
&lt;li&gt;removing the owner's cleanup call makes the test fail consistently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is especially valuable. A small mutation check—temporarily disabling the cleanup call—shows whether the test detects the regression it claims to detect. If the test still passes, it is probably measuring something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production code still needs lifecycle discipline
&lt;/h2&gt;

&lt;p&gt;A focused test does not excuse fragile implementation. Cleanup should normally be idempotent because framework callbacks, navigation, error handling, and explicit shutdown can converge on the same release path.&lt;/p&gt;

&lt;p&gt;The object should also refuse to acquire new resources after disposal. An asynchronous load may resume after the component has gone away. If its continuation subscribes again, a perfectly correct earlier disposal becomes irrelevant because nothing will release the new subscription.&lt;/p&gt;

&lt;p&gt;The same reasoning applies beyond subscriptions. Failed in-flight loads should close or abandon temporary buffers. Retained arrivals should have a sensible bound. Work dispatched to another recipient should not accidentally carry ambient request state from the publisher. These are all lifecycle questions: who owns the state, when does ownership end, and what context is allowed to cross the boundary?&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep two layers of evidence
&lt;/h2&gt;

&lt;p&gt;Direct ownership tests and integration tests answer different questions.&lt;/p&gt;

&lt;p&gt;An owner-level test asks: when this component is disposed, does it release what it owns, and can it remain released?&lt;/p&gt;

&lt;p&gt;An integration test asks: does navigation, circuit shutdown, or framework teardown actually invoke the right lifecycle path across the rendered tree?&lt;/p&gt;

&lt;p&gt;Both matter. The mistake is using only the second, broad test as proof of the first, narrow contract. When the broad test fails, you cannot tell whether ownership or orchestration is broken.&lt;/p&gt;

&lt;p&gt;Separating the layers improves diagnosis. A direct test failure points toward the owner. An integration-only failure points toward wiring, scheduling, or framework interaction. The test suite becomes a map of contracts instead of a collection of timing guesses.&lt;/p&gt;

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

&lt;p&gt;For each subscription, timer, observer, or callback:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name the owner explicitly.&lt;/li&gt;
&lt;li&gt;Make release safe to call more than once.&lt;/li&gt;
&lt;li&gt;Prevent acquisition after the owner is disposed.&lt;/li&gt;
&lt;li&gt;Test the owner's release contract directly.&lt;/li&gt;
&lt;li&gt;Give child-owned resources separate coverage.&lt;/li&gt;
&lt;li&gt;Keep one integration test for the framework path.&lt;/li&gt;
&lt;li&gt;Prove the test's sensitivity by breaking cleanup locally and seeing it fail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is a few more focused tests and slightly more explicit lifecycle code. In return, failures become actionable. You know whether the owner leaked, the child still legitimately owns something, or the framework has not completed teardown.&lt;/p&gt;

&lt;p&gt;Reliable tests do not merely wait longer. They make ownership visible.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>testing</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Hand Off Intent, Not Authority</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:36:33 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/hand-off-intent-not-authority-3ilh</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/hand-off-intent-not-authority-3ilh</guid>
      <description>&lt;p&gt;Moving a user from one application host to another often looks harmless. A button opens a browser, the user continues, and the original client waits for them to return.&lt;/p&gt;

&lt;p&gt;Architecturally, that is not merely navigation. It is a trust-boundary decision.&lt;/p&gt;

&lt;p&gt;This becomes especially important when one user experience runs across several .NET hosts: perhaps an ASP.NET Core site, a Blazor WebAssembly client, and a .NET MAUI hybrid app. The shared components may look identical, but the hosts do not own the same secrets, sessions, callbacks, or security guarantees.&lt;/p&gt;

&lt;p&gt;The lesson is simple: when a workflow crosses hosts, hand off intent, not authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tempting shortcut
&lt;/h2&gt;

&lt;p&gt;Suppose a client has already loaded an order-like resource and can display the next sensitive action. It is tempting to let every host render the same provider form or carry the current access proof into the browser.&lt;/p&gt;

&lt;p&gt;That preserves continuity, but it also widens the boundary. A client that only needed to display state may now receive signed fields, transaction details, callback assumptions, or a reusable proof. Putting any of that into a query string or fragment creates more problems: URLs are copied, logged, cached, placed in browser history, exposed to extensions, and sometimes sent as referrers.&lt;/p&gt;

&lt;p&gt;Shared UI does not imply shared authority. Reuse the presentation and state model where it helps, but make each host's capabilities explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A narrower handoff
&lt;/h2&gt;

&lt;p&gt;A safer design gives the less-trusted client one small capability: open a fixed public entry point on the authoritative host.&lt;/p&gt;

&lt;p&gt;The destination should be deliberately boring. It should not contain an order reference, email address, one-time code, access token, signed provider fields, or a claimed outcome. It should have no dynamic query string or fragment. It communicates only this intent: “the user wants to continue this workflow on the web.”&lt;/p&gt;

&lt;p&gt;The authoritative host then starts again from its own security context. It asks for the identifiers appropriate to the workflow, performs anti-forgery checks, verifies ownership through its normal mechanism, and only then exposes the sensitive action.&lt;/p&gt;

&lt;p&gt;That separation gives each statement a precise meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opening the browser is navigation.&lt;/li&gt;
&lt;li&gt;Proving ownership grants access in the destination host.&lt;/li&gt;
&lt;li&gt;Starting the action is a separate command.&lt;/li&gt;
&lt;li&gt;A callback or authoritative read confirms the outcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those statements is allowed to stand in for another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat public configuration as an API
&lt;/h2&gt;

&lt;p&gt;Installable browser clients need runtime configuration, but a generated client configuration document is still a public API. Do not serialize a server configuration tree and hope that only safe values happen to be present.&lt;/p&gt;

&lt;p&gt;Build a small allowlist instead. For this kind of handoff, the browser might receive an enablement flag and a reviewed public origin. That is enough. Return the document with a no-store policy so a stale capability does not linger after an operational change.&lt;/p&gt;

&lt;p&gt;Validate the destination before the application starts. Require the exact shape you intend: HTTPS, a root origin, no embedded credentials, no path, no query, no fragment, and no unexpected local or alternate-port destination. Keep the feature disabled when configuration is absent. If someone explicitly enables it with an invalid value, fail startup with a value-free error rather than echoing the bad input into logs.&lt;/p&gt;

&lt;p&gt;Failing closed here is not pessimism. It turns configuration from ambient text into an enforceable security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the destination defensive too
&lt;/h2&gt;

&lt;p&gt;The source client should generate a clean URL, but the destination should not trust that promise.&lt;/p&gt;

&lt;p&gt;If someone arrives with a query string, canonicalize to the fixed entry before rendering. Apply no-store and a restrictive referrer policy. Do not pre-fill sensitive identifiers from URL input. Start with the ordinary ownership flow and its existing anti-forgery protection.&lt;/p&gt;

&lt;p&gt;This matters because links escape their intended callers. They are bookmarked, pasted into messages, opened by scanners, and reconstructed by users. A robust boundary remains safe even when the source is not your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure must remain boring
&lt;/h2&gt;

&lt;p&gt;A browser launch can fail for mundane reasons: platform policy, missing handlers, lifecycle changes, or an exception from the native shell. None of those failures should mutate domain state.&lt;/p&gt;

&lt;p&gt;The client should report only that the launch did not happen. It should not create the sensitive form, mark work as started, grant access, or imply success. Concurrent clicks can be collapsed while an open attempt is running, but a failed attempt should be retryable.&lt;/p&gt;

&lt;p&gt;This is a useful design test: if navigation failure requires compensation, the handoff probably carried too much authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the negative space
&lt;/h2&gt;

&lt;p&gt;Most happy-path tests prove that a button eventually opens the right page. Boundary tests need to prove what cannot happen.&lt;/p&gt;

&lt;p&gt;Useful tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the capability is off by default;&lt;/li&gt;
&lt;li&gt;malformed enablement and invalid destinations fail safely;&lt;/li&gt;
&lt;li&gt;the client opens only the fixed path;&lt;/li&gt;
&lt;li&gt;the URL contains no proof or private state;&lt;/li&gt;
&lt;li&gt;each host renders only its intended action;&lt;/li&gt;
&lt;li&gt;public configuration contains only allowlisted fields;&lt;/li&gt;
&lt;li&gt;the web entry removes unexpected query data before rendering;&lt;/li&gt;
&lt;li&gt;caching and referrer protections are present;&lt;/li&gt;
&lt;li&gt;ownership and anti-forgery checks happen in the destination;&lt;/li&gt;
&lt;li&gt;launch failure changes no workflow state; and&lt;/li&gt;
&lt;li&gt;repeated clicks do not create duplicate launches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are negative properties. They are easy to lose during refactoring unless tests name them directly.&lt;/p&gt;

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

&lt;p&gt;Fresh ownership proof adds a step. A dedicated entry route, host-specific capability registration, and configuration validation add code. The boundary also needs maintenance as new hosts appear.&lt;/p&gt;

&lt;p&gt;That cost buys something valuable: the sensitive operation stays with the host designed to own it. The other clients remain simpler, public configuration stays small, and failures have a narrow blast radius.&lt;/p&gt;

&lt;p&gt;When reviewing any cross-host workflow, write down four things: the exact destination, the data allowed to cross, the authority that must be re-established, and the state a failed launch may change. If those answers are vague, the implementation probably is too.&lt;/p&gt;

&lt;p&gt;Intent can cross a boundary. Authority should have to earn its way back in.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>dotnet</category>
      <category>blazor</category>
      <category>security</category>
    </item>
    <item>
      <title>Soft Delete Is a Model Contract, Not a Repository Habit</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sat, 17 Oct 2026 05:45:30 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/soft-delete-is-a-model-contract-not-a-repository-habit-7d7</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/soft-delete-is-a-model-contract-not-a-repository-habit-7d7</guid>
      <description>&lt;p&gt;Soft deletion often begins innocently: add an IsDeleted flag, remember to exclude retained rows, and move on. That works until the application grows into several modules, several DbContext types, and configuration spread across assemblies.&lt;/p&gt;

&lt;p&gt;At that point, soft deletion is no longer a local query concern. It is a visibility boundary for the complete EF Core model.&lt;/p&gt;

&lt;p&gt;The difference is practical. A repository predicate protects the query paths that remember to use it. A model contract protects ordinary reads by default and makes exceptional reads visible in code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dangerous partial success
&lt;/h2&gt;

&lt;p&gt;Imagine a shared base context that scans the entity types it knows and installs a query filter. Later, a module applies another configuration assembly and adds more entities. The base code is correct for the model it saw, but its timing was wrong. Some retained rows are hidden and others are returned normally.&lt;/p&gt;

&lt;p&gt;This failure is uncomfortable because both sides look reasonable in isolation. The entity has the deletion marker. The shared context contains filter code. A few tests pass. Yet the effective model has uneven visibility rules.&lt;/p&gt;

&lt;p&gt;The lesson is to configure cross-cutting rules at a lifecycle point where the model is complete. An EF Core model-finalising convention can inspect every registered type after the contexts and configuration assemblies have contributed their mappings.&lt;/p&gt;

&lt;p&gt;For each root entity carrying the deletion marker, the convention can add the standard predicate. Owned types follow their owner, and derived types inherit the root filter. Existing filters must be preserved: combine with an anonymous filter, or add a separate named filter when the model uses named filters. Any always-visible audit type should be an explicit, narrow exception rather than an accidental omission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the effective predicate
&lt;/h2&gt;

&lt;p&gt;Checking that a filter exists is too weak.&lt;/p&gt;

&lt;p&gt;An entity might have an unrelated visibility filter. It might even mention the deletion marker beneath a logical OR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;not deleted OR visible to this user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That expression can still admit a retained row. The required property is stronger: not deleted must be a mandatory conjunct of the effective filter.&lt;/p&gt;

&lt;p&gt;A reusable model audit can enumerate all root entity types with the marker, exclude only documented exceptions, and fail if any type lacks that mandatory predicate. It should have tests against itself: prove that it rejects an unrelated filter and rejects the OR case. Otherwise the audit risks becoming a green check that does not guard the boundary it names.&lt;/p&gt;

&lt;p&gt;Provider-backed tests add the next layer of evidence. Insert one active row and one retained row. Show that normal queries return only the active row, while an explicit bypass can retrieve both. This verifies the generated query and provider behaviour rather than only evaluating a compiled expression in memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  A correct filter changes workflows
&lt;/h2&gt;

&lt;p&gt;When global filtering becomes consistent, previously accidental behaviour may break. That is useful information.&lt;/p&gt;

&lt;p&gt;Restoration is the clearest example. Suppose a retained row still owns a unique business slot. A normal lookup cannot see it, so a naive create path tries to insert a replacement and collides with the database constraint.&lt;/p&gt;

&lt;p&gt;The solution is not to weaken the global filter. Give the restoration path an explicit way to ignore model filters, then make its intention precise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating into a slot held by a retained row restores that same row;&lt;/li&gt;
&lt;li&gt;editing a different active row into the reserved slot is refused;&lt;/li&gt;
&lt;li&gt;failure leaves both rows unchanged;&lt;/li&gt;
&lt;li&gt;historical or restoration reads are the only paths allowed to bypass the boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Putting the bypass on a named specification or dedicated query object is easier to review than scattering IgnoreQueryFilters() through services. The name communicates why the exception exists, and focused tests can pin its scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch required navigations
&lt;/h2&gt;

&lt;p&gt;Global filters also interact with relationship shape. If a required navigation uses an inner join and the principal is filtered out, the dependent can disappear from a query even when the dependent itself is active.&lt;/p&gt;

&lt;p&gt;That may be correct, but it should be a decision.&lt;/p&gt;

&lt;p&gt;An inventory of required relationships that cross filtered and unfiltered types gives reviewers a bounded list of hazards. For each one, choose deliberately: align filters on both sides, make the navigation optional, change the query shape, or accept and test the disappearance.&lt;/p&gt;

&lt;p&gt;This is one reason metadata-driven verification scales better than a long list of hand-written repository tests. The model can tell you what exists now, including a new entity or relationship added by a future module.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical verification stack
&lt;/h2&gt;

&lt;p&gt;A durable soft-delete boundary has four layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Apply the convention after the model is complete.&lt;/li&gt;
&lt;li&gt;Audit every soft-deletable root for the exact mandatory predicate.&lt;/li&gt;
&lt;li&gt;Keep bypasses explicit and limited to restoration or historical use.&lt;/li&gt;
&lt;li&gt;Run real-provider tests for visibility, restoration, uniqueness, and required-navigation behaviour.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a cost: more model infrastructure, a deliberate exception list, and tests that can expose old assumptions. Some teams may decide that hard deletion or temporal tables better match particular data. The point is not that soft deletion is always right.&lt;/p&gt;

&lt;p&gt;The point is that, once chosen, it must behave like one coherent contract.&lt;/p&gt;

&lt;p&gt;The most valuable outcome is not the filter itself. It is that a future entity cannot quietly arrive outside the boundary—and that exceptional access reads like an exception.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>Queued Work Is a Snapshot, Not the Truth</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Fri, 16 Oct 2026 05:43:28 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/queued-work-is-a-snapshot-not-the-truth-kdc</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/queued-work-is-a-snapshot-not-the-truth-kdc</guid>
      <description>&lt;p&gt;Background work is usually introduced to make a system more reliable. We persist an intent, let the request finish, and deliver the external side effect later. That protects the user-facing path from temporary provider failures and gives us room for retries.&lt;/p&gt;

&lt;p&gt;It also creates a time gap.&lt;/p&gt;

&lt;p&gt;Inside that gap, the world can change. A task can be completed. A new task can appear. A recipient can lose access. A preference can change. Another worker can advance the same operation. The queued record may still be durable and validly serialized while the message it would produce is already wrong.&lt;/p&gt;

&lt;p&gt;The lesson is simple: queued intent is evidence about an earlier snapshot. It is not timeless authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the promise, not the presentation
&lt;/h2&gt;

&lt;p&gt;Suppose a service groups several outstanding items into one reminder. The tempting design is to serialize the titles, amounts, names, and finished message into an outbox row. Delivery is then easy: deserialize and send.&lt;/p&gt;

&lt;p&gt;That convenience turns the row into a stale cache of user-facing truth. It may also retain more sensitive data than the queue needs.&lt;/p&gt;

&lt;p&gt;A safer durable record can be much smaller. Keep the routing identity, recipient identity, sequence, source, and a deterministic fingerprint of the complete set the message was created for. The fingerprint represents the promise: “this message describes exactly this observed set.”&lt;/p&gt;

&lt;p&gt;The display payload is rebuilt later from authoritative state. The durable row says what snapshot was intended without pretending that its old presentation is still current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuild from the complete current set
&lt;/h2&gt;

&lt;p&gt;At execution time, re-read what the authoritative source currently exposes to the recipient. Then run the same grouping rules again and compare the resulting fingerprint with the stored fingerprint.&lt;/p&gt;

&lt;p&gt;Why the complete set? Because stale work changes in two directions.&lt;/p&gt;

&lt;p&gt;The obvious case is subtraction. An item was completed or removed after the reminder was queued. The old message now overstates what is outstanding.&lt;/p&gt;

&lt;p&gt;The less obvious case is addition. A new item appeared after the reminder was queued. If the worker checks only the item keys remembered by the row, every remembered item may still be valid. The worker can still send a message that understates the current request.&lt;/p&gt;

&lt;p&gt;Reconstructing the whole set catches both. If the current fingerprint differs, the old intent is superseded. Stop it. Let the current state produce a new intent under a new identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep authorization in the authoritative read
&lt;/h2&gt;

&lt;p&gt;The background worker should not invent a second, simplified version of the authorization and status rules used by the foreground feature. That duplicate will drift.&lt;/p&gt;

&lt;p&gt;Prefer a shared underlying read path with an explicit recipient parameter. The interactive request and the delayed worker should agree on what the recipient is currently allowed to see and how current status is mapped.&lt;/p&gt;

&lt;p&gt;This also makes an important distinction possible. “Nothing remains” is not the same as “the recipient is no longer authorized.” Both may yield an empty visible set, but they mean different things operationally and in an audit trail.&lt;/p&gt;

&lt;p&gt;Completion can finish the old intent. Revocation should stop disclosure for a security reason. A missing source integration is different again: it is usually a retryable deployment fault, not evidence that the work is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model more than send or fail
&lt;/h2&gt;

&lt;p&gt;A robust recheck tends to produce several outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send: the complete current snapshot still matches, preferences allow delivery, and the operation sequence is current.&lt;/li&gt;
&lt;li&gt;Stop as completed: the intended work no longer exists.&lt;/li&gt;
&lt;li&gt;Stop as changed: the set still exists but is no longer the set the queued message promised to describe.&lt;/li&gt;
&lt;li&gt;Stop as revoked: the recipient no longer has authority.&lt;/li&gt;
&lt;li&gt;Stop as superseded: a later sequence has already advanced the operation.&lt;/li&gt;
&lt;li&gt;Defer: authoritative evaluation is temporarily unavailable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These outcomes can feel verbose compared with a Boolean. That verbosity is useful. It prevents a temporary infrastructure gap from being filed as success, and it prevents a security revocation from being misreported as ordinary completion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test addition as carefully as subtraction
&lt;/h2&gt;

&lt;p&gt;The most valuable regression test in this pattern is often the one teams forget: add something after the row is written.&lt;/p&gt;

&lt;p&gt;A focused suite should cover an unchanged set, a removed item, an added item, an empty set, revoked authority, changed preferences, a superseded sequence, and an unavailable source. It should also prove that each evaluation performs a fresh authoritative read rather than accidentally reusing stored data.&lt;/p&gt;

&lt;p&gt;These tests do more than exercise branches. They define the temporal contract of the workflow: which facts may age, which facts must be refreshed, and which outcome owns each kind of change.&lt;/p&gt;

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

&lt;p&gt;This design adds a database read at the most sensitive point, deterministic reconstruction, fingerprint maintenance, and more explicit state transitions. At high volume, that cost deserves measurement. Poorly indexed rechecks can simply move a reliability problem into the database.&lt;/p&gt;

&lt;p&gt;The benefit is not that fingerprints make distributed systems magically consistent. They do not. There is still a race between the final read and the external side effect, and systems with stricter requirements may need provider idempotency, transactional boundaries, or domain-specific reservations.&lt;/p&gt;

&lt;p&gt;The narrower claim is valuable enough: a delayed worker should not knowingly act on an obsolete snapshot when it can cheaply revalidate current truth.&lt;/p&gt;

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

&lt;p&gt;When reviewing delayed email, push, webhook, export, billing, or approval work, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What exact snapshot did this row promise to act on?&lt;/li&gt;
&lt;li&gt;Does the queue store minimal identity or stale presentation data?&lt;/li&gt;
&lt;li&gt;Can the worker re-read the full authoritative set?&lt;/li&gt;
&lt;li&gt;Are additions detected as well as removals?&lt;/li&gt;
&lt;li&gt;Are completion, revocation, supersession, and temporary unavailability distinct?&lt;/li&gt;
&lt;li&gt;Is the external side effect itself idempotent or otherwise bounded?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Durability keeps work from disappearing. Revalidation keeps durable work from becoming confidently wrong.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>sre</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Isolation Is Sometimes a Feature, Not a Smell</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Thu, 15 Oct 2026 06:00:44 +0000</pubDate>
      <link>https://dev.to/ivan_rossouw_63dff0f4084e/isolation-is-sometimes-a-feature-not-a-smell-20o8</link>
      <guid>https://dev.to/ivan_rossouw_63dff0f4084e/isolation-is-sometimes-a-feature-not-a-smell-20o8</guid>
      <description>&lt;p&gt;Two projects expose similar helpers. One has no project references. It is missing from the solution most developers build every day. The obvious reaction is understandable: share the code, add the reference, and bring the lonely project back into the fold.&lt;/p&gt;

&lt;p&gt;That cleanup can be exactly wrong.&lt;/p&gt;

&lt;p&gt;A recent architecture correction I reviewed was a useful reminder that repository shape is not the whole system. Build agents, installed workloads, runtime origins, signing steps, and release pipelines are part of the architecture too. An isolated .NET MAUI head looked neglected when viewed only from the solution file. Viewed from its release path, the isolation was load-bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Delivery Graph
&lt;/h2&gt;

&lt;p&gt;Before deduplicating across project boundaries, ask how each project is actually built and shipped.&lt;/p&gt;

&lt;p&gt;In the case I reviewed, a server-focused solution deliberately excluded the mobile head. That kept routine server builds from requiring mobile workloads on every developer machine and CI runner. The mobile app was still real software: a separate pipeline restored, built, signed, and released it independently.&lt;/p&gt;

&lt;p&gt;Adding a shared project reference would therefore do more than remove a few repeated lines. It would expand the mobile restore graph and couple its release path to a project the mobile pipeline had never needed. Adding the mobile project to the server solution would impose a different cost in the other direction: machines building the server product would now need tooling they did not otherwise use.&lt;/p&gt;

&lt;p&gt;Neither consequence is visible in a tidy dependency diagram. Both matter in production delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Similar Code Can Encode Different Assumptions
&lt;/h2&gt;

&lt;p&gt;The same review contained two link resolvers that appeared ripe for consolidation. The browser implementation derived an external base address from its hosting origin. That was valid because the browser app was served by the web host.&lt;/p&gt;

&lt;p&gt;The mobile app ran inside an embedded web view. Its internal origin did not identify the external service it needed to reach. Reusing the browser resolver would not create elegant sharing; it would import an invalid environment assumption.&lt;/p&gt;

&lt;p&gt;This is a useful test for any proposed abstraction: are the implementations merely similar in syntax, or are they equivalent in authority and context?&lt;/p&gt;

&lt;p&gt;Imagine a browser client and a desktop client that both produce a download URL. The browser can safely resolve a relative path against the page origin. The desktop client may need an explicitly configured service endpoint. A common interface can still be useful, but a common implementation may be actively misleading.&lt;/p&gt;

&lt;p&gt;Duplication is not automatically good. It is sometimes the smallest honest representation of two different worlds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard the Constraint—and Its Reason
&lt;/h2&gt;

&lt;p&gt;Documentation corrected the misleading story, but the stronger improvement was an architecture test suite.&lt;/p&gt;

&lt;p&gt;The tests did not merely assert that the mobile head had no project references and stayed outside the server solution. They also checked that the separate mobile build path still existed. That second half changes the character of the rule.&lt;/p&gt;

&lt;p&gt;Without it, the test says: “keep this isolated forever.”&lt;/p&gt;

&lt;p&gt;With it, the test says: “keep this isolated while this independent delivery path remains true.”&lt;/p&gt;

&lt;p&gt;That is a healthier architectural guard. It protects a decision without pretending the decision is timeless. If the separate pipeline disappears, the guard fails and asks maintainers to reconsider the boundary. The test becomes executable rationale rather than fossilized preference.&lt;/p&gt;

&lt;p&gt;This pattern generalizes well. If a component must stay dependency-free because it is loaded by a restricted host, test both the dependency rule and the existence of that host. If a migration assembly must remain separate because tooling discovers it independently, guard both the assembly boundary and the discovery path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-Off Is Still Real
&lt;/h2&gt;

&lt;p&gt;Deliberate isolation has costs.&lt;/p&gt;

&lt;p&gt;Separate helpers can drift. A visual shell copied for a second head may receive fixes later. Architecture tests add maintenance, and source-shape checks can become brittle if they assert incidental text instead of meaningful structure.&lt;/p&gt;

&lt;p&gt;The answer is not to ignore those costs. It is to compare them with the coupling being avoided.&lt;/p&gt;

&lt;p&gt;Prefer the smallest duplication that preserves the boundary. Keep behavioural contracts aligned where the behaviour truly is shared. Document the assumptions that must differ. Review the boundary when release topology changes. And avoid broad claims such as “this project shares nothing” when the important truth is more precise: it ships independently and intentionally owns a narrow set of platform-specific choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Review Checklist
&lt;/h2&gt;

&lt;p&gt;Before merging apparently duplicate code across application heads, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which pipeline builds and releases each head?&lt;/li&gt;
&lt;li&gt;What workloads and SDKs does each build environment guarantee?&lt;/li&gt;
&lt;li&gt;Will a new reference expand the restore or signing boundary?&lt;/li&gt;
&lt;li&gt;Do the implementations rely on different origins, storage, identity, or lifecycle rules?&lt;/li&gt;
&lt;li&gt;Can we test the constraint and the continuing fact that justifies it?&lt;/li&gt;
&lt;li&gt;What event should trigger reconsideration of the decision?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These questions turn “DRY versus duplication” into a more useful discussion about operational ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Includes the Way Software Leaves the Repository
&lt;/h2&gt;

&lt;p&gt;The most valuable lesson for me was a leadership one: do not label unusual structure before tracing its history and delivery path.&lt;/p&gt;

&lt;p&gt;An isolated project may be forgotten. It may also be a carefully bounded product whose release assumptions are invisible from the main solution. We should investigate before simplifying, explain the decision in language that cannot be mistaken for neglect, and encode the rationale where future changes will encounter it.&lt;/p&gt;

&lt;p&gt;Clean architecture is not the fewest projects or the fewest repeated lines. It is a system whose dependencies match the realities of how it runs and ships—and whose exceptions are specific, reviewable, and allowed to expire.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>architecture</category>
      <category>dotnet</category>
      <category>maui</category>
    </item>
  </channel>
</rss>
