<?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 CI Retry Should Be an Experiment, Not a Reflex</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Tue, 21 Jul 2026 05:37:10 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/a-ci-retry-should-be-an-experiment-not-a-reflex-4749</link>
      <guid>https://dev.to/iqtechsolutions/a-ci-retry-should-be-an-experiment-not-a-reflex-4749</guid>
      <description>&lt;p&gt;Flaky builds create a difficult kind of pressure. The code may be correct, the failure may disappear on rerun, and the delivery pipeline is blocked right now. Adding a retry feels pragmatic.&lt;/p&gt;

&lt;p&gt;Sometimes it is. But a retry can also make the pipeline look healthier while making its signal less trustworthy.&lt;/p&gt;

&lt;p&gt;A recent .NET CI investigation reminded me of a useful rule: &lt;strong&gt;a retry should change the suspected failure domain and produce evidence&lt;/strong&gt;. If it does neither, it is probably repeating the same experiment and hoping for a different result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first mitigation sounded sensible
&lt;/h2&gt;

&lt;p&gt;The intermittent failure appeared in a Razor-heavy build. The source was valid, local builds were stable, and the error signature changed between hosted CI runs.&lt;/p&gt;

&lt;p&gt;The initial mitigation did what many of us would try:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the build.&lt;/li&gt;
&lt;li&gt;If it fails, stop build processes.&lt;/li&gt;
&lt;li&gt;Clean intermediate output.&lt;/li&gt;
&lt;li&gt;Rerun the same command in the same job.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That approach had a plausible hypothesis behind it: perhaps cached compiler state or stale intermediate files were contaminating the build.&lt;/p&gt;

&lt;p&gt;The problem was not the idea. The problem was leaving the idea unmeasured.&lt;/p&gt;

&lt;p&gt;When we compared the two attempts, they produced the same failure pattern. Cleaning the workspace and restarting build processes had not crossed the boundary where the fault lived. The retry consumed another full build cycle without adding meaningful evidence.&lt;/p&gt;

&lt;p&gt;That creates a second risk. Once a team becomes accustomed to calling a failure “the known flake,” a genuine regression can receive the same label. A retry can quietly turn from mitigation into permission to ignore the gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move the retry to the boundary you suspect
&lt;/h2&gt;

&lt;p&gt;The next version changed the structure of the experiment.&lt;/p&gt;

&lt;p&gt;First, the build and test sequence was extracted into one shared CI definition. Both attempts had to run byte-identical steps. Without that, a passing retry might only prove that the second path tested less than the first.&lt;/p&gt;

&lt;p&gt;Second, the retry moved to a fresh hosted runner and ran only when the first attempt failed. This changed the machine and process boundary instead of merely repeating work inside the same environment. It also kept the happy path free of retry cost.&lt;/p&gt;

&lt;p&gt;Third, the final gate evaluated outcomes explicitly. The primary attempt could pass, or a failed primary could be rescued by a successful second attempt. A cancelled or skipped job was not treated as success.&lt;/p&gt;

&lt;p&gt;This matters because conditional CI jobs often report “skipped,” and broad aggregation expressions can accidentally make a required check look healthy.&lt;/p&gt;

&lt;p&gt;That design made the retry a more useful second opinion. It still did not explain the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture evidence at the point of failure
&lt;/h2&gt;

&lt;p&gt;The important improvement was diagnostic capture.&lt;/p&gt;

&lt;p&gt;On failure, the job recorded machine characteristics and an inventory of generated source files. The goal was to distinguish between competing explanations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generated files exist but are incomplete, suggesting truncation or a generation-stage defect.&lt;/li&gt;
&lt;li&gt;No generated files exist, suggesting the build is failing earlier.&lt;/li&gt;
&lt;li&gt;Failures correlate with constrained resources, suggesting concurrency or memory pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first artifact ruled out the original mental model. No generated files existed, and the errors occurred during parsing. The failure happened before the stage we had been blaming.&lt;/p&gt;

&lt;p&gt;That did not prove the root cause, but it narrowed the search. The changing error signature, the size of the Razor project, and the constraints of the hosted runner supported a new hypothesis: parallel build activity was increasing resource pressure and destabilising the parse stage.&lt;/p&gt;

&lt;p&gt;The build was then serialised, and worker-process reuse was disabled.&lt;/p&gt;

&lt;p&gt;Notice the wording: &lt;strong&gt;supported a hypothesis&lt;/strong&gt;. Not “fixed the compiler.” Not “root cause solved.” The diagnostic capture and fresh-runner fallback stayed in place so later runs could confirm or falsify the theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off is throughput for signal quality
&lt;/h2&gt;

