<?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: Preeti Islur</title>
    <description>The latest articles on DEV Community by Preeti Islur (@preeti_islur_08045d6b7089).</description>
    <link>https://dev.to/preeti_islur_08045d6b7089</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%2F4051287%2F2fcb53a1-d814-430f-a61e-61db2eca7822.png</url>
      <title>DEV Community: Preeti Islur</title>
      <link>https://dev.to/preeti_islur_08045d6b7089</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/preeti_islur_08045d6b7089"/>
    <language>en</language>
    <item>
      <title>Real-Time Architecture Patterns for Blazor Server and SignalR at Scale</title>
      <dc:creator>Preeti Islur</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:17:52 +0000</pubDate>
      <link>https://dev.to/preeti_islur_08045d6b7089/real-time-architecture-patterns-for-blazor-server-and-signalr-at-scale-5bnk</link>
      <guid>https://dev.to/preeti_islur_08045d6b7089/real-time-architecture-patterns-for-blazor-server-and-signalr-at-scale-5bnk</guid>
      <description>&lt;p&gt;Beyond the basics&lt;/p&gt;

&lt;p&gt;Getting a single SignalR Hub broadcasting to a handful of connected clients is the easy part. The real architectural decisions show up once you need real-time features to work reliably across multiple users, multiple servers, and long-running connections that occasionally drop. This is where a lot of Blazor Server / SignalR apps hit a wall that "add a hub method" doesn't solve.&lt;/p&gt;

&lt;p&gt;Groups: the first scaling decision&lt;/p&gt;

