<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ivan Rossouw</title>
    <description>The latest articles on DEV Community by Ivan Rossouw (@iqtechsolutions).</description>
    <link>https://dev.to/iqtechsolutions</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035800%2F9c84067b-fe9a-46e7-8144-5b05a4ffb504.png</url>
      <title>DEV Community: Ivan Rossouw</title>
      <link>https://dev.to/iqtechsolutions</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iqtechsolutions"/>
    <language>en</language>
    <item>
      <title>A Page Is Not Shipped Until People Can Find It</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:06:10 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-page-is-not-shipped-until-people-can-find-it-375o</link>
      <guid>https://dev.to/iqtechsolutions/a-page-is-not-shipped-until-people-can-find-it-375o</guid>
      <description>&lt;p&gt;A routed page can compile, pass its authorization checks, have documentation, and still be functionally absent. If the people entitled to use it cannot discover a normal way in, the product behaves as though the capability was never built.&lt;/p&gt;

&lt;p&gt;That is easy to miss in a modular Blazor application. One team adds pages in one project, another module owns a second page tree, and a central hub or navigation catalogue tries to present the whole product coherently. Each local change can look correct while the experience between modules quietly breaks.&lt;/p&gt;

&lt;p&gt;The remedy is not another release checklist. It is an executable discoverability contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the invariant at the page boundary
&lt;/h2&gt;

&lt;p&gt;A useful rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every static operational page must have one intentional entry point whose visibility matches the page's access rule.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;“Static” matters. A detail page with an identifier in its route is normally reached from a list, search result, or parent screen. It does not need a global menu item. A screen that answers on two route aliases is still one page; adding two cards would create duplication rather than discoverability.&lt;/p&gt;

&lt;p&gt;The rule should cover pages, not merely strings that resemble URLs. That keeps the test aligned with the product concept people actually use.&lt;/p&gt;

&lt;p&gt;It should also say “intentional entry point,” not “mentioned somewhere.” A link buried inside another page may technically make a route reachable, but it does not necessarily make the capability findable. Likewise, a help article that names a screen is documentation, not navigation. Requiring a deliberate card or menu entry forces the information architecture to be explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scan every module, not only the original tree
&lt;/h2&gt;

&lt;p&gt;Architecture tests often begin with a useful local assumption: all administrative pages live under one directory, namespace, or assembly. The test passes for years. Then a new module introduces another tree and the guard continues to report green because it never sees the new pages.&lt;/p&gt;

&lt;p&gt;This is a particularly awkward failure because both the feature and the guard are “working as designed.” The design boundary is simply stale.&lt;/p&gt;

&lt;p&gt;The test therefore needs a completeness check as well as a matching check. Enumerate every known page tree. Parse the routed components. Exclude only clearly contextual routes. Then compare the resulting pages with the navigation catalogue.&lt;/p&gt;

&lt;p&gt;Add a sanity assertion too. If a mature tree suddenly produces zero—or far fewer—pages, the parser or directory assumption probably broke. Without that guard, a failed scan can look like perfect compliance.&lt;/p&gt;

&lt;p&gt;When a new module owns pages, extending the scan is part of integrating that module. This turns architectural coverage into an explicit responsibility rather than inherited folklore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep navigation and authorization separate
&lt;/h2&gt;

&lt;p&gt;The navigation entry should use the same access decision as the destination page. If the card is broader, people see an invitation that ends in refusal. If it is narrower, people who are entitled to use the page cannot find it.&lt;/p&gt;

&lt;p&gt;It is tempting to reuse a nearby permission because it is convenient. That can be wrong in both directions. When no existing decision expresses the page's real audience, adding a focused visibility flag is usually clearer than bending an unrelated one.&lt;/p&gt;

&lt;p&gt;But matching visibility does not turn navigation into security. A hidden card only changes what the interface presents. A direct request, a bookmarked address, or another client can still reach the endpoint. The page or server operation must continue to enforce authorization independently.&lt;/p&gt;

&lt;p&gt;This separation produces a healthy pair of invariants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;navigation makes entitled capabilities discoverable;&lt;/li&gt;
&lt;li&gt;authorization rejects requests that are not entitled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing both is stronger than asking either mechanism to do two jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer a precise contract over “linked somewhere”
&lt;/h2&gt;

&lt;p&gt;A broad reachability scan can be a good first diagnostic. Search the codebase for links to each route and identify pages that have no inbound reference. That quickly reveals obvious orphans.&lt;/p&gt;

&lt;p&gt;As a permanent contract, however, “linked somewhere” is weak. A tab rail is visible only after someone has already reached a page in that tab set. A contextual link may depend on data or state. A help page may mention the route without providing product navigation. Source text can prove that a string exists; it cannot prove that the intended audience has a clear entry point.&lt;/p&gt;

&lt;p&gt;Tightening the rule to require the chosen navigation catalogue makes the standard less ambiguous. It also creates useful pressure: when a page fits nowhere, the team must decide whether the navigation model is incomplete, the page is contextual, or the feature should not be exposed yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accept the maintenance cost deliberately
&lt;/h2&gt;

&lt;p&gt;The stricter contract adds work. New pages need catalogue entries, accurate visibility mapping, and suitable grouping. Route conventions must be understood well enough for the test to distinguish static pages, detail pages, and aliases. Large navigation surfaces may expose sections that no longer fit their historical categories.&lt;/p&gt;

&lt;p&gt;Those are not test problems to hide with a long exemption list. They are product and architecture decisions the test has made visible.&lt;/p&gt;

&lt;p&gt;Keep exemptions small, named, and justified by a navigation model rather than by inconvenience. Fail when an exemption becomes stale. If a page genuinely belongs behind a list or workflow, encode that reason. If it is an operational destination, give it an intentional entry point.&lt;/p&gt;

&lt;p&gt;The payoff is modest but durable: completed capabilities do not disappear between modules, permission drift becomes easier to see, and a new page cannot quietly land outside the product's navigation contract.&lt;/p&gt;

&lt;p&gt;A page is not shipped merely because its route exists. It is shipped when the right person can find it—and the wrong person still cannot execute it.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>blazor</category>
      <category>dotnet</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Remove a Workspace Member Safely from Blazor</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Fri, 25 Sep 2026 05:13:29 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/remove-a-workspace-member-safely-from-blazor-5en1</link>
      <guid>https://dev.to/iqtechsolutions/remove-a-workspace-member-safely-from-blazor-5en1</guid>
      <description>&lt;h1&gt;
  
  
  Remove a Workspace Member Safely from Blazor
&lt;/h1&gt;

&lt;p&gt;A destructive UI should protect the operator's intent without pretending that browser state is authoritative. This slice adds a Blazor workspace-member page on top of an existing owner-authorized ASP.NET Core removal command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat the roster as a snapshot
&lt;/h2&gt;

&lt;p&gt;The page loads an allow-listed member projection containing a user identifier, email, numeric role, concurrency version, and current-user marker. The client defines its own transport record and rejects unknown roles, blank fields, duplicate identifiers, non-positive versions, null content, and unmapped members. Both the browser request and server response use no-store semantics so an explicit recovery refresh reaches the current server state.&lt;/p&gt;

&lt;p&gt;Only another ordinary member exposes a remove action. Selecting it captures the identifier, email, and displayed version together. Confirmation names the member and sends no request until the owner accepts. Starting any refresh clears that snapshot first, preventing an old confirmation from surviving beside a newer roster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the HTTP result finite
&lt;/h2&gt;

&lt;p&gt;The POST route contains the encoded target identifier, while the body contains only expectedVersion. The adapter maps No Content, Unauthorized, Forbidden, Not Found, invalid-version, stale-version, and transfer-required outcomes into explicit result types. Malformed bodies, unknown codes, unexpected statuses, and transport failures become one safe Failure result.&lt;/p&gt;