&lt;p&gt;Serial builds are not free. On large runners or well-behaved workloads, reducing parallelism can lengthen feedback time. A fresh-runner retry also consumes extra minutes whenever the first attempt fails.&lt;/p&gt;

&lt;p&gt;The costs can still be acceptable when they are bounded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The normal successful path does not launch another runner.&lt;/li&gt;
&lt;li&gt;Serialisation is scoped to the unstable suite, not the whole pipeline.&lt;/li&gt;
&lt;li&gt;Both attempts use the same build definition.&lt;/li&gt;
&lt;li&gt;Failure artifacts are retained long enough to compare incidents.&lt;/li&gt;
&lt;li&gt;The mitigation has a stated hypothesis and can be removed when the evidence changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The objective is not merely a green badge. It is a gate whose meaning the team can explain.&lt;/p&gt;

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

&lt;p&gt;Before adding a retry to CI, I now want clear answers to five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What failure domain do we suspect?&lt;/strong&gt; Process, workspace, cache, machine, network, or external service?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changes between attempts?&lt;/strong&gt; If nothing relevant changes, the attempts are not independent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are the attempts equivalent?&lt;/strong&gt; A shared definition prevents the retry from becoming a weaker test path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What evidence survives failure?&lt;/strong&gt; Logs alone may not reveal the pipeline stage or resource conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the exit criterion?&lt;/strong&gt; Decide when the retry will be removed, narrowed, or promoted into a permanent resilience policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Retries are sometimes the right operational tool. They can protect delivery while a rare platform defect is investigated. But they should buy time and information, not erase uncertainty.&lt;/p&gt;

&lt;p&gt;The most useful shift for me was simple: stop treating a retry as a button, and start treating it as an experiment.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your Profile Form Shouldn't Change Your Login</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Mon, 20 Jul 2026 05:33:08 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/your-profile-form-shouldnt-change-your-login-4k6l</link>
      <guid>https://dev.to/iqtechsolutions/your-profile-form-shouldnt-change-your-login-4k6l</guid>
      <description>&lt;p&gt;Generic update endpoints are convenient. A profile page loads a DTO, the user edits a few fields, and the server maps the result back onto the stored record. That pattern works well for a biography, display preferences, or a phone number.&lt;/p&gt;

&lt;p&gt;It becomes dangerous when the DTO also contains a security-sensitive identifier such as the sign-in email.&lt;/p&gt;

&lt;p&gt;The subtle problem is not validation. A perfectly formatted address can still belong to somebody else. The real question is authority: has the user proved control of the new address, or have they merely typed it into an authenticated form?&lt;/p&gt;

&lt;p&gt;A recent change across ASP.NET Core, Blazor, and .NET MAUI reinforced a lesson I keep finding useful: sensitive identity mutations deserve intent-revealing commands, not incidental property mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden authority inside an ordinary field
&lt;/h2&gt;

&lt;p&gt;A login email often does more than label an account. It can determine the username, receive password-reset messages, receive security notifications, and act as a lookup key in related data.&lt;/p&gt;

&lt;p&gt;If a general profile update assigns that value directly, the endpoint quietly gains the power to move the account's identity. An authenticated session may be enough to edit a profile, but it should not automatically be enough to redirect account recovery.&lt;/p&gt;

&lt;p&gt;This is where "the UI validates it" is not a defence. Clients change. Multiple clients may call the same API. Requests can be constructed without the UI. The invariant has to live on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the command from the profile update
&lt;/h2&gt;

&lt;p&gt;The safer shape is deliberately more explicit.&lt;/p&gt;

&lt;p&gt;First, the general profile update ignores the login email. The field may still be returned for display, but it is not writable through that operation. This negative rule matters: there should be no second, easier path around the verified workflow.&lt;/p&gt;

&lt;p&gt;Second, a dedicated authenticated command starts the change. It accepts only the proposed address. The server resolves the current account from the authenticated principal rather than accepting an account identifier from the request body. That removes an entire class of "change someone else's value" mistakes.&lt;/p&gt;

&lt;p&gt;Third, the server validates the proposal and creates a change token bound to both the account and the new address. It sends that token to the proposed address. Possession of the current session starts the request; possession of the new mailbox completes it.&lt;/p&gt;

&lt;p&gt;Fourth, the confirmation target is built from trusted server configuration or an allowlist. A client-supplied origin should not decide where a security link points. Otherwise, a legitimate token can be wrapped in an untrusted destination.&lt;/p&gt;

