How to manage client connections in a Blazor Server + ASP.NET Core API + SignalR project

Happy Development 20 Reputation points
2023-02-13T15:09:21.07+00:00

I am working on a Blazor Server app over a SignalR connection with an ASP.NET Core API to send real-time updates from the server to clients. However, I am having a problem with managing user connections.

The uniqueness of the problem is that each tab opened in a browser by a user represents an individual connection on the SignalR server. This becomes a problem when a user has multiple tabs open with the application and each of them is generating a unique connection. This is because each tab is considered a unique session and therefore the SignalR server creates a new connection for each tab. For example:

If a user "User1" opens 3 tabs in their browser, 3 individual connections will be created for User1 on the server. If another user "User2" opens 2 tabs in their browser, 2 more connections will be created for User2 on the server. And if I'm not logged in, and I open 3 tabs, it will create 3 more connections on the server. The desired environment, regardless of the number of tabs open, instead of duplicating connections:

User1 = 1 connection. User2 = 1 connection. Not logged = 1 connection. My question is how can I effectively manage user connections so that there is only one connection per user/client/session instead of as many as opened tabs. Has anyone had a similar problem and knows how to solve it? I'm sorry if there is an usual easy know fix for this, but I'm looking around and I can't find something that fits exactly my behaviour; and I need some orientation on here instead of copy-paste some code, since conections managment it's a core feature and I'm not much familiar with these.

To clarifly some solutions I've tried are:

Sol. A) In the client: AddSingleton instead of AddScoped

Sol. B) In the client: Set the ConnectionId after hubConn.StartAsync()

Sol. B) In the server: Clients.Client(Context.ConnectionId).SendAsync() instead of Clients.All.SendAsync()

And to mention I didn't used services.AddHttpClient() w/ IClientFactory, but I dont know if it's needed at all or if it's involved in the problem.

Thank you for your time and help!

I provide code used in the connections below:

ASP.NET Core API - SERVER:

Program.cs

using ChartServer.DataProvider; using ChartServer.RHub; using SharedModels;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

// Add CORS Policy

builder.Services.AddCors(option => { option.AddPolicy("cors", policy => { policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyHeader(); }); }); builder.Services.AddSignalR(); // Register the Watcher builder.Services.AddScoped<TimeWatcher>();

builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }

app.UseHttpsRedirection(); app.UseCors("cors");

app.UseAuthorization();

app.MapControllers();

// Add the SignalR Hub
app.MapHub<MarketHub>("/marketdata");

app.Run(); MarketController.cs

namespace ChartServer.Controllers { [Route("api/[controller]")] [ApiController] public class MarketController : ControllerBase { private IHubContext<MarketHub> marketHub; private TimeWatcher watcher;

    public MarketController(IHubContext<MarketHub> mktHub, TimeWatcher watch)
    {
        marketHub = mktHub;
        watcher = watch;
    }
    [HttpGet]
    public IActionResult Get()
    {
        if(!watcher.IsWatcherStarted)
        {
            watcher.Watcher(()=>marketHub.Clients.All.SendAsync("SendMarketStatusData",MarketDataProvider.GetMarketData()));

        }

        return Ok(new { Message = "Request Completed" });
    }
}

}
MarketHub.cs

namespace ChartServer.RHub { public class MarketHub : Hub { public async Task AcceptData(List<Market> data) => await Clients.All.SendAsync("CommunicateMarketData", data);
}

} TimeWatcher.cs

namespace ChartServer.DataProvider { /// <summary> /// This call will be used to send the data after each second to the client /// </summary> public class TimeWatcher { private Action? Executor; private Timer? timer; // we need to auto-reset the event before the execution private AutoResetEvent? autoResetEvent;

    public DateTime WatcherStarted { get; set; }

    public bool IsWatcherStarted { get; set; }

    /// <summary>
    /// Method for the Timer Watcher
    /// This will be invoked when the Controller receives the request
    /// </summary>
    public void Watcher(Action execute)
    {
        int callBackDelayBeforeInvokeCallback = 1000;
        int timeIntervalBetweenInvokeCallback = 2000;
        Executor = execute;
        autoResetEvent = new AutoResetEvent(false);
        timer = new Timer((object? obj) => {
            Executor();
        }, autoResetEvent, callBackDelayBeforeInvokeCallback, timeIntervalBetweenInvokeCallback);

        WatcherStarted = DateTime.Now;
        IsWatcherStarted = true;
    }
}

} Blazor Server app - CLIENT:

Program.cs

using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using SignalsServer.Data; using SignalsServer.HttpCaller;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddSingleton<WeatherForecastService>();

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://localhost:7084/") }); builder.Services.AddScoped<MarketDataCaller>();

var app = builder.Build();

// Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); }

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub(); app.MapFallbackToPage("/_Host");

app.Run(); MarketDataCaller.cs