&lt;p&gt;Timeout-style operation cancellation becomes Failure so the page enters stale lock. Cancellation explicitly requested through the caller token still propagates, which avoids showing a false network warning during navigation or disposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refresh after the server decides
&lt;/h2&gt;

&lt;p&gt;The page never removes a row optimistically. After success it reloads the server roster and announces the result. If that reload fails, it says the command completed but the visible roster could not be refreshed. A stale conflict also reloads, but it never retries the destructive command automatically. The owner must inspect the new row and choose again.&lt;/p&gt;

&lt;p&gt;Any unclassified failure retains the last good roster for context, shows the exact stale warning, disables every remove action, and leaves Refresh available. Only a successful validated load clears that lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Focus after rendering
&lt;/h2&gt;

&lt;p&gt;Conditional confirmation and result elements do not exist until Blazor renders them. The component queues a semantic focus target, then uses OnAfterRenderAsync and ElementReference.FocusAsync. Programmatic headings and messages use tabindex minus one. Cancel returns to the initiating row action when it still exists; otherwise focus falls back to the roster heading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove both sides of the boundary
&lt;/h2&gt;

&lt;p&gt;Typed-adapter tests pin the wire contract, cache mode, status mapping, cancellation behavior, and strict validation. bUnit tests drive confirmation, cancellation, refresh invalidation, success reload, stale locking, access failure, and the ambiguous case where removal succeeds but refresh fails. Existing integration tests continue to prove workspace scoping, owner authorization, optimistic concurrency, SignalR eviction, and old-token denial.&lt;/p&gt;

&lt;p&gt;The result is a small vertical slice with a useful rule: the UI protects intent, the version identifies the reviewed snapshot, and the server decides whether the command is still valid.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>blazor</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Retry Is Not Safe Until the Side Effect Is Safe</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Thu, 24 Sep 2026 05:56:03 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-retry-is-not-safe-until-the-side-effect-is-safe-4nik</link>
      <guid>https://dev.to/iqtechsolutions/a-retry-is-not-safe-until-the-side-effect-is-safe-4nik</guid>
      <description>&lt;p&gt;A retry can duplicate a side effect even when the original command completed successfully.&lt;/p&gt;

&lt;p&gt;This is easy to miss in a method that does two ordinary things: save a record and send a message. Each operation is understandable on its own. The difficult part is the uncertainty between them.&lt;/p&gt;

&lt;p&gt;Suppose an endpoint creates an invitation, persists it, asks an email provider to deliver a link, and returns a response. The database commit succeeds. The provider may accept the message. Then the network drops the response. The client cannot tell whether anything happened, so it retries.&lt;/p&gt;

&lt;p&gt;If the server repeats the whole method, it can create another record, another secret, or another email. The code has turned a lost response into duplicated authority-bearing side effects.&lt;/p&gt;

&lt;p&gt;The lesson is broader than invitations: design the durable commit point, provider boundary, and retry contract as one workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commit the fact before announcing it
&lt;/h2&gt;

&lt;p&gt;Sending before saving creates the most confusing failure mode. A recipient receives a plausible link, but the backing operation never committed. To them, the system appears to reject a valid action for no visible reason.&lt;/p&gt;

&lt;p&gt;A safer ordering is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the command.&lt;/li&gt;
&lt;li&gt;Create the durable operation.&lt;/li&gt;
&lt;li&gt;Store an idempotency receipt that points to that operation.&lt;/li&gt;
&lt;li&gt;Commit both together.&lt;/li&gt;
&lt;li&gt;Only then ask the external provider to send.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The durable record becomes the source of truth. The message announces a fact that already exists rather than promising that a later write will probably succeed.&lt;/p&gt;

&lt;p&gt;This does not make the database and email provider atomic. It gives the gap between them explicit semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make replay return, not repeat
&lt;/h2&gt;

&lt;p&gt;An idempotency key is useful only when it protects the irreversible side effect too.&lt;/p&gt;

&lt;p&gt;The receipt should bind a caller-generated command key to a stable fingerprint of the request. If the same key returns with the same request, the service reads and returns the original result. It does not send again. If the key returns with different input, the service rejects the conflict rather than guessing which request the caller intended.&lt;/p&gt;

&lt;p&gt;The database should also enforce uniqueness. Two identical requests can arrive concurrently before either sees the other's receipt. Let one commit win; have the loser load and return the winning result. Otherwise a perfectly designed application check can still race.&lt;/p&gt;

&lt;p&gt;The important invariant is not merely “one row.” It is “one durable result and one automatic send for one command.” Tests should assert both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unknown is not the same as failed
&lt;/h2&gt;

&lt;p&gt;Provider calls rarely produce only success or failure. They produce at least three useful outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepted: the provider acknowledged the message.&lt;/li&gt;
&lt;li&gt;Definitely rejected: the provider did not accept it.&lt;/li&gt;
&lt;li&gt;Unknown: the caller lost certainty, often through a timeout or interrupted response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unknown is the dangerous one. The message may already be queued. Reporting a definite failure can encourage an immediate retry and a second message.&lt;/p&gt;

&lt;p&gt;This is a small state-model decision with a large operational effect. Do not collapse “I do not know” into “it did not happen.” Preserve uncertainty so the recovery path can respond deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a recovery handle
&lt;/h2&gt;

&lt;p&gt;If delivery is definitely rejected after the database commit, deleting or closing the record may feel tidy. It also removes the user's only handle on the problem.&lt;/p&gt;

&lt;p&gt;Keeping the durable operation pending can be more honest. The UI can show that it exists, report that delivery failed, and offer an explicit resend. That resend is a new action, not a transparent replay of the original command.&lt;/p&gt;

&lt;p&gt;For a message carrying a secret, explicit resend should rotate the secret, invalidate the old link, persist the new state, and then send. Leaving both links active multiplies hidden authority. Reusing a raw secret is often impossible or unsafe if only its hash was stored.&lt;/p&gt;

&lt;p&gt;This recovery model needs expiry, status visibility, observability, and clear language. “Pending” must not imply “delivered.” The system should say what it knows and avoid claiming what it cannot know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the gaps, not only the happy path
&lt;/h2&gt;

&lt;p&gt;The valuable tests live at the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repeating the same command returns the same result, leaves one durable record, and sends once.&lt;/li&gt;
&lt;li&gt;Concurrent duplicates converge on one receipt and one result.&lt;/li&gt;
&lt;li&gt;A definite pre-provider rejection is reported while the recovery record remains available.&lt;/li&gt;
&lt;li&gt;An unknown outcome is not presented as definite non-delivery.&lt;/li&gt;
&lt;li&gt;Explicit resend invalidates the earlier secret before attempting delivery again.&lt;/li&gt;
&lt;li&gt;The provider is never called before the database commit succeeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests express the reliability contract better than a broad “request succeeds” test. They also make later refactoring safer because the order and meaning of side effects are visible.&lt;/p&gt;

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

&lt;p&gt;This design is not free. It introduces command receipts, request fingerprints, provider outcome types, pending states, expiry, and explicit recovery. Operators need enough telemetry to distinguish rejected sends from unknown outcomes. Product language must explain uncertainty without alarming users.&lt;/p&gt;

&lt;p&gt;The alternative is simpler code with less truthful behaviour: phantom links, duplicate messages, or retries that silently create extra authority.&lt;/p&gt;

&lt;p&gt;My practical review question is: for every workflow that combines a database write with an external side effect, can we point to the commit point, replay rule, uncertainty model, and recovery action?&lt;/p&gt;