&lt;p&gt;Broadcasting to every connected client is rarely what you want. SignalR Groups let you scope messages to a subset of connections — a specific document's collaborators, a specific tenant's users, a specific dashboard's viewers.&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class CollaborationHub : Hub&lt;br&gt;
{&lt;br&gt;
    public async Task JoinDocument(string documentId)&lt;br&gt;
    {&lt;br&gt;
        await Groups.AddToGroupAsync(Context.ConnectionId, $"doc-{documentId}");&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public async Task NotifyDocumentUpdate(string documentId, string change)
{
    await Clients.Group($"doc-{documentId}").SendAsync("DocumentUpdated", change);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The common mistake here is forgetting to remove connections from groups on disconnect — OnDisconnectedAsync should clean up group membership, or you'll leak stale group entries that accumulate over the app's lifetime.&lt;/p&gt;

&lt;p&gt;Scaling across multiple servers: the backplane problem&lt;/p&gt;

&lt;p&gt;A single server holding all SignalR connections in memory works fine until you need more than one server instance — at which point, a client connected to Server A has no way to receive a message triggered by an event on Server B, unless you introduce a backplane.&lt;/p&gt;

&lt;p&gt;The standard solution is a Redis backplane, which lets all server instances share connection and group state:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
services.AddSignalR()&lt;br&gt;
    .AddStackExchangeRedis(redisConnectionString, options =&amp;gt;&lt;br&gt;
    {&lt;br&gt;
        options.Configuration.ChannelPrefix = "MyApp";&lt;br&gt;
    });&lt;/p&gt;

&lt;p&gt;This is the point where "it works on my machine" and "it works behind a load balancer" diverge — if you skip the backplane, users on different server instances simply won't see each other's real-time updates, and this failure mode is easy to miss in local development where everything runs on one instance.&lt;/p&gt;

&lt;p&gt;Sticky sessions matter for Blazor Server specifically&lt;/p&gt;

&lt;p&gt;Blazor Server has an additional constraint beyond typical SignalR apps: because a circuit holds server-side component state, a user's requests must consistently route to the same server instance for the life of that circuit. If a load balancer round-robins a user's reconnection attempt to a different server, that circuit — and all its in-memory state — is gone.&lt;/p&gt;

&lt;p&gt;This means your load balancer needs sticky sessions (session affinity) configured specifically for Blazor Server apps, in addition to any backplane you set up for a separate custom Hub. These solve different problems: the backplane lets separate hub connections communicate across servers; sticky sessions keep a single circuit's requests on one server so its state isn't lost mid-session.&lt;/p&gt;

&lt;p&gt;Handling reconnection gracefully&lt;/p&gt;

&lt;p&gt;Network drops happen — WiFi hiccups, laptop sleep, mobile network handoffs. Blazor Server has built-in reconnection UI, but the default experience is minimal. A more robust pattern tracks connection state explicitly and gives users clear feedback:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
protected override async Task OnInitializedAsync()&lt;br&gt;
{&lt;br&gt;
    hubConnection.Reconnecting += (error) =&amp;gt;&lt;br&gt;
    {&lt;br&gt;
        connectionState = "Reconnecting...";&lt;br&gt;
        InvokeAsync(StateHasChanged);&lt;br&gt;
        return Task.CompletedTask;&lt;br&gt;
    };&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hubConnection.Reconnected += (connectionId) =&amp;gt;
{
    connectionState = "Connected";
    InvokeAsync(StateHasChanged);
    return Task.CompletedTask;
};

hubConnection.Closed += async (error) =&amp;gt;
{
    connectionState = "Disconnected";
    await InvokeAsync(StateHasChanged);
    // Optionally attempt manual reconnect with backoff here
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;For custom Hub connections (as opposed to the circuit's own reconnection, which Blazor Server handles separately), building in exponential backoff for reconnect attempts avoids hammering the server during a wider outage.&lt;/p&gt;

&lt;p&gt;Message ordering and delivery guarantees&lt;/p&gt;

&lt;p&gt;SignalR doesn't guarantee message delivery or ordering out of the box — if a connection drops between two SendAsync calls, the client may simply never receive one of them, with no automatic retry. For features where this matters (financial updates, sequential state changes), consider:&lt;/p&gt;

&lt;p&gt;Including a sequence number or timestamp in each message so the client can detect gaps&lt;br&gt;
Falling back to a periodic full-state refresh rather than relying purely on incremental real-time messages&lt;br&gt;
Treating SignalR as a "notify and re-fetch" signal rather than the sole source of truth for critical state&lt;br&gt;
Takeaway&lt;/p&gt;

&lt;p&gt;The patterns that matter as real-time apps grow are rarely about the Hub method signatures themselves — they're about what happens when you have more than one server, more than a few users, and connections that don't behave perfectly. Groups, a backplane, sticky sessions for Blazor Server circuits specifically, and honest handling of delivery guarantees are the differences between a demo and something that holds up in production.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Fixing Blazor Server Concurrency Issues: SignalR Circuits, StateHasChanged, and Thread Safety</title>
      <dc:creator>Preeti Islur</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:15:33 +0000</pubDate>
      <link>https://dev.to/preeti_islur_08045d6b7089/fixing-blazor-server-concurrency-issues-signalr-circuits-statehaschanged-and-thread-safety-11io</link>
      <guid>https://dev.to/preeti_islur_08045d6b7089/fixing-blazor-server-concurrency-issues-signalr-circuits-statehaschanged-and-thread-safety-11io</guid>
      <description>&lt;p&gt;Blazor Server doesn't run your app in the browser — it runs on the server and pushes UI updates to the browser over a persistent connection called a "circuit." This circuit is built into Blazor Server itself and exists for every app automatically, whether or not you ever write a line of SignalR code yourself. Every user session gets its own circuit, and every circuit runs on a single logical thread of execution managed by a synchronization context, similar to how WinForms or WPF UI threads work.&lt;/p&gt;

&lt;p&gt;This is exactly where most concurrency bugs come from — and it's important to be clear that this is a &lt;strong&gt;circuit problem, not a "you added SignalR" problem&lt;/strong&gt;. Any code that touches component state from a background thread, a timer, or an async callback outside the normal render flow can violate that single-threaded assumption, regardless of whether your app uses a custom SignalR Hub at all. Blazor won't always throw an obvious error either — sometimes it just silently corrupts UI state or throws a vague &lt;code&gt;InvalidOperationException&lt;/code&gt; about the current thread.&lt;/p&gt;

&lt;p&gt;It's worth separating two things clearly, since they're easy to conflate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The circuit&lt;/strong&gt; — Blazor Server's own internal connection that renders your components. Every Blazor Server app has this, automatically, with no code from you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom SignalR Hub&lt;/strong&gt; — something you explicitly add to your app for app-level real-time features (chat, notifications, live dashboards). This is optional and separate from the circuit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;InvokeAsync&lt;/code&gt;/synchronization-context problem below applies to &lt;strong&gt;both&lt;/strong&gt; — but it's fundamentally a circuit threading issue. A custom Hub callback is just one common trigger for it, not the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most common failure pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD: updating component state from a background task&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;StartBackgroundWork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;someField&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Updated!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// this can throw or silently fail&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem: &lt;code&gt;Task.Run&lt;/code&gt; schedules work on a thread pool thread, completely outside the circuit's synchronization context. When that background thread calls &lt;code&gt;StateHasChanged()&lt;/code&gt;, it's trying to trigger a UI update from a thread Blazor never expected to touch component state.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: InvokeAsync
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GOOD: marshal back onto the circuit's synchronization context&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;StartBackgroundWork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;someField&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Updated!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;InvokeAsync&lt;/code&gt; (inherited from &lt;code&gt;ComponentBase&lt;/code&gt;) dispatches the given delegate back onto the correct synchronization context for that circuit — this is the fix for almost every "why does my background update not show up" bug in Blazor Server.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you also have a custom SignalR Hub
&lt;/h2&gt;

&lt;p&gt;If your app additionally uses its own SignalR Hub (separate from the circuit) — say, to push updates to a component from a hub connection in a real-time dashboard or collaboration feature — the same underlying circuit rule applies. Hub callbacks run on their own thread, not the component's, so they hit the exact same synchronization-context problem as the &lt;code&gt;Task.Run&lt;/code&gt; example above. The Hub isn't a special case; it's just another source of "code running outside the circuit's thread."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD: hub callback directly touching component state&lt;/span&gt;
&lt;span class="n"&gt;hubConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;On&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReceiveUpdate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;latestMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// still the same problem&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GOOD: wrap the callback body in InvokeAsync&lt;/span&gt;
&lt;span class="n"&gt;hubConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;On&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReceiveUpdate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;latestMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A subtler bug: concurrent renders on the same circuit
&lt;/h2&gt;

&lt;p&gt;Even with &lt;code&gt;InvokeAsync&lt;/code&gt; used correctly, if multiple async operations both try to update the same component in overlapping windows, you can get render exceptions like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.InvalidOperationException: The current thread is not associated with the Dispatcher...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.InvalidOperationException: A task was canceled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This usually means two different code paths are racing to call &lt;code&gt;StateHasChanged()&lt;/code&gt; on the same component near-simultaneously. A practical fix is guarding state mutations with a lock object scoped to the circuit's own logic, not a global static lock (which would serialize unrelated users):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SemaphoreSlim&lt;/span&gt; &lt;span class="n"&gt;_updateLock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SafeUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;updateAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_updateLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;updateAction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_updateLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Disposal and circuit lifecycle
&lt;/h2&gt;

&lt;p&gt;A related class of bugs shows up when a component is disposed (user navigates away, tab closes) while a background task is still running and later tries to call &lt;code&gt;InvokeAsync&lt;/code&gt; on a component that no longer exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GOOD: guard against calls after disposal&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_disposed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInitialized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;PollForUpdatesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PollForUpdatesAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_disposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_disposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// safe update here&lt;/span&gt;
            &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_disposed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this guard, you'll eventually see &lt;code&gt;ObjectDisposedException&lt;/code&gt; or circuit errors in production logs from users who navigated away mid-operation — a very common source of intermittent, hard-to-reproduce bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist for diagnosing Blazor Server concurrency bugs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Any &lt;code&gt;StateHasChanged()&lt;/code&gt; called from inside &lt;code&gt;Task.Run&lt;/code&gt;, a timer callback, or a SignalR hub handler needs &lt;code&gt;InvokeAsync&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Background loops need a disposal flag checked before every state update.&lt;/li&gt;
&lt;li&gt;If multiple async paths update the same component, consider a lightweight lock scoped per-component, not global.&lt;/li&gt;
&lt;li&gt;Watch server logs for &lt;code&gt;InvalidOperationException&lt;/code&gt; mentioning the dispatcher or synchronization context — that's almost always this exact class of bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Most Blazor Server concurrency issues trace back to one root cause: code touching component state from outside the circuit's synchronization context. Once you internalize "anything not triggered directly by a Blazor lifecycle method or event handler needs &lt;code&gt;InvokeAsync&lt;/code&gt;," most of these bugs become straightforward to both diagnose and prevent.&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Building and Connecting an MCP Server to Claude Code: A Practical Walkthrough</title>
      <dc:creator>Preeti Islur</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:05:45 +0000</pubDate>
      <link>https://dev.to/preeti_islur_08045d6b7089/building-and-connecting-an-mcp-server-to-claude-code-a-practical-walkthrough-pmi</link>
      <guid>https://dev.to/preeti_islur_08045d6b7089/building-and-connecting-an-mcp-server-to-claude-code-a-practical-walkthrough-pmi</guid>
      <description>&lt;p&gt;Why MCP matters right now&lt;/p&gt;

&lt;p&gt;The Model Context Protocol (MCP) has quickly become the standard way to give AI coding agents — like Claude Code — structured access to your own tools, APIs, and data sources. Instead of copy-pasting context into a chat window, an MCP server exposes your systems as callable tools the agent can use directly, with proper typed inputs and outputs.&lt;/p&gt;

&lt;p&gt;If you're building enterprise SaaS with a lot of internal APIs, this is a genuinely useful integration pattern — not just a novelty.&lt;/p&gt;

&lt;p&gt;The core concept&lt;/p&gt;

&lt;p&gt;An MCP server is just a lightweight service that speaks a specific JSON-RPC-based protocol. It exposes a set of "tools" (functions with defined input schemas) and optionally "resources" (data the agent can read). Claude Code — or any MCP-compatible client — connects to it, discovers the available tools, and can call them mid-conversation.&lt;/p&gt;

&lt;p&gt;The architecture is deliberately simple:&lt;/p&gt;

&lt;p&gt;Server: exposes tools/resources over stdio or HTTP&lt;br&gt;
Client (Claude Code, Claude Desktop, etc.): discovers and calls those tools&lt;br&gt;
Transport: stdio for local servers, Streamable HTTP for remote/hosted servers&lt;br&gt;
Local stdio server vs remote HTTP server&lt;/p&gt;

&lt;p&gt;For quick personal tooling, a local stdio-based MCP server is the simplest starting point — it runs as a subprocess, communicates over standard input/output, and requires zero networking setup. Great for prototyping.&lt;/p&gt;

&lt;p&gt;Once you want a team or multiple clients to share the same server, converting to Streamable HTTP is the natural next step. This lets you host the server on something like Azure Container Apps or AWS App Runner, with proper authentication, instead of every developer running their own local instance.&lt;/p&gt;

&lt;p&gt;The conversion mainly involves:&lt;/p&gt;

&lt;p&gt;Swapping the stdio transport layer for an HTTP-based one that implements the Streamable HTTP spec&lt;br&gt;
Adding session handling, since HTTP is stateless and the protocol expects some continuity across calls&lt;br&gt;
Putting standard auth (API keys, OAuth) in front of it, since anyone who can reach the endpoint can call your tools&lt;br&gt;
Connecting Claude Code to your server&lt;/p&gt;

&lt;p&gt;Once a server is running, pointing Claude Code at it is straightforward — you register the server (local command or remote URL) in Claude Code's configuration, and it becomes available as a tool source in any session. From there, Claude Code can call your custom tools the same way it calls its built-in ones — reading files, running code, or now, hitting whatever internal API or database action you've exposed.&lt;/p&gt;

&lt;p&gt;Where this gets genuinely useful&lt;/p&gt;

&lt;p&gt;The interesting use cases aren't "let the AI read my database" — they're things like:&lt;/p&gt;

&lt;p&gt;Domain-specific coding agents: exposing your own API conventions, schema definitions, or internal libraries as tools so the agent generates code that matches your actual system instead of generic patterns&lt;br&gt;
Agent-to-tool routing: multiple specialized tools (one for database queries, one for deployment, one for internal docs) that the agent picks between based on the task&lt;br&gt;
Multi-agent workflows: chaining several MCP-exposed capabilities together for a task that needs more than one system&lt;br&gt;
Practical lessons&lt;/p&gt;

&lt;p&gt;A few things worth knowing before you build one:&lt;/p&gt;

&lt;p&gt;Schema discipline matters more than it seems. The agent only knows what your tool's input schema tells it — vague or overly permissive schemas lead to more failed or malformed calls.&lt;br&gt;
Stdio-to-HTTP conversion isn't just a networking change. Session state, error handling, and reconnection logic all need rethinking once you're not running as a simple subprocess anymore.&lt;br&gt;
Start with read-only tools. Exposing anything that mutates state (writes, deletes, sends) needs much more careful guardrails than a tool that just retrieves information.&lt;br&gt;
Takeaway&lt;/p&gt;

&lt;p&gt;MCP is still young, but it's rapidly becoming the default way AI agents interact with real systems — moving past "chat with context pasted in" toward "agent with proper tool access." If you're building internal developer tools or SaaS with a lot of API surface area, it's worth experimenting with even a small, single-tool MCP server to see how it changes your own workflow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>mcp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Deploying a .NET Core API for Free in 2026: Railway vs Render vs Fly.io vs Cloudflare Tunnel</title>
      <dc:creator>Preeti Islur</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:01:35 +0000</pubDate>
      <link>https://dev.to/preeti_islur_08045d6b7089/deploying-a-net-core-api-for-free-in-2026-railway-vs-render-vs-flyio-vs-cloudflare-tunnel-1lhk</link>
      <guid>https://dev.to/preeti_islur_08045d6b7089/deploying-a-net-core-api-for-free-in-2026-railway-vs-render-vs-flyio-vs-cloudflare-tunnel-1lhk</guid>
      <description>&lt;p&gt;Why this comparison&lt;/p&gt;

&lt;p&gt;When I started building Sa, a car comparison platform for the Indian market, I had a working .NET Core Web API with MongoDB running locally — Makes and Models endpoints, Swagger docs, all good. Then came the question every side-project developer eventually hits: where do I deploy this for free?&lt;/p&gt;

&lt;p&gt;Azure and AWS have free tiers, but they come with credit card requirements, complex IAM setup, and cold-start behavior that isn't beginner-friendly for a side project you're iterating on daily. So I evaluated four alternatives that are actually built for indie developers.&lt;/p&gt;

&lt;p&gt;The options&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Railway&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Railway offers a straightforward deploy-from-GitHub experience with a genuinely free starter tier. For a .NET Core API + MongoDB setup, it handles builds via Nixpacks or a Dockerfile without much configuration.&lt;/p&gt;

&lt;p&gt;Pros: Fast setup, clean UI, good logs, straightforward environment variable management. Cons: Free tier has usage limits (hours/month) that can run out faster than expected if your API stays warm.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Render's free web service tier is popular for small APIs. It supports Docker deployments directly, which works well for a .NET Core app with a custom Dockerfile.&lt;/p&gt;

&lt;p&gt;Pros: Reliable, simple YAML-based config, good docs for containerized .NET apps. Cons: Free tier services spin down after inactivity — the first request after idle can take 30-60 seconds, which matters if you're demoing the API live.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fly.io&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fly.io deploys apps as lightweight VMs close to users, which appealed to me for eventually serving India-based traffic with lower latency.&lt;/p&gt;

&lt;p&gt;Pros: Good performance once warm, supports persistent volumes, CLI-first workflow that's scriptable. Cons: Slightly steeper learning curve — you're working with fly.toml and flyctl rather than a pure dashboard experience. Free allowances have also gotten stricter over time, so double-check current limits before committing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cloudflare Tunnel&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a different approach entirely — instead of hosting the API on a cloud platform, Cloudflare Tunnel exposes an API running on your own machine or a home server to the internet securely, without opening ports.&lt;/p&gt;

&lt;p&gt;Pros: Free, no cold starts if your machine is always on, full control over the runtime environment. Cons: Only as reliable as your own uptime — not suitable for anything beyond early development/demo purposes.&lt;/p&gt;

&lt;p&gt;What I chose and why&lt;/p&gt;

&lt;p&gt;For active development with a MongoDB Atlas backend, the tradeoff came down to cold-start tolerance vs. setup complexity. If you're building an API that needs to feel responsive during demos or early user testing, spin-down delays on free tiers are the biggest hidden cost — worth testing with a simple health-check endpoint before committing to a platform.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;There's no universally "best" free option — it depends on whether you prioritize zero cold-starts, ease of setup, or long-term scalability path. For a project still in active development, I'd recommend starting with whichever platform gets you deployed fastest, then re-evaluating once you have real usage patterns to optimize against.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>mongodb</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