namespace SignalsServer.HttpCaller { public class MarketDataCaller { private HttpClient httpClient;

    public MarketDataCaller(HttpClient http)
    {
        httpClient = http;
    }

    public async Task GetMarketDataAsync()
    {
        try
        {
            var response = await httpClient.GetAsync("marketdata");

            if (!response.IsSuccessStatusCode)
                throw new Exception("Something is wrong with the connection make sure that the server is running.");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            throw ex;
        }
    }

    public async Task GetMarketEndpoint()
    {
        try
        {
            var response = await httpClient.GetAsync("https://localhost:7193/api/Market");
            if (!response.IsSuccessStatusCode)
                throw new Exception("Something is wrong with the connection so get call is not executing.");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            throw ex;
        }
    }

}

} ChartComponent.razor

@page "/chartui" @using Microsoft.AspNetCore.SignalR.Client; @using SharedModels @using System.Text.Json @inject IJSRuntime js @inject SignalsServer.HttpCaller.MarketDataCaller service; <h3>Chart Component</h3>

<div> <div class="container"> <table class="table table-bordered table-striped"> <tbody> <tr> <td> <button class="btn btn-success" @onclick="@generateLineChartTask">Line Chart</button> </td> <td> <button class="btn btn-danger" @onclick="@generateBarChartTask">Bar Chart</button> </td> </tr> </tbody> </table> <div id="market"></div> <table class="table table-bordered table-striped"> <thead> <tr> <th>Company Name</th> <th>Volume</th> </tr> </thead> <tbody> @foreach (var item in MarketData) { <tr> <td>@item.CompanyName</td> <td>@item.Volume</td> </tr> } </tbody> </table> <hr/> <div class="container"> @ConnectionStatusMessage </div> </div> </div>

@code { private HubConnection? hubConn; private string? ConnectionStatusMessage; public List<Market> MarketData = new List<Market>(); public List<Market> MarketReceivedData = new List<Market>();

private  List<string> xSource;
private  List<int> ySource;
private List<object> source; 

protected override async Task OnInitializedAsync()
{

    xSource = new List<string>();
    ySource = new List<int>();
    source = new List<object>();  

    await service.GetMarketEndpoint();

    hubConn = new HubConnectionBuilder().WithUrl("https://localhost:7193/marketdata").Build();
    await hubConn.StartAsync();

    if(hubConn.State == HubConnectionState.Connected )
        ConnectionStatusMessage = "Connection is established Successfully...";
    else
        ConnectionStatusMessage = "Connection is not established...";
}

private int contador = 0;
private void MarketDataListener(string chartType)
{
    hubConn.On<List<Market>>("SendMarketStatusData", async (data) =>
    {
        MarketData = new List<Market>(); 
        foreach (var item in data)
        {
            Console.WriteLine($"Company Name: {item.CompanyName}, Volumn: {item.Volume}");
            xSource.Add(item.CompanyName);
            ySource.Add(item.Volume);
        }


        source.Add(ySource);
        source.Add(xSource);


        MarketData = data;

        contador++;
        Console.WriteLine($"CONTADOR: {contador}");
        InvokeAsync(StateHasChanged);
        await js.InvokeAsync<object>(chartType, source.ToArray());
        xSource.Clear();
        ySource.Clear();
    });
}

private void ReceivedMarketDataListener()
{
    hubConn.On<List<Market>>("CommunicateMarketData", (data) =>
    {
        MarketReceivedData = data;
        InvokeAsync(StateHasChanged);


    });
}



public async Task Dispose()
{
    await hubConn.DisposeAsync();
}


async Task  generateLineChartTask()
{
    MarketDataListener("marketLineChart");
     ReceivedMarketDataListener();
    await service.GetMarketDataAsync();
}
 async Task   generateBarChartTask()
{
  MarketDataListener("marketBarChart"); 
   ReceivedMarketDataListener();
    await service.GetMarketDataAsync();
}

} FULL CODE: https://github.com/maheshsabnis/SignalRChartBlazor

Developer technologies | ASP.NET | ASP.NET Core
Developer technologies | .NET | Blazor
Developer technologies | ASP.NET | ASP.NET API
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Bruce (SqlWork.com) 77,926 Reputation points Volunteer Moderator
    2023-02-20T17:17:46.7266667+00:00

    the issue is that each instance of blazor creates a unique signal/r connection to the blazor server and creates a unique server instance. you then have this server blazor instance create a new signal/r connection to the other hub.

    to limit the connection from blazor server to second hub, you will need to share the signal/r connection. you could create a background thread, that created a connection per user, and use a custom queuing system to deliver messages from the the background thread to the blazer app instance threads.

    note: browsers are pretty aggressive about shutting down web socket connections in non displaying tabs. generally this will shutdown the blazor server app anyway. so the issue may not be a big as you expect.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.