&lt;p&gt;If those four answers are explicit, retries become a designed part of the workflow rather than a hopeful repetition of it.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>reliability</category>
      <category>testing</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Transfer Workspace Ownership Atomically</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Wed, 23 Sep 2026 21:49:54 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/transfer-workspace-ownership-atomically-684</link>
      <guid>https://dev.to/iqtechsolutions/transfer-workspace-ownership-atomically-684</guid>
      <description>&lt;p&gt;Ownership is not two unrelated role edits. It is one invariant over two active membership rows: the supported transfer command replaces the current owner with exactly one member.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust the server boundary
&lt;/h2&gt;

&lt;p&gt;The browser proposes a target user and the two versions shown in its roster. The server derives the acting user and workspace from authenticated state, then rechecks the live owner membership and security stamp inside the transaction. A foreign or inactive target is never allowed to choose its way into the workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine isolation and optimistic versions
&lt;/h2&gt;

&lt;p&gt;A serializable transaction makes the decision and write one unit. Both membership Version properties are EF Core concurrency tokens, so the displayed owner and target versions must still match. Isolation orders concurrent work; optimistic versions explain that the operator's roster is stale.&lt;/p&gt;

&lt;p&gt;On SQL Server, the candidate query uses UPDLOCK and HOLDLOCK in a stable order. This avoids the shared-lock upgrade deadlock that can occur when two serializable readers both intend to write the owner row. Other providers retain their normal tracked query inside the serializable transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change both roles and save once
&lt;/h2&gt;

&lt;p&gt;The domain ChangeRole method accepts only active memberships, advances Version, and assigns the new role. The store demotes the old owner, promotes the target member, calls SaveChanges once, and commits once. EF checks both original versions. A conflict rolls back the pair and maps to a finite stale-version result.&lt;/p&gt;

&lt;p&gt;Advancing both versions also invalidates old tenant-local authority. The existing policies compare the principal's membership version with the live selected membership. The former owner cannot retain owner access, and the promoted member must refresh before receiving new owner authority. Unrelated workspace memberships are untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove the concurrency boundary
&lt;/h2&gt;

&lt;p&gt;A provider-backed test starts two transfers through separate DbContext instances against SQL Server. Exactly one command succeeds. The other loses current authority or sees stale state. The final query finds exactly one Owner row, with both changed memberships advanced to version two.&lt;/p&gt;

&lt;p&gt;The claim is intentionally scoped to supported application commands. Arbitrary out-of-band database edits can bypass application invariants and need separate operational controls.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>programming</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Recommendations Are Not Decisions: Designing an Auditable Human Boundary</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:52:42 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/recommendations-are-not-decisions-designing-an-auditable-human-boundary-1aa2</link>
      <guid>https://dev.to/iqtechsolutions/recommendations-are-not-decisions-designing-an-auditable-human-boundary-1aa2</guid>
      <description>&lt;p&gt;Software is very good at making a calculation look authoritative. Give a rule engine enough inputs and it can return a crisp result in milliseconds. In a consequential workflow, that neatness is dangerous: a computed recommendation can quietly become a decision without anyone deliberately granting it that authority.&lt;/p&gt;

&lt;p&gt;A better design treats computation and authority as different responsibilities. The software may evaluate evidence, explain what it found, and suggest an outcome. A person still records the actual decision. The data model preserves both.&lt;/p&gt;

&lt;p&gt;That separation is more than a user-interface disclaimer. It has to reach the evaluator, persistence model, activation workflow, and tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the evaluator pure
&lt;/h2&gt;

&lt;p&gt;Start with an evaluator that has no write path. It accepts a versioned rule and a set of observations, then returns an explanation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which inputs contributed;&lt;/li&gt;
&lt;li&gt;which requirements passed or failed;&lt;/li&gt;
&lt;li&gt;what evidence was missing;&lt;/li&gt;
&lt;li&gt;which rule version was used; and&lt;/li&gt;
&lt;li&gt;what outcome, if any, is suggested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The return value should say “suggested,” not “decided.” Naming is not sufficient, but it makes accidental authority harder to hide.&lt;/p&gt;

&lt;p&gt;Purity matters here. A preview operation should be safe to run repeatedly without creating decisions, sending notifications, or mutating state. That keeps calculation testable and prevents a read-like action from acquiring an unexpected side effect.&lt;/p&gt;

&lt;p&gt;When essential evidence is missing, the evaluator should return an advisory state with no suggestion. A nullable recommendation can be a feature: it says the software cannot justify a conclusion. Returning the most likely answer would make uncertainty easy to rubber-stamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make rules prove their intended behaviour
&lt;/h2&gt;

&lt;p&gt;Syntactically valid configuration is not the same as reviewed policy. A collection of thresholds can pass field validation and still express something the author did not mean.&lt;/p&gt;

&lt;p&gt;Before a configurable rule becomes active, require named worked examples. Each example contains representative inputs and the outcome the author expects. Activation replays every example through the same evaluator used by real records. If one disagrees, activation stops.&lt;/p&gt;

&lt;p&gt;Using the same evaluator is important. A second “fixture evaluator” would only prove that two implementations can disagree in production.&lt;/p&gt;

&lt;p&gt;Malformed examples should fail before replay. If a parser silently misreads an input and the example happens to reach the expected result, the gate gives false confidence. An explicit failure is safer than a coincidental pass.&lt;/p&gt;

&lt;p&gt;These examples act as executable policy conversations. They let reviewers ask, “What should happen in this borderline case?” before the rule affects a real decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist recommendation and decision separately
&lt;/h2&gt;

&lt;p&gt;The final record should not overwrite the computed recommendation. Store both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the recommendation available at decision time;&lt;/li&gt;
&lt;li&gt;the outcome the person recorded;&lt;/li&gt;
&lt;li&gt;the exact rule version that informed it;&lt;/li&gt;
&lt;li&gt;the actor and timestamp; and&lt;/li&gt;
&lt;li&gt;a reason when the decision departs from the recommendation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns disagreement into useful data instead of treating it as an error to conceal. A departure may reveal legitimate context that the model deliberately does not represent. It may also reveal a weak rule. Either way, preserving both facts makes later review possible.&lt;/p&gt;

&lt;p&gt;The same principle applies to overrides. Do not edit the old outcome in place. Append a new version, mark it as current, retain the previous value, and require a reason. Historical records should describe what was known and decided then—not what today’s configuration would calculate now.&lt;/p&gt;

&lt;p&gt;Pinning the rule version is crucial. If a rule changes next month, an old decision must still point to the version that produced its original recommendation. Otherwise, history becomes a moving target.&lt;/p&gt;

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

&lt;p&gt;Formula tests are necessary, but the more revealing tests are often about authority and time.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;previewing produces an explanation and writes nothing;&lt;/li&gt;
&lt;li&gt;missing essential evidence produces no recommendation;&lt;/li&gt;
&lt;li&gt;a rule without worked examples cannot be activated;&lt;/li&gt;
&lt;li&gt;an example that disagrees with the rule blocks activation;&lt;/li&gt;
&lt;li&gt;departing from a recommendation without a reason is refused;&lt;/li&gt;
&lt;li&gt;an override appends a version instead of editing history; and&lt;/li&gt;
&lt;li&gt;changing a rule later does not alter an already recorded decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests express the operating model. They protect not only mathematical correctness, but also who is allowed to decide and whether past context remains trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accept the deliberate friction
&lt;/h2&gt;

&lt;p&gt;This approach costs more than returning an enum and saving it. It introduces draft and active rule states, advisory outcomes, versioned decisions, reasons, worked examples, and review steps. Operators need to understand the distinction. Developers have more invariants to maintain.&lt;/p&gt;

&lt;p&gt;That friction should be proportional to consequence. A low-stakes recommendation may not need an append-only ledger. A decision affecting access, eligibility, money, safety, or a person’s future probably deserves more than an opaque calculation.&lt;/p&gt;