&lt;p&gt;Only the confirmation step performs the authoritative mutation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep projections on the correct side of the boundary
&lt;/h2&gt;

&lt;p&gt;Real systems often mirror an email into profile, contact, search, or role-specific records. Those copies create another temptation: let whichever screen is being edited update every copy, including the identity account.&lt;/p&gt;

&lt;p&gt;That reverses the authority.&lt;/p&gt;

&lt;p&gt;The identity workflow should own the sign-in address. After confirmation, downstream records may be synchronized from that authoritative result. A projection synchronizer should never reach back and modify the login account. Its job is to follow the decision, not make it.&lt;/p&gt;

&lt;p&gt;This distinction also improves failure handling. If a downstream synchronization fails, the system can log the drift and repair it without pretending that the user did not successfully confirm the change.&lt;/p&gt;

&lt;p&gt;Whether you choose immediate retry, a durable outbox, or a reconciliation job depends on the consistency needs of the system. The important part is to make the authority boundary explicit.&lt;/p&gt;

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

&lt;p&gt;This design costs more than assigning a property.&lt;/p&gt;

&lt;p&gt;There is a dedicated request contract, endpoint, token workflow, email template, confirmation handler, client method, and test surface. Users do not see the new value everywhere until confirmation completes. If data is duplicated, projections can temporarily lag and need an observable repair path.&lt;/p&gt;

&lt;p&gt;That complexity is justified because it matches the actual risk. Changing a sign-in identifier is not ordinary CRUD. Treating it as a workflow makes the proof, transition, and failure modes visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests should pin the boundary
&lt;/h2&gt;

&lt;p&gt;The most valuable tests are often negative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A blank or malformed proposal is rejected.&lt;/li&gt;
&lt;li&gt;The current address is rejected as a no-op.&lt;/li&gt;
&lt;li&gt;The account comes from authentication, not a body-supplied identifier.&lt;/li&gt;
&lt;li&gt;Starting the request sends a confirmation link but does not change the identity.&lt;/li&gt;
&lt;li&gt;The token is tied to the proposed address.&lt;/li&gt;
&lt;li&gt;A general profile save cannot change the login email.&lt;/li&gt;
&lt;li&gt;A downstream synchronizer cannot write the identity account.&lt;/li&gt;
&lt;li&gt;The confirmation URL uses a trusted host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests document who is allowed to change what. That is more durable than testing only that a button eventually displays a success message.&lt;/p&gt;

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

&lt;p&gt;When reviewing a broad update DTO, stop at every field and ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is possession of this form enough authority to change this value?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For many fields, the answer is yes. For login identifiers, recovery destinations, payout details, privileged roles, and consent states, it often is not.&lt;/p&gt;

&lt;p&gt;When the answer is no, remove that property from generic mapping. Give it a dedicated command, derive the subject from authenticated context, require the appropriate proof, and test the paths that must remain impossible.&lt;/p&gt;

&lt;p&gt;The goal is not more ceremony. It is making authority visible in the design.&lt;/p&gt;

</description>
      <category>security</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Realtime Is a Hint, Not Your Source of Truth</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:22:20 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/realtime-is-a-hint-not-your-source-of-truth-29i5</link>
      <guid>https://dev.to/iqtechsolutions/realtime-is-a-hint-not-your-source-of-truth-29i5</guid>
      <description>&lt;p&gt;A notification badge looks like a small feature. In a distributed application, it can expose a large architectural assumption.&lt;/p&gt;

&lt;p&gt;I recently investigated a badge that sometimes stayed stale in an ASP.NET Core and Blazor application running across multiple server instances. The notification itself had been created successfully. Durable state was correct. A separate push path could also complete. Yet the visible count did not always change until the user left the screen and returned.&lt;/p&gt;

&lt;p&gt;The bug was not really about arithmetic. It was about assigning too much authority to a realtime transport.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden assumption
&lt;/h2&gt;

&lt;p&gt;The UI initially loaded its count from the server and then relied on live events to remain current. That feels reasonable while developing against one server with a stable connection. In production, several ordinary conditions weaken the assumption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The client can disconnect briefly.&lt;/li&gt;
&lt;li&gt;A load balancer can place the sender and receiver on different instances.&lt;/li&gt;
&lt;li&gt;A workflow can create the same durable state without passing through the page that happens to broadcast.&lt;/li&gt;
&lt;li&gt;A transport or hub can fail after the database operation has already succeeded.&lt;/li&gt;
&lt;li&gt;An idempotent retry can revisit work that should not increment the UI again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these conditions makes realtime technology a poor choice. They simply mean that a realtime message should usually be treated as a hint that fresh state is available, not as the state itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  A three-layer reliability pattern
&lt;/h2&gt;

