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