title: What Is The Correct Way of Closing / Disconnecting An ASP.NET Core signalR Client Connection?
body: <p>I am a new user struggling to close a secondary signalR client gracefully from an ASP.NET Core Blazor Server Page.</p>
  <p>I am setting up a secondary signalR client connection on first render of a Blazor Server Page. I am trying to close this secondary signalR client connection when the Page is closed via the browser tab.</p>
  <p>At the time of writing <code>DisposeAsync</code> does not seem to be triggered when the page is closed via the browser tab. However, the <code>Dispose</code> method <em>IS</em> triggered. Furthermore in Safari 13.0.5 the <code>Dispose</code> method is not triggered when the browser tab is closed? Opera, Firefox and Chrome all have <code>Dispose</code> triggered upon closing the browser tab. Fixed this by updating Safari to v14.0 (15610.1.28.9, 15610) via macOS Catalina v10.15.7.</p>
  <p>Currently, I am calling <code>DisposeAsync</code> from <code>Dispose</code> to close the signalR connection. I am closing the client connection using the following code:</p>
  <pre><code>...
  Logger.LogInformation(&quot;Closing secondary signalR connection...&quot;);
  await hubConnection.StopAsync();
  Logger.LogInformation(&quot;Closed secondary signalR connection&quot;);
  ...
  </code></pre>
  <p>The <code>StopAsync</code> method appears to block, i.e. no message is output for <em>&quot;Closed secondary signalR connection&quot;</em>. Although, the <code>OnDisconnectedAsync</code> handler of my server hub displays that the connection has being disconnected. This is similar to the <a href="https://github.com/dotnet/aspnetcore/issues/9893#issuecomment-491998333" rel="noreferrer">behaviour</a> described in this <a href="https://github.com/dotnet/aspnetcore/issues/9893" rel="noreferrer">issue</a>.</p>
  <p>How do I correctly dispose of a signalR connection in <strong>ASP.NET Core 3.1</strong>?</p>
  <p>Full code listings are shown below:</p>
  <p><strong>Disposing Of signalR Connection</strong></p>
  <pre><code> #region Dispose
          public void Dispose()
          {
              Dispose(true);
              GC.SuppressFinalize(this);
          }
  
  
          /// &lt;summary&gt;
          /// Clear secondary signalR Closed event handler and stop the
          /// secondary signalR connection
          /// &lt;/summary&gt;
          /// &lt;remarks&gt;
          /// ASP.NET Core Release Candidate 5 calls DisposeAsync when 
          /// navigating away from a Blazor Server page. Until the 
          /// release is stable DisposeAsync will have to be triggered from
          /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
          /// in Dispose().
          /// However, providing DisposeAsync() now makes the migration easier
          /// https://github.com/dotnet/aspnetcore/issues/26737
          /// https://github.com/dotnet/aspnetcore/issues/9960
          /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
          /// &lt;/remarks&gt;
          protected virtual void Dispose(bool disposing)
          {
              if (disposed)
                  return;
  
              if (disposing)
              {
                  Logger.LogInformation(&quot;Index.razor page is disposing...&quot;);
  
                  try
                  {
                      if (hubConnection != null)
                      {
                          Logger.LogInformation(&quot;Removing signalR client event handlers...&quot;);
                          hubConnection.Closed -= CloseHandler;
                      }
  
                      // Until ASP.NET Core 5 is released in November
                      // trigger DisposeAsync(). See docstring and DiposeAsync() below.
                      // not ideal, but having to use GetAwaiter().GetResult() until
                      // forthcoming release of ASP.NET Core 5 for the introduction
                      // of triggering DisposeAsync on pages that implement IAsyncDisposable
                      DisposeAsync().GetAwaiter().GetResult();
                  }
                  catch (Exception exception)
                  {
                      Logger.LogError($&quot;Exception encountered while disposing Index.razor page :: {exception.Message}&quot;);
                  }
              }
  
              disposed = true;
          }
  
  
          /// &lt;summary&gt;
          /// Dispose the secondary backend signalR connection
          /// &lt;/summary&gt;
          /// &lt;remarks&gt;
          /// ASP.NET Core Release Candidate 5 adds DisposeAsync when 
          /// navigating away from a Blazor Server page. Until the 
          /// release is stable DisposeAsync will have to be triggered from
          /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
          /// in Dispose().
          /// However, providing DisposeAsync() now makes the migration easier
          /// https://github.com/dotnet/aspnetcore/issues/26737
          /// https://github.com/dotnet/aspnetcore/issues/9960
          /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
          /// &lt;/remarks&gt;
          public async virtual ValueTask DisposeAsync()
          {
              try
              {
                  if (hubConnection != null)
                  {
                      Logger.LogInformation(&quot;Closing secondary signalR connection...&quot;);
                      await hubConnection.StopAsync();
                      Logger.LogInformation(&quot;Closed secondary signalR connection&quot;);
                  }
                  // Dispose(); When migrated to ASP.NET Core 5 let DisposeAsync trigger Dispose
              }
              catch (Exception exception)
              {
                  Logger.LogInformation($&quot;Exception encountered wwhile stopping secondary signalR connection :: {exception.Message}&quot;);
              }
          }
          #endregion
  </code></pre>
  <p><strong>Full Code For Blazor Server Page</strong></p>
  <pre><code>using System;
  using System.Threading.Tasks;
  
  using Microsoft.AspNetCore.Components;
  using Microsoft.AspNetCore.SignalR.Client;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Logging;
  
  using WebApp.Data;
  using WebApp.Data.Serializers.Converters;
  using WebApp.Data.Serializers.Converters.Visitors;
  using WebApp.Repository.Contracts;
  
  
  
  namespace WebApp.Pages
  {
      public partial class Index : IAsyncDisposable, IDisposable
      {
          private HubConnection hubConnection;
          public bool IsConnected =&gt; hubConnection.State == HubConnectionState.Connected;
          private bool disposed = false;
  
  
          [Inject]
          public NavigationManager NavigationManager { get; set; }
          [Inject]
          public IMotionDetectionRepository Repository { get; set; }
          [Inject]
          public ILogger&lt;MotionDetectionConverter&gt; LoggerMotionDetection { get; set; }
          [Inject]
          public ILogger&lt;MotionInfoConverter&gt; LoggerMotionInfo { get; set; }
          [Inject]
          public ILogger&lt;JsonVisitor&gt; LoggerJsonVisitor { get; set; }
          [Inject]
          public ILogger&lt;Index&gt; Logger { get; set; }
  
  
          #region Dispose
          public void Dispose()
          {
              Dispose(true);
              GC.SuppressFinalize(this);
          }
  
  
          /// &lt;summary&gt;
          /// Clear secondary signalR Closed event handler and stop the
          /// secondary signalR connection
          /// &lt;/summary&gt;
          /// &lt;remarks&gt;
          /// ASP.NET Core Release Candidate 5 calls DisposeAsync when 
          /// navigating away from a Blazor Server page. Until the 
          /// release is stable DisposeAsync will have to be triggered from
          /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
          /// in Dispose().
          /// However, providing DisposeAsync() now makes the migration easier
          /// https://github.com/dotnet/aspnetcore/issues/26737
          /// https://github.com/dotnet/aspnetcore/issues/9960
          /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
          /// &lt;/remarks&gt;
          protected virtual void Dispose(bool disposing)
          {
              if (disposed)
                  return;
  
              if (disposing)
              {
                  Logger.LogInformation(&quot;Index.razor page is disposing...&quot;);
  
                  try
                  {
                      if (hubConnection != null)
                      {
                          Logger.LogInformation(&quot;Removing signalR client event handlers...&quot;);
                          hubConnection.Closed -= CloseHandler;
                      }
  
                      // Until ASP.NET Core 5 is released in November
                      // trigger DisposeAsync(). See docstring and DiposeAsync() below.
                      // not ideal, but having to use GetAwaiter().GetResult() until
                      // forthcoming release of ASP.NET Core 5 for the introduction
                      // of triggering DisposeAsync on pages that implement IAsyncDisposable
                      DisposeAsync().GetAwaiter().GetResult();
                  }
                  catch (Exception exception)
                  {
                      Logger.LogError($&quot;Exception encountered while disposing Index.razor page :: {exception.Message}&quot;);
                  }
              }
  
              disposed = true;
          }
  
  
          /// &lt;summary&gt;
          /// Dispose the secondary backend signalR connection
          /// &lt;/summary&gt;
          /// &lt;remarks&gt;
          /// ASP.NET Core Release Candidate 5 adds DisposeAsync when 
          /// navigating away from a Blazor Server page. Until the 
          /// release is stable DisposeAsync will have to be triggered from
          /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
          /// in Dispose().
          /// However, providing DisposeAsync() now makes the migration easier
          /// https://github.com/dotnet/aspnetcore/issues/26737
          /// https://github.com/dotnet/aspnetcore/issues/9960
          /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
          /// &lt;/remarks&gt;
          public async virtual ValueTask DisposeAsync()
          {
              try
              {
                  if (hubConnection != null)
                  {
                      Logger.LogInformation(&quot;Closing secondary signalR connection...&quot;);
                      await hubConnection.StopAsync();
                      Logger.LogInformation(&quot;Closed secondary signalR connection&quot;);
                  }
                  // Dispose(); When migrated to ASP.NET Core 5 let DisposeAsync trigger Dispose
              }
              catch (Exception exception)
              {
                  Logger.LogInformation($&quot;Exception encountered wwhile stopping secondary signalR connection :: {exception.Message}&quot;);
              }
          }
          #endregion
  
  
          #region ComponentBase
  
          /// &lt;summary&gt;
          /// Connect to the secondary signalR hub after rendering.
          /// Perform on the first render. 
          /// &lt;/summary&gt;
          /// &lt;remarks&gt;
          /// This could have been performed in OnInitializedAsync but
          /// that method gets executed twice when server prerendering is used.
          /// &lt;/remarks&gt;
          protected override async Task OnAfterRenderAsync(bool firstRender)
          {
              if (firstRender)
              {
                  var hubUrl = NavigationManager.BaseUri.TrimEnd('/') + &quot;/motionhub&quot;;
  
                  try
                  {
                      Logger.LogInformation(&quot;Index.razor page is performing initial render, connecting to secondary signalR hub&quot;);
  
                      hubConnection = new HubConnectionBuilder()
                          .WithUrl(hubUrl)
                          .ConfigureLogging(logging =&gt;
                          {
                              logging.AddConsole();
                              logging.AddFilter(&quot;Microsoft.AspNetCore.SignalR&quot;, LogLevel.Information);
                          })
                          .AddJsonProtocol(options =&gt;
                          {
                              options.PayloadSerializerOptions = JsonConvertersFactory.CreateDefaultJsonConverters(LoggerMotionDetection, LoggerMotionInfo, LoggerJsonVisitor);
                          })
                          .Build();
  
                      hubConnection.On&lt;MotionDetection&gt;(&quot;ReceiveMotionDetection&quot;, ReceiveMessage);
                      hubConnection.Closed += CloseHandler;
  
                      Logger.LogInformation(&quot;Starting HubConnection&quot;);
  
                      await hubConnection.StartAsync();
  
                      Logger.LogInformation(&quot;Index Razor Page initialised, listening on signalR hub url =&gt; &quot; + hubUrl.ToString());
                  }
                  catch (Exception e)
                  {
                      Logger.LogError(e, &quot;Encountered exception =&gt; &quot; + e);
                  }
              }
          }
  
          protected override async Task OnInitializedAsync()
          {
              await Task.CompletedTask;
          }
          #endregion
  
  
          #region signalR
  
          /// &lt;summary&gt;Log signalR connection closing&lt;/summary&gt;
          /// &lt;param name=&quot;exception&quot;&gt;
          /// If an exception occurred while closing then this argument describes the exception
          /// If the signaR connection was closed intentionally by client or server, then this
          /// argument is null
          /// &lt;/param&gt;
          private Task CloseHandler(Exception exception)
          {
              if (exception == null)
              {
                  Logger.LogInformation(&quot;signalR client connection closed&quot;);
              }
              else
              {
                  Logger.LogInformation($&quot;signalR client closed due to error =&gt; {exception.Message}&quot;);
              }
  
              return Task.CompletedTask;
          }
  
          /// &lt;summary&gt;
          /// Add motion detection notification to repository
          /// &lt;/summary&gt;
          /// &lt;param name=&quot;message&quot;&gt;Motion detection received via signalR&lt;/param&gt;
          private void ReceiveMessage(MotionDetection message)
          {
              try
              {
                  Logger.LogInformation(&quot;Motion detection message received&quot;);
  
                  Repository.AddItem(message);
  
                  StateHasChanged();
              }
              catch (Exception ex)
              {
                  Logger.LogError(ex, &quot;An exception was encountered =&gt; &quot; + ex.ToString());
              }
          }
          #endregion
      }
  }
  </code></pre>
  <p><strong>signalR Server Hub</strong></p>
  <pre><code>using System;
  using System.Threading.Tasks;
  
  using Microsoft.AspNetCore.SignalR;
  using Microsoft.Extensions.Logging;
  
  
  namespace WebApp.Realtime.SignalR
  {
      /// &lt;summary&gt;
      /// This represents endpoints available on the server, available for the
      /// clients to call
      /// &lt;/summary&gt;
      public class MotionHub : Hub&lt;IMotion&gt;
      {
          private bool _disposed = false;
          public ILogger&lt;MotionHub&gt; Logger { get; set; }
  
          public MotionHub(ILogger&lt;MotionHub&gt; logger) : base()
          {
              Logger = logger;
          }
  
  
          public override async Task OnConnectedAsync()
          {
              Logger.LogInformation($&quot;OnConnectedAsync =&gt; Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name}&quot;);
              await base.OnConnectedAsync();
          }
  
          public override async Task OnDisconnectedAsync(Exception exception)
          {
              if (exception != null)
              {
                  Logger.LogInformation($&quot;OnDisconnectedAsync =&gt; Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name} : Exception={exception.Message}&quot;);
              }
              else
              {
                  Logger.LogInformation($&quot;OnDisconnectedAsync =&gt; Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name}&quot;);
              }
  
              await base.OnDisconnectedAsync(exception);
          }
  
          // Protected implementation of Dispose pattern.
          protected override void Dispose(bool disposing)
          {
              if (_disposed)
              {
                  return;
              }
  
              _disposed = true;
  
              // Call base class implementation.
              base.Dispose(disposing);
          }
      }
  }
  </code></pre>