&lt;p&gt;The repair became much clearer once the responsibilities were separated into three layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  First, persist the truth
&lt;/h3&gt;

&lt;p&gt;The durable record must be committed independently of whether any connected client is listening. If the user closes the app, changes networks, or connects to another instance, the correct unread state must still be recoverable later.&lt;/p&gt;

&lt;p&gt;This ordering matters. Broadcasting first can produce a convincing UI update for data that subsequently fails to commit. Persisting first means a missed broadcast can be healed; an invented broadcast cannot be trusted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second, broadcast from the business chokepoint
&lt;/h3&gt;

&lt;p&gt;The live update originally depended on a narrower UI workflow. Other creation paths could produce the same durable result without emitting the event. Moving the broadcast to the shared dispatch path made the behavior consistent across interactive and background work.&lt;/p&gt;

&lt;p&gt;The broadcast was also tied to the outcome of the durable write. A genuinely new record emits a hint. An idempotent replay that finds the work already completed does not. That prevents duplicate increments and makes the event reflect a real state transition rather than merely an attempted command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Third, reconcile periodically
&lt;/h3&gt;

&lt;p&gt;Even a correctly placed broadcast can be missed. The client therefore refreshes the authoritative count on a bounded interval. The loop is cancelled when the component is disposed, and a transient refresh failure does not kill future attempts.&lt;/p&gt;

&lt;p&gt;This is not polling as the primary experience. Realtime still gives the fast path. Polling is the repair path.&lt;/p&gt;

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

&lt;p&gt;Hybrid delivery is not free.&lt;/p&gt;

&lt;p&gt;Periodic reconciliation adds read traffic. The interval also defines a maximum period of visible staleness when a realtime event is missed. A very short interval increases load and can waste battery or bandwidth on mobile clients. A long interval lowers cost but leaves stale UI visible for longer.&lt;/p&gt;

&lt;p&gt;That makes the interval a product and operational decision, not a magic constant. Ask how damaging stale information is, how expensive the read is, how many active clients exist, and whether the endpoint can use a cheap aggregate or cache.&lt;/p&gt;

&lt;p&gt;For a badge, bounded staleness may be acceptable. For a payment confirmation or permission change, the interaction may require an immediate authoritative read instead. The pattern stays the same, but the recovery budget changes.&lt;/p&gt;

&lt;p&gt;Failure boundaries matter too. Once durable work has succeeded, a hub failure should not turn the whole operation into a failure. Otherwise a temporary transport problem can encourage retries of already-completed business work. Log the broadcast failure, observe it, and let reconciliation repair the presentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the contract, not the transport
&lt;/h2&gt;

&lt;p&gt;The most useful tests were not attempts to prove that a network never fails. They pinned the business boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New durable state produces one realtime hint.&lt;/li&gt;
&lt;li&gt;Previously completed or deduplicated work produces no new hint.&lt;/li&gt;
&lt;li&gt;A failing realtime transport does not invalidate successful durable work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Client-side tests can add the complementary guarantees: an initial authoritative load occurs, the reconciliation loop refreshes state, transient errors do not stop later ticks, and disposal cancels the loop.&lt;/p&gt;

&lt;p&gt;These tests document the intended hierarchy. The database-backed read is authoritative. Realtime reduces latency. Reconciliation restores convergence.&lt;/p&gt;

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

&lt;p&gt;When reviewing a live-update feature, I now ask five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is the source of truth?&lt;/li&gt;
&lt;li&gt;Where is the single shared point that knows a real state transition occurred?&lt;/li&gt;
&lt;li&gt;Can the live event be safely missed or duplicated?&lt;/li&gt;
&lt;li&gt;How and how quickly does the client converge again?&lt;/li&gt;
&lt;li&gt;What happens if the transport fails after the business operation succeeds?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If those answers are vague, the feature may look realtime in a demo while remaining fragile in production.&lt;/p&gt;

&lt;p&gt;The broader lesson is modest: reliability often comes from combining mechanisms with explicit roles. Durable storage provides truth. Realtime provides speed. Periodic reconciliation provides healing. None needs to pretend it can do the others’ job.&lt;/p&gt;

&lt;p&gt;That separation made a small badge easier to trust, and it is a pattern I would reuse for dashboards, counters, presence indicators, job progress, and other derived UI state.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>architecture</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