&lt;p&gt;Worked examples also have limits. They prove that the authored rule behaves as expected on selected cases. They do not prove that the policy is fair, lawful, complete, or statistically sound. Human approval can still be mistaken. Permissions, peer review, monitoring, and periodic policy review remain necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical design question
&lt;/h2&gt;

&lt;p&gt;When reviewing a rule-driven workflow, ask one question first: is this output evidence for a decision, or is the system silently making the decision itself?&lt;/p&gt;

&lt;p&gt;If the answer matters, create a visible boundary. Keep evaluation pure. Refuse to guess when evidence is insufficient. Gate activation with executable examples. Record human choices separately. Preserve departures and history.&lt;/p&gt;

&lt;p&gt;The goal is not to weaken automation. It is to make its authority honest.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>management</category>
    </item>
    <item>
      <title>When a Green Test Protects the Wrong Authorization Rule</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Tue, 22 Sep 2026 07:07:37 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/when-a-green-test-protects-the-wrong-authorization-rule-5bfn</link>
      <guid>https://dev.to/iqtechsolutions/when-a-green-test-protects-the-wrong-authorization-rule-5bfn</guid>
      <description>&lt;p&gt;An automated test is useful only when its assertion represents the rule we intend to preserve. That sounds obvious until a test passes for the exact condition that should make a reviewer uneasy.&lt;/p&gt;

&lt;p&gt;A recent Blazor change offered a concrete reminder. Two routed administrative pages required a signed-in user but did not assert a permission policy at the page. The links that led to them were filtered by permission. That made the interface look appropriately restricted, while a signed-in person could still enter the URL directly. Two existing tests expected the permissive page attributes, so the green suite helped the gap survive.&lt;/p&gt;

&lt;p&gt;The engineering lesson is broader than those pages: when a test blesses an authorization omission, fix the assertion and add an invariant that catches the next omission. Keep the invariant honest about what it can and cannot prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigation visibility is a different boundary
&lt;/h2&gt;

&lt;p&gt;Consider an administrative interface with a configuration card. The card appears only when the current viewer has a configuration permission. The destination page, however, asks only whether the viewer is signed in. Hiding the card changes what the viewer sees; it does not change what the route accepts.&lt;/p&gt;

&lt;p&gt;The direct URL is the simplest counterexample. If the user knows or guesses it, the navigation filter is bypassed. A page-level policy is needed to decide whether that route may render for the viewer. The underlying API or command must make its own authorization decision as well. These checks serve different entry points, so one cannot stand in for the other.&lt;/p&gt;

&lt;p&gt;In the inspected change, the page policies were added. One data endpoint already had a stricter authorization rule and kept it. The lesson is to review each boundary on its own terms: visible link, routed page, and operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read assertions as product decisions
&lt;/h2&gt;

&lt;p&gt;The surprising part was not that a page attribute was too broad. It was that tests explicitly expected that breadth. One assertion accepted the absence of a policy; another looked for a bare sign-in attribute. Those statements had made an implementation detail into a contract.&lt;/p&gt;

&lt;p&gt;When correcting a test like this, start by naming the intended behaviour in plain language. For example: “This routed configuration page requires the configuration policy.” Then inspect the compiled authorization attribute and assert that policy. This makes the test follow the behaviour the framework will see, rather than a particular text fragment in a Razor file.&lt;/p&gt;

&lt;p&gt;That specific test matters even if a broader scan exists. A scan can report that a policy is present, but a policy for an unrelated administrative task could still pass it. The page-specific assertion checks the narrower decision about &lt;em&gt;which&lt;/em&gt; permission belongs there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the omission into an invariant
&lt;/h2&gt;

&lt;p&gt;The change also added a structural test over routed administrative pages in two application areas. It checks whether each page asserts a permission policy. The test has a nonempty coverage guard, because a scan that accidentally finds zero pages is worse than a failing scan: it can look reassuring while doing nothing.&lt;/p&gt;

&lt;p&gt;This is a useful pattern for requirements that should apply across a family of files. A single page test protects a known case. A route scan makes it harder for a new page to repeat the same omission. Neither test needs to simulate every user and role to answer the structural question, “Did this route declare a permission policy?”&lt;/p&gt;

&lt;p&gt;The boundary of that answer is important. The scan does not prove that every declared policy is correct. It does not prove that an API rejects unauthorized calls. It does not prove that authorization is complete across the product. Those claims require different tests and review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make exceptions accountable
&lt;/h2&gt;

&lt;p&gt;Some landing pages can legitimately be available to any signed-in viewer because their contents filter themselves by the viewer's permissions. A blanket rule that rejects every such page would create noise and encourage people to weaken the test. The committed scan therefore allows explicit exemptions with reasons.&lt;/p&gt;

&lt;p&gt;An exemption is still a maintenance obligation. If a page is deleted, renamed, or later gains a policy, its old exemption should disappear. A second test checks for those stale entries. It keeps the exception list from becoming a historical catalogue that a future developer might mistake for permission to add more exceptions.&lt;/p&gt;

&lt;p&gt;The two corrected pages are pinned separately. That matters because someone could otherwise make the broad scan green by putting a vulnerable page on the exemption list. The pinned tests say those particular pages must keep their policies and cannot be exempted.&lt;/p&gt;

&lt;p&gt;The same change initially documented a separate unresolved permission decision as a known gap. A follow-up commit assigned that page a policy after an owner made the decision. Recording the gap first kept the review honest while the right policy was decided rather than guessed from a neighbouring page.&lt;/p&gt;

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

&lt;p&gt;When a green test has protected the wrong rule, I use this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce the bypass at the relevant entry point in a controlled test or review, such as direct route access rather than clicking the navigation card.&lt;/li&gt;
&lt;li&gt;State the intended permission and correct the local assertion so it checks the framework-visible attribute.&lt;/li&gt;
&lt;li&gt;Add a structural invariant if the same omission can recur across many pages.&lt;/li&gt;
&lt;li&gt;Give every real exception a reason, and make stale exceptions fail.&lt;/li&gt;
&lt;li&gt;Keep page and operation authorization under separate review; presence of a page policy is only one piece of evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach costs some test and exception-list maintenance. The benefit is a much earlier signal when another route omits a policy, while the specific assertions preserve the decisions that matter most. A green suite should mean that our intended boundary is holding, not merely that yesterday's implementation has stayed the same.&lt;/p&gt;

</description>
      <category>security</category>
      <category>testing</category>
      <category>blazor</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Revoke an Established SignalR Connection Safely</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Mon, 21 Sep 2026 22:38:37 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/revoke-an-established-signalr-connection-safely-4e9b</link>
      <guid>https://dev.to/iqtechsolutions/revoke-an-established-signalr-connection-safely-4e9b</guid>
      <description>&lt;p&gt;A successful membership removal stops new requests, but an existing SignalR connection may still carry an earlier principal and tenant group membership. Those are separate surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track the minimum live state
&lt;/h2&gt;

&lt;p&gt;Keep an in-process registry from tenant and connection ID to user ID. Avoid tokens, email addresses, and message bodies. A per-tenant gate coordinates admission, revocation, and outgoing broadcasts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate both directions
&lt;/h2&gt;

&lt;p&gt;On connection, check the exact tenant, membership version, and security stamp against the live row before joining the group. A hub filter repeats that check before each client-invoked method. A method check does not stop group delivery, so after a successful membership commit, remove the user's matching connection IDs from that tenant group.&lt;/p&gt;

&lt;h2&gt;
  
  
  State the ordering guarantee
&lt;/h2&gt;

&lt;p&gt;A chat broadcast takes the same tenant gate and rechecks its sender before saving or sending. Once the removal response returns, a later broadcast cannot select the removed connection. A frame already dispatched before removal cannot be recalled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test an established connection
&lt;/h2&gt;

&lt;p&gt;Hold real SignalR clients open during owner removal. The remaining member receives a subsequent message. The removed member receives none, its next send fails, and no rejected text appears in persistence. New-negotiation and tenant-isolation tests still run.&lt;/p&gt;

&lt;p&gt;This registry is single-node. A multi-node deployment needs shared revocation coordination or a managed SignalR service. The next episode transfers ownership atomically.&lt;/p&gt;

&lt;p&gt;Watch the 6-minute lesson: &lt;a href="https://youtu.be/RFI5AaHbzMU" rel="noopener noreferrer"&gt;https://youtu.be/RFI5AaHbzMU&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>programming</category>
    </item>
    <item>
      <title>Offline Readiness Is a State-Machine Contract</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:07:16 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/offline-readiness-is-a-state-machine-contract-4coc</link>
      <guid>https://dev.to/iqtechsolutions/offline-readiness-is-a-state-machine-contract-4coc</guid>
      <description>&lt;p&gt;“Make it work offline” often turns into a storage task: serialize the last successful response, read it when the network fails, and move on.&lt;/p&gt;

&lt;p&gt;That is necessary, but it is not the hard part.&lt;/p&gt;

&lt;p&gt;The hard part is deciding what the stored response means across launch, failure, reconnect, configuration change, and session recovery. Once a mobile application depends on runtime configuration, offline readiness becomes a state-machine contract. A cache is only one implementation detail inside that contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the contradictory assumptions first
&lt;/h2&gt;

&lt;p&gt;Consider a .NET MAUI wrapper hosting a Blazor UI. A lower startup layer may correctly treat a reachability probe as diagnostic: the network can be absent, so failure should not prevent launch. But a higher layer may immediately call a runtime-configuration endpoint and require a successful answer before activating the application scope.&lt;/p&gt;

&lt;p&gt;Each layer looks reasonable in isolation. Together they disagree.&lt;/p&gt;

&lt;p&gt;The lower layer says, “reachability is not a launch gate.” The higher layer says, “no live response, no ready state.” A short outage is then presented as invalid configuration even though nothing has been invalidated.&lt;/p&gt;

&lt;p&gt;This is why I like to draw the complete launch state machine before changing storage. List the states—uninitialised, resolving, ready from live data, ready from fallback data, unavailable—and name the events that move between them. Contradictions become much easier to see when they are transitions rather than scattered &lt;code&gt;if&lt;/code&gt; statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify failure before consulting the cache
&lt;/h2&gt;

&lt;p&gt;Not every failed request grants permission to use stale data.&lt;/p&gt;

&lt;p&gt;A timeout, lost connection, or temporary server error says, “the current answer is unavailable.” A recent cached configuration may be the best safe answer.&lt;/p&gt;

&lt;p&gt;An authoritative not-found or gone response says something different: “this configuration no longer exists here.” Serving the cache would override current server knowledge with old local knowledge. That is not resilience; it is refusal to accept invalidation.&lt;/p&gt;

&lt;p&gt;So the order matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attempt the live load.&lt;/li&gt;
&lt;li&gt;Classify the failure.&lt;/li&gt;
&lt;li&gt;Consider fallback only for the transient class.&lt;/li&gt;
&lt;li&gt;Clear the fallback when an authoritative response makes it wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This distinction is reusable well beyond configuration. Offline permissions, feature manifests, routing metadata, and other control-plane data all need an explicit invalidation rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make “recent enough” a real policy
&lt;/h2&gt;

&lt;p&gt;Even a transient failure does not make every cached value safe.&lt;/p&gt;

&lt;p&gt;A useful policy checks at least four things. The entry must belong to the same logical scope. It must come from the same configured source. Its age must be within a deliberate bound. Its timestamp must not be implausibly far in the future.&lt;/p&gt;

&lt;p&gt;Source binding is easy to miss. Runtime configuration often contains addresses and capability switches. If an application is repointed but continues serving a cache captured from the old source, it may look healthy while sending work to the wrong place. Treating a source change as a cache miss is safer and easier to reason about.&lt;/p&gt;

&lt;p&gt;The age bound is a product and operational decision, not a magic constant. It should cover the outages the application is expected to tolerate while still placing a limit on how long retired configuration can survive. Document the reasoning so the next engineer knows whether changing it is a reliability decision or a cosmetic edit.&lt;/p&gt;

&lt;p&gt;Clock skew deserves a rule too. A future-dated entry can otherwise remain “fresh” for far longer than intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve continuity when connectivity returns
&lt;/h2&gt;

&lt;p&gt;An application that opens from fallback data is usable, but not settled. It should carry an explicit “needs refresh” hint.&lt;/p&gt;

&lt;p&gt;When connectivity returns, the tempting implementation is to rerun the entire startup pipeline. That can be surprisingly destructive. Full reinitialisation may advance a session epoch, cancel in-flight requests, clear actor state, restart background rails, and remount the renderer. On a flapping connection, users can pay that cost repeatedly.&lt;/p&gt;

&lt;p&gt;Prefer an in-place refresh while the current scope remains valid. Update the runtime configuration, clear the refresh hint, and let the active session continue. Escalate to full resolution only if the refresh proves that the scope disappeared or changed in a way that makes continuity unsafe.&lt;/p&gt;

&lt;p&gt;This gives reconnect two distinct transitions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unavailable to online: resolve fully because there is no active scope to preserve;&lt;/li&gt;
&lt;li&gt;ready-from-fallback to online: refresh in place because there is useful state to protect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those transitions may share a network event, but they should not share an indiscriminate reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let storage fail softly—and test the wiring
&lt;/h2&gt;

&lt;p&gt;Device storage is another dependency, not a certainty. Reads can fail, writes can fail, and cleanup can fail. If a cache exists to make cold start more resilient, an escaping storage exception must not become a new cold-start crash.&lt;/p&gt;

&lt;p&gt;Fail-soft behaviour should be intentional: a failed read behaves like a miss, a failed write loses future offline convenience, and a failed clear remains bounded by the expiry policy. Cancellation is different and should still propagate.&lt;/p&gt;

&lt;p&gt;Test the serializer separately with round trips. A cache that writes successfully but cannot read its own format is indistinguishable from having no fallback at all.&lt;/p&gt;

&lt;p&gt;Also test dependency-injection resolution through the real container. Optional constructor dependencies are convenient, but they can compile and quietly fall back to a null implementation when registration is missing. A focused integration test proves that the platform adapter actually reaches the service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off is explicit complexity
&lt;/h2&gt;

&lt;p&gt;This design is more complex than “try the network, then read a file.” It introduces failure classification, cache provenance, expiry, clock rules, lifecycle state, reconnect transitions, serialization, and a wider test matrix.&lt;/p&gt;

&lt;p&gt;That complexity is buying something concrete: continuity without pretending stale configuration is always correct.&lt;/p&gt;

&lt;p&gt;The practical review question is not, “Do we have a cache?” Ask instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which state are we in, what evidence permits this fallback, and what is the least disruptive path back to live truth?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the implementation can answer that clearly, offline readiness has become a deliberate contract rather than an accidental side effect.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>mobile</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Pagination Is a Consistency Contract</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sun, 20 Sep 2026 07:51:38 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/pagination-is-a-consistency-contract-265m</link>
      <guid>https://dev.to/iqtechsolutions/pagination-is-a-consistency-contract-265m</guid>
      <description>&lt;p&gt;Page numbers feel harmless. They are familiar, easy to render, and easy to explain. But on a mutable work queue, they can lie.&lt;/p&gt;

&lt;p&gt;Imagine a reviewer reading a newest-first queue. They load the first 25 items. Before they request the next page, a new item arrives at the top. An offset such as &lt;code&gt;skip 25&lt;/code&gt; now starts one row later than it would have a moment ago. The last row from page one may appear again, and the row displaced by the insert may never appear in that walk.&lt;/p&gt;

&lt;p&gt;Nothing crashed. No error was logged. The interface simply presented an inconsistent view of the work.&lt;/p&gt;

&lt;p&gt;The engineering lesson is that pagination over changing data is a consistency contract, not a decorative control beneath a table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Begin with the mutation model
&lt;/h2&gt;

&lt;p&gt;The right pagination strategy depends on how the collection changes.&lt;/p&gt;

&lt;p&gt;For an append-mostly queue ordered newest first, new work normally arrives above the operator's current position. Other fields, such as workflow status, may change while the operator is reviewing an item. Those two facts suggest a useful boundary: continue from something fixed when the item entered the queue, not from a mutable field and not from the current numeric position.&lt;/p&gt;

&lt;p&gt;A submission timestamp can provide that boundary if it is frozen when the item is created. Status usually cannot; ordering by status lets a decision move rows between pages while someone is walking the list.&lt;/p&gt;

&lt;p&gt;The principle is broader than timestamps: choose an ordering key whose meaning remains stable for the duration of a walk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the server own the continuation
&lt;/h2&gt;

&lt;p&gt;A client should not reconstruct position from a page number, a visible row, or a guessed offset. The server should return an opaque continuation and accept it unchanged on the next request.&lt;/p&gt;

&lt;p&gt;For a newest-first queue, a continuation can describe the last timestamp boundary and how many rows at that exact instant have already been emitted. The next query includes rows at or below the boundary, skips only the already-seen ties at that boundary, and takes one extra row to determine whether another page exists.&lt;/p&gt;

&lt;p&gt;Why count ties instead of comparing an identifier? Identifier ordering is not always portable across database providers. Let the query provider apply its established ordering, and record only how far through the boundary group the walk has progressed.&lt;/p&gt;

&lt;p&gt;Opaque does not mean unchecked. Reject blank, malformed, oversized, or unsupported continuations. A bad continuation should not silently restart at the top, because a restart looks like valid data with unexplained duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind position to query context
&lt;/h2&gt;

&lt;p&gt;A continuation names a position in one specific ordered result set. Change the filter and that position may no longer mean anything.&lt;/p&gt;

&lt;p&gt;Suppose page one was filtered to one workflow cycle, then the same continuation was replayed with the filter removed. Honouring it could skip rows that were never part of the original walk. The continuation should therefore carry enough context to prove it belongs to the active query. If the context differs, refuse the request and let the client restart explicitly.&lt;/p&gt;

&lt;p&gt;The same discipline applies to page size. Put a small upper bound in the contract and reject values outside it. Quietly shrinking an oversized request hides caller mistakes; accepting it defeats the point of paging.&lt;/p&gt;

&lt;p&gt;Bounding the first query is only half the work. If each row needs evidence, comments, or another related record, fetch those details only for identifiers on the current page. Otherwise an apparently paged endpoint can still perform an unbounded secondary read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the operator's place
&lt;/h2&gt;

&lt;p&gt;Continuation cursors naturally move forward. A usable review screen also needs Back and refresh.&lt;/p&gt;

&lt;p&gt;The client can keep a short trail of the server-provided continuations used to open each visited page. Previous then replays the cursor that originally opened the prior page. The client is not interpreting the cursor; it is remembering the path.&lt;/p&gt;

&lt;p&gt;After a reviewer changes an item, refresh the page they are currently viewing. Returning to page one after every decision is technically simple and operationally punishing. It also encourages people to work around the interface rather than trust it.&lt;/p&gt;

&lt;p&gt;There is one awkward edge case: a later page may become empty before it is refreshed. Do not render a global empty-state message that implies the entire queue has vanished. Reset to the first page and show the current truth from a valid starting position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the walk, not only the query
&lt;/h2&gt;

&lt;p&gt;Happy-path tests that assert “25 rows returned” are not enough. The important properties live between requests.&lt;/p&gt;

&lt;p&gt;Useful executable checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new item arriving above the boundary neither duplicates nor hides an existing row;&lt;/li&gt;
&lt;li&gt;a workflow status change does not alter page membership;&lt;/li&gt;
&lt;li&gt;several items sharing one timestamp are neither repeated nor lost;&lt;/li&gt;
&lt;li&gt;a continuation cannot be replayed with a different filter;&lt;/li&gt;
&lt;li&gt;malformed continuations and invalid page sizes are refused;&lt;/li&gt;
&lt;li&gt;forward and backward navigation use the exact server continuations;&lt;/li&gt;
&lt;li&gt;a write refreshes the active page; and&lt;/li&gt;
&lt;li&gt;an empty later page recovers without claiming the whole queue is empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests describe the contract more clearly than the word “paged” ever could.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off is explicit complexity
&lt;/h2&gt;

&lt;p&gt;Cursor paging asks more of both sides. The server must define and validate a stable continuation. The client must retain a small history. Ties and disappearing pages need deliberate behaviour. Troubleshooting is less intuitive than looking at &lt;code&gt;page=3&lt;/code&gt; in a URL.&lt;/p&gt;

&lt;p&gt;In return, database work stays bounded and a reviewer can walk a changing queue without silent duplicates or gaps. That is a worthwhile trade whenever missing one item matters more than keeping the paging code superficially simple.&lt;/p&gt;

&lt;p&gt;Before choosing offsets for a mutable list, simulate one insert and one update between consecutive requests. If the same walk no longer produces the same set of rows, the pager needs a stronger contract.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>blazor</category>
      <category>architecture</category>
    </item>
    <item>
      <title>When EF Core Sees Only Part of Your Model</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sat, 19 Sep 2026 12:18:25 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/when-ef-core-sees-only-part-of-your-model-4ge2</link>
      <guid>https://dev.to/iqtechsolutions/when-ef-core-sees-only-part-of-your-model-4ge2</guid>
      <description>&lt;p&gt;Framework warnings are valuable because they turn hidden risk into visible friction. But a warning is still an interpretation of the evidence available to the tool. In a modular application, the command-line process may not load the same assemblies as the running application. When that happens, the design-time model can be a projection rather than the whole truth.&lt;/p&gt;

&lt;p&gt;That creates an uncomfortable situation: a migration warning may be both technically correct about the model it sees and dangerously wrong about the database operation it proposes.&lt;/p&gt;

&lt;p&gt;The answer is not to ignore warnings casually. It is to identify the boundary, narrow the exception, and replace the lost signal with a stronger executable invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model can change with the process
&lt;/h2&gt;

&lt;p&gt;Consider a lower-level persistence module that owns a database context. At runtime, higher-level modules can contribute entity configurations through a discovery mechanism. This preserves the dependency direction: the lower-level module does not need references back to every feature that extends it.&lt;/p&gt;

&lt;p&gt;Now run command-line migration tooling from the lower-level project. The higher-level contributor may not be loaded, because adding that reference would create a project cycle. The tool builds a valid model, but it is only the portion visible from that process.&lt;/p&gt;

&lt;p&gt;The migration snapshot, meanwhile, includes the contributed table. It was created from the full application model and must remain there for every host that uses the shared schema.&lt;/p&gt;

&lt;p&gt;The differ compares those two views and concludes that the table exists in the snapshot but not in the current model. Its proposed fix is logical from that narrow perspective: drop the table.&lt;/p&gt;

&lt;p&gt;From the system’s perspective, that operation is destructive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not “fix” the snapshot to match a blind spot
&lt;/h2&gt;

&lt;p&gt;Regenerating the snapshot until the warning disappears is tempting. It is also exactly the wrong response when the design-time model is incomplete by construction.&lt;/p&gt;

&lt;p&gt;The first diagnostic question should be: what is the entire proposed difference? A probe migration can make the answer concrete. If the only change is removal of a table supplied by a module that the tooling process cannot load, the warning describes a visibility gap—not necessarily real drift.&lt;/p&gt;

&lt;p&gt;This distinction matters. “The warning is a false positive” is too broad. A particular warning, for a particular context, under a documented loading boundary may be a false positive. Other differences could still be real.&lt;/p&gt;

&lt;p&gt;The safe decision is therefore narrow: allow the legitimate database-update path to proceed despite that one known condition, while keeping a separate full-model comparison as the authoritative drift test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suppression spends a safety signal
&lt;/h2&gt;

&lt;p&gt;Ignoring a warning removes friction. It does not remove the underlying ambiguity.&lt;/p&gt;

&lt;p&gt;If a design-time factory suppresses a known pending-model warning, the team has spent a safety signal. That signal should be replaced with evidence closer to the failure mode.&lt;/p&gt;

&lt;p&gt;In this case, the feared outcome is not an abstract “model mismatch.” It is a migration that drops, renames, or damages a table the design-time process cannot see. That can be tested directly.&lt;/p&gt;

&lt;p&gt;An executable migration guard can inspect the operations produced by every committed migration and fail when it finds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a drop of the protected table;&lt;/li&gt;
&lt;li&gt;a rename that moves it out of the contract;&lt;/li&gt;
&lt;li&gt;a column removal targeting that table; or&lt;/li&gt;
&lt;li&gt;raw SQL containing the destructive operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more specific than the original warning, and that specificity is a strength. The test says exactly which invariant must survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Negative guards need positive controls
&lt;/h2&gt;

&lt;p&gt;There is a subtle testing trap here. A test that scans migrations and finds no forbidden operation can pass because the migrations are safe—or because discovery found nothing.&lt;/p&gt;

&lt;p&gt;That is a vacuous pass.&lt;/p&gt;

&lt;p&gt;Add a positive control that proves the scan sees the migration that originally created the protected table. Now the pair of tests establishes both halves of the claim:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the evidence set is present; and&lt;/li&gt;
&lt;li&gt;none of that evidence violates the destructive-operation invariant.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern generalizes well. Whenever a test says “nothing bad exists,” pair it with proof that the search space is populated. It is useful for dependency scans, authorization maps, route inventories, migration checks, and reflection-based conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the full model as the authority
&lt;/h2&gt;

&lt;p&gt;The migration guard protects one known destructive edge. It does not prove that every legitimate model change has a migration.&lt;/p&gt;

&lt;p&gt;For that, retain a snapshot comparison that runs in a process where all runtime contributors are loaded. That test compares the snapshot with the full model rather than the design-time projection.&lt;/p&gt;

&lt;p&gt;These checks have different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the full-model comparison detects real snapshot drift;&lt;/li&gt;
&lt;li&gt;the migration guard blocks the known destructive artifact;&lt;/li&gt;
&lt;li&gt;the positive control proves the guard actually inspected meaningful input; and&lt;/li&gt;
&lt;li&gt;the narrowly scoped suppression keeps the operational update path usable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, they are stronger than treating one framework warning as universally authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off is deliberate maintenance
&lt;/h2&gt;

&lt;p&gt;This approach is not free. The guard knows about a specific architectural boundary. If extension mechanics change, the tests and documentation must change with them. Contributors need to understand why the suppression exists and why generated migration output still requires review.&lt;/p&gt;

&lt;p&gt;The alternative is worse: either block legitimate operations forever, or normalize a habit of dismissing warnings and accepting generated drops.&lt;/p&gt;

&lt;p&gt;The practical review rule is simple: when tooling sees only a projection, document the blind spot, scope the exception, and encode the real safety property as a failing test. Framework diagnostics are excellent inputs. Your architecture still owns the final claim.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>csharp</category>
    </item>
    <item>
      <title>A Successful Security Call May Prove Nothing</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:45:36 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-successful-security-call-may-prove-nothing-4g15</link>
      <guid>https://dev.to/iqtechsolutions/a-successful-security-call-may-prove-nothing-4g15</guid>
      <description>&lt;p&gt;Security integrations often fail in a surprisingly polite way. The library loads. Initialization succeeds. The function returns. Nothing crashes. Yet the protection we expected may not have participated at all.&lt;/p&gt;

&lt;p&gt;That distinction matters when an application accepts untrusted documents that another person may later open. The relevant question is not “did the scanning API answer?” It is “did a real security engine inspect this content and produce a trustworthy verdict?”&lt;/p&gt;

&lt;p&gt;A recent engineering change reinforced a pattern worth carrying into other systems: prove the capability end to end, fail closed when the proof is missing, and preserve the difference between malicious content and content that has not been judged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration health is not capability health
&lt;/h2&gt;

&lt;p&gt;Suppose an operating-system security interface sits between your application and whichever antimalware product is installed. It is tempting to treat successful initialization as a health check. A clean transport result feels even stronger: the call returned normally and reported nothing detected.&lt;/p&gt;

&lt;p&gt;But an integration layer and its downstream provider are separate things. The interface can be present while the engine behind it is stopped, unavailable, or incorrectly registered. In that condition, a nominal response may demonstrate only that the front door opened.&lt;/p&gt;

&lt;p&gt;This is a general reliability problem, not an accusation against one API. Message brokers can accept a connection while a critical consumer is stalled. A secrets client can authenticate while the expected key is unavailable. A feature-flag SDK can initialize while serving stale defaults. Health must be defined by the capability the application depends on, not by the first component in the chain.&lt;/p&gt;

&lt;p&gt;For a document scanner, “the library loaded” is weak evidence. “A known signal travelled through the production scanning path and produced the expected security verdict” is much stronger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the behaviour you actually depend on
&lt;/h2&gt;

&lt;p&gt;The design used a harmless standard test signal that security engines are expected to recognize. The literal signal does not belong in application content or public examples; what matters is its role.&lt;/p&gt;

&lt;p&gt;At startup, the application sends that signal through the same in-memory scanning boundary used for uploaded bytes. The feature is registered only if the expected detection comes back. On unsupported hosts, or when no live engine answers, the risky upload capability remains unavailable.&lt;/p&gt;

&lt;p&gt;That is a capability probe. It asks the downstream system to demonstrate the exact category of behaviour the application needs before the application trusts it.&lt;/p&gt;

&lt;p&gt;The same proof is repeated when a scan begins. Startup checks become stale. A service can stop, a provider can be disabled, or policy can change while the host remains running. Rechecking at the point of use makes failure visible before untrusted content is accepted as safe.&lt;/p&gt;

&lt;p&gt;This does add work. A liveness probe consumes a small amount of time, and the application may temporarily deny uploads during a dependency outage. That cost is deliberate. Availability is being traded for an honest security boundary rather than for a comforting but unsupported status flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Malicious” and “not judged” are different states
&lt;/h2&gt;

&lt;p&gt;Failing closed does not mean treating every failure as malware.&lt;/p&gt;

&lt;p&gt;A positive engine verdict can justify refusing a document. An administrator policy may also explicitly block it. Those are security decisions based on evidence.&lt;/p&gt;

&lt;p&gt;An exception is different. The bytes may be missing. The scanning interface may fail to initialize. The engine may disappear between startup and use. None of those facts proves that the document is malicious. They prove that the system does not have a verdict.&lt;/p&gt;

&lt;p&gt;A useful state model keeps at least three outcomes separate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cleared: the control produced an acceptable verdict.&lt;/li&gt;
&lt;li&gt;Refused: the control positively detected or blocked the content.&lt;/li&gt;
&lt;li&gt;Pending: the system could not obtain a trustworthy verdict.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Collapsing the third state into “cleared” silently weakens security. Collapsing it into “refused” creates false certainty and can turn a temporary infrastructure fault into a permanent user-facing decision. Leaving the item pending supports a retry, an operational alert, or a safe recovery path while remaining truthful about what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep adjacent boundaries aligned
&lt;/h2&gt;

&lt;p&gt;The scan is only one part of the upload boundary. The storage layer still owns how opaque keys resolve to files, including protection against paths escaping the intended root. The scanner should use that existing resolver rather than copying path logic into a second component.&lt;/p&gt;

&lt;p&gt;That avoids a subtle split-brain problem: storage accepts one interpretation of a key while scanning uses another. Security checks are strongest when every stage inspects the same bytes through the same trusted boundary.&lt;/p&gt;

&lt;p&gt;Tests should reflect those boundaries too. Pure verdict mapping can be tested with ordinary inputs, but the liveness question deserves at least one test against the real host integration where the platform permits it. A mock can prove that our code handles the answer we programmed the mock to return. It cannot prove that the downstream engine is actually answering.&lt;/p&gt;

&lt;p&gt;Useful tests include clean content, blocked-result ranges, an unavailable provider, missing bytes, and traversal attempts. Together they exercise the difference between a positive verdict, an operational failure, and an invalid storage request.&lt;/p&gt;

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

&lt;p&gt;When reviewing a security or reliability integration, I now ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What observable behaviour proves the downstream capability is alive?&lt;/li&gt;
&lt;li&gt;Are we checking a transport, a library, or the outcome we actually need?&lt;/li&gt;
&lt;li&gt;Can the dependency fail after startup, and is it checked again at use time?&lt;/li&gt;
&lt;li&gt;Does failure disable the risky capability or quietly weaken it?&lt;/li&gt;
&lt;li&gt;Are positive security verdicts kept distinct from operational errors?&lt;/li&gt;
&lt;li&gt;Does the test suite cross the real boundary that a mock would hide?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The broader lesson is simple: successful plumbing is not the same as successful protection. Trust the capability only after it demonstrates the behaviour your application depends on—and keep uncertainty visible when it cannot.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>dotnet</category>
      <category>security</category>
      <category>sre</category>
    </item>
    <item>
      <title>A Green Local Test Can Hide a Broken Project Graph</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Wed, 16 Sep 2026 05:55:06 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-green-local-test-can-hide-a-broken-project-graph-1epp</link>
      <guid>https://dev.to/iqtechsolutions/a-green-local-test-can-hide-a-broken-project-graph-1epp</guid>
      <description>&lt;p&gt;“The tests pass on my machine” is often treated as a testing problem. Sometimes it is really a dependency-graph problem.&lt;/p&gt;

&lt;p&gt;Consider a browser application that contains a small deterministic rule: given one origin, derive another related origin. The rule has no UI state, network access, storage, or framework lifecycle. It deserves focused unit tests. The quickest route appears to be adding a project reference from the test project to the browser application and calling the rule directly.&lt;/p&gt;

&lt;p&gt;A plain local test run can make that choice look correct. Compilation succeeds, the assertions pass, and the change feels complete. Then CI evaluates the same test project with different SDK properties and fails before test discovery. The failure is not in the rule. It comes from importing the application head and all the build behavior attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A project reference imports more than types
&lt;/h2&gt;

&lt;p&gt;We often read a project reference as “this assembly may use those classes.” In SDK-style .NET builds, it also joins build graphs, targets, workloads, generated assets, and property-sensitive behavior.&lt;/p&gt;

&lt;p&gt;An executable browser project is not just another class library. It may bring web-asset processing and browser-specific targets. A test project that references it now participates in enough of that build pipeline for CI properties to matter.&lt;/p&gt;

&lt;p&gt;Imagine that CI disables web-asset work while compiling test projects. That may be a sensible estate-wide convention for ordinary tests. Once a test imports a browser head, however, the browser SDK can expect targets or tasks that the reduced test build does not provide. Evaluation fails before the runner reaches a single assertion.&lt;/p&gt;

&lt;p&gt;The local green run and the CI failure are therefore compatible facts. They exercised different build contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not make the pipeline absorb a bad edge
&lt;/h2&gt;

&lt;p&gt;The immediate reaction might be to special-case the workflow: enable the missing feature for this test project, remove the shared flags, or create a new CI branch for the exception.&lt;/p&gt;

&lt;p&gt;That can make the build green while preserving the architectural problem. A test project still depends on an executable application head only to reach a pure rule. Every future change to that head can now expand the test project’s build surface.&lt;/p&gt;

&lt;p&gt;A better question is: what is the smallest dependency that the test actually needs?&lt;/p&gt;

&lt;p&gt;In this case, it needs the deterministic mapping rule, not the application host. Moving that rule behind an existing neutral library boundary lets both the browser application and the test project depend inward. The invalid application reference disappears, and the graph gains no replacement edge because both consumers already reference the neutral library.&lt;/p&gt;

&lt;p&gt;That is a small refactor with a useful architectural effect: the test compiles against the capability it exercises rather than the executable that happened to contain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The neutral seam is a trade-off, not a slogan
&lt;/h2&gt;

&lt;p&gt;“Move it to shared” can become a dumping-ground strategy. A neutral library should not accumulate unrelated helpers simply because tests can reach it.&lt;/p&gt;

&lt;p&gt;The placement here is deliberately narrower. The rule sits beside the abstraction used by its only application consumer, in a library already shared by the two relevant projects. Its dependencies remain simple, and the compromise is documented.&lt;/p&gt;

&lt;p&gt;The documentation matters. It should explain why the apparently more natural location is currently invalid, why the selected seam is acceptable, and what future condition should trigger another move. For example, if the application head later gains its own compatible test project, the rule may be able to return to the feature boundary without weakening test coverage.&lt;/p&gt;

&lt;p&gt;This is not architectural purity. It is an explicit, reversible decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the build contract that failed
&lt;/h2&gt;

&lt;p&gt;After changing the graph, rerun more than the convenient local command. Use the properties that exposed the failure.&lt;/p&gt;

&lt;p&gt;A focused verification sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the test project no longer references the executable head.&lt;/li&gt;
&lt;li&gt;Confirm both consumers already reference the neutral library.&lt;/li&gt;
&lt;li&gt;Run the focused behavior tests.&lt;/li&gt;
&lt;li&gt;Build or test with the same SDK properties CI supplies.&lt;/li&gt;
&lt;li&gt;Check that no unrelated project edge or asset requirement appeared.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point is not to mimic every pipeline detail locally. It is to reproduce the part of the build contract relevant to the failure. A bare test run proves behavior under default properties; it does not prove compatibility with a property-sensitive CI graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the graph before reviewing the assertion
&lt;/h2&gt;

&lt;p&gt;When a test needs code from an executable head, pause before adding the reference. Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the code actually coupled to the host, or is it a pure rule trapped there?&lt;/li&gt;
&lt;li&gt;Will this reference import browser, web, desktop, or mobile SDK targets?&lt;/li&gt;
&lt;li&gt;Do local and CI builds use the same relevant properties?&lt;/li&gt;
&lt;li&gt;Is there an existing inward-facing seam both projects already depend on?&lt;/li&gt;
&lt;li&gt;If the new location is a compromise, is its exit condition recorded?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lesson applies beyond browser applications. Desktop heads, mobile apps, migration executables, worker hosts, and deployment projects can all carry build behavior that ordinary libraries do not.&lt;/p&gt;

&lt;p&gt;The durable fix is often not another pipeline exception. It is a smaller, more truthful dependency: put the deterministic rule where its consumers can reach it without importing an executable world they do not need.&lt;/p&gt;

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