Here I am trying to remove ConnectionId from connected connectedId list UserHandler.ConnectedUser in OnDisconnectedAsync method of my hub.
But the problem is that when ever user gets logout from application or close that window the OnDisconnectedAsync method on hub does not get hit.
Any help with my code will be grate. Thank you.
Below is my hub
public class ChatHub : Hub
{
public List<string> SendingUserList = new List<string>();
public async Task SendMessage(string to, string message)
{
var senderConnectionId = Context.ConnectionId;
SendingUserList.Add(to);
SendingUserList.Add(senderConnectionId);
foreach (var connectionId in SendingUserList)
{
await Clients.Client(connectionId).SendAsync("ReceiveMessage", message);
}
}
public override Task OnConnectedAsync()
{
Clients.All.SendAsync("ReciveUser", UserHandler.UserName, Context.ConnectionId);
return base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception e)
{
UserHandler.ConnectedUser.Remove(Context.ConnectionId));
await base.OnDisconnectedAsync(e);
}
}
public static class UserHandler
{
public static HashSet<string> ConnectedUser = new HashSet<string>();
public static string UserName { get; set; }
}
Below is my remaining code
protected override async Task OnInitializedAsync()
{
UserHandler.UserName = httpContextAccessor.HttpContext.User.Identity.Name;
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.ServerTimeout = TimeSpan.FromMinutes(60);
hubConnection.On<string>("ReceiveMessage", BroadcastMessage);
hubConnection.On<string,string>("ReciveUser", RefreshUserList);
await hubConnection.StartAsync();
StateHasChanged();
}
private void RefreshUserList(string connectedUserId, string connectionId )
{
UserHandler.ConnectedUser.Add(connectionId);
connectedUserList = UserHandler.ConnectedUser;
StateHasChanged();
}
private void BroadcastMessage(string message)
{
var encodedMsg = $"{message}";
messages.Add(encodedMsg);
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
await hubConnection.DisposeAsync();
}
You'll need to implement the IAsyncDisposable interface in the Hub's consumer (Razor Component), like this:
#implements IAsyncDisposable
Add these methods:
public void Dispose()
{
hubConnection.DisposeAsync();
}
public async ValueTask DisposeAsync()
{
await hubConnection.DisposeAsync();
}
Now, when you close the client's page the OnDisconnectedAsync method will be called.
Related
I have a web API project which is configured to connect to Azure SignalR and clients can connect to it through its hub.
Web Api:
[Authorize]
public class ClientHub : Hub
{
private string GroupName
{
get
{
return $"{Context?.User?.GetClaim(ClaimTypes.Role) ?? null}_{ Context?.User?.GetClaim(ClaimType.hdid) ?? null}";
}
}
public ClientHub()
{
}
public async override Task OnConnectedAsync()
{
Console.WriteLine("OnConnectedAsync");
await Groups.AddToGroupAsync(Context?.ConnectionId, GroupName);
await base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Console.WriteLine("OnDisconnectedAsync");
return base.OnDisconnectedAsync(exception);
}
}
public class SignalRService : IDataPushService
{
private readonly IHubContext<ClientHub> _hubContext;
public SignalRService(IHubContext<ClientHub> hubContext)
{
_hubContext = hubContext;
}
public Task Receive(string data)
{
return Task.FromResult(0);
}
public async Task Send(PushServiceDataPackage dataPackage)
{
await _hubContext.Clients.Group(dataPackage.GroupName)
.SendAsync(dataPackage.EventType.ToString(), dataPackage.Data);
}
}
ConfigureServices method
// Add SignalR service
bool.TryParse(configuration["SignalRSettings:EnableDetailedErrors"], out bool enableDetailedErrors);
services.AddSignalR(config => config.EnableDetailedErrors = enableDetailedErrors).AddAzureSignalR(configuration["SignalRSettings:ConnectionString"]);
Configure method:
endpoints.MapHub<ClientHub>(configuration["SignalRSettings:HubPath"]);
Console App:
I also have a console app that is meant to use the same SignalR server to send data to connected clients.
bool.TryParse(hostContext.Configuration["SignalRSettings:EnableDetailedErrors"], out bool enableDetailedErrors);
services.AddSignalR(config => config.EnableDetailedErrors = enableDetailedErrors).AddAzureSignalR(hostContext.Configuration["SignalRSettings:ConnectionString"]);
public class SignalRService : IDataPushService
{
private readonly IHubContext<Hub> _hubContext;
public SignalRService(IHubContext<Hub> hubContext)
{
_hubContext = hubContext;
}
public Task Receive(string data)
{
return Task.FromResult(0);
}
public async Task Send(PushServiceDataPackage dataPackage)
{
await _hubContext.Clients.Group(dataPackage.GroupName)
.SendAsync(dataPackage.EventType.ToString(), dataPackage.Data);
}
}
How can I access the clients connected to the server in the console app?
Please note that there are two apps and one Azure SignalR server?
I create the BackgroundService:
public class CustomService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
//...
}
}
and I added to the project:
public class Startup
{
//...
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<CustomService>();
//...
}
//...
}
How can I find the CustomService from another class?
How to start it again?
Create an interface just for the call to StartAsync:
public interface ICustomServiceStarter
{
Task StartAsync(CancellationToken token = default);
}
public class CustomService : BackgroundService, ICustomServiceStarter
{
//...
Task ICustomServiceStarter.StartAsync(CancellationToken token = default) => base.StartAsync(token);
//...
}
Register the interface as a singleton:
public class Startup
{
//...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddSingleton<ICustomServiceStarter, CustomService>();
}
//...
}
and inject ICustomServiceStarter when needed:
public class MyServiceControllerr : Controller
{
ICustomServiceStarter _starter;
public MyServiceController(ICustomServiceStarter starter)
{
_starter = starter;
}
[HttpPost]
public async Task<IActionResult> Start()
{
await _starter.StartAsync();
return Ok();
}
}
When it comes to controller's action, using "await BackgroundService.StartAsync" is the wrong way for long-running tasks.
For instance, the main ussue could be request's timeout depended on proxy settings.
Here is an example how to make your BackgroundService restartable.
BackgroundService implementation:
public class MyBackgroundService: BackgroundService
{
private volatile bool _isFinished = false;
private SemaphoreSlim _semaphore = new SemaphoreSlim(0,1);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_isFinished = false;
// DoSomeWork
_isFinished = true;
await WaitToRestartTask(stoppingToken);
}
private async Task WaitToRestartTask(CancellationToken stoppingToken)
{
// wait until _semaphore.Release()
await _semaphore.WaitAsync(stoppingToken);
// run again
await base.StartAsync(stoppingToken);
}
public void RestartTask()
{
if (!_isFinished)
throw new InvalidOperationException("Background service is still working");
// enter from _semaphore.WaitAsync
_semaphore.Release();
}
}
Controller's action (for instance):
public async Task<IActionResult> Restart()
{
var myBackgroundService= _serviceProvider.GetServices<IHostedService>()
.OfType<MyBackgroundService>()
.First();
try
{
myBackgroundService.RestartTask();
return Ok($"MyBackgroundService was restarted");
}
catch (InvalidOperationException exception)
{
return BadRequest(exception.Message);
}
}
I am trying to refresh the connected ConnectionId whenever a new connection is made for all the connected connections in blazor server using signalr.
But the problem is that whenever the second or later connection is made the the same ConnectionId is added twice in the list.
Any help with code will be grate. Thank you
Below is my hub
public class ChatHub : Hub
{
public Task SendMessage(string ConnectionId, string message)
{
return Clients.Client(ConnectionId).SendAsync("ReceiveMessage", message);
}
public override Task OnConnectedAsync()
{
Clients.All.SendAsync("ReceiveUser", Context.ConnectionId);
return base.OnConnectedAsync();
}
}
public static class UserHandler
{
public static List<string> ConnectedUsers = new List<string>();
}
Below is my code to refresh connected ConnectionId
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string>("ReceiveMessage", broad);
hubConnection.On<string>("ReceiveUser", RefreshUserList);
await hubConnection.StartAsync();
StateHasChanged();
}
private void RefreshUserList(string connectedUserId) // Double hit in this method as a result same connectionId is added twice in the list
{
UserHandler.ConnectedUsers.Add(connectedUserId);
connectedUserList = UserHandler.ConnectedUsers;
StateHasChanged();
}
I have implemented a REST API Server with asp.NET Core. I need a background task which does some administrative stuff.
I tried the several suggestions in the web to create a timed background task and implemented it as fpllowing:
BackgroundService.cs
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_executingTask = ExecuteAsync(_stoppingCts.Token);
if (_executingTask.IsCompleted)
{
return _executingTask;
}
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executingTask == null)
{
return;
}
try
{
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}
RecureHostedService.cs
public class RecureHostedService : BackgroundService
{
private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private int _executionCount = 0;
private Timer _timer;
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var count = Interlocked.Increment(ref _executionCount);
log.InfoFormat(
"Timed Hosted Service is working. Count: {0}", count);
await Task.Delay(10000, stoppingToken);
}
}
}
SystemController.cs
public class SystemController : Controller
{
private readonly RecureHostedService _recureHostedService;
public SystemController(IHostedService hostedService)
{
_recureHostedService = hostedService as RecureHostedService;
}
[HttpGet(ApiRoutes.System.Start)]
public IActionResult Start()
{
Console.WriteLine("Start Service");
_recureHostedService.StartAsync(new CancellationToken());
return Ok();
}
[HttpGet(ApiRoutes.System.Stop)]
public IActionResult Stop()
{
Console.WriteLine("Stop Service");
Console.WriteLine(_recureHostedService == null);
_recureHostedService.StopAsync(new CancellationToken());
return Ok();
}
}
in Startup.cs I added the Service as Singleton:
services.AddSingleton<IHostedService,RecureHostedService>();
The RecureHostedService starts on startup of my app and does its work. When I do an APIrequest /api/v1/stop from the SystemController (method Stop()) I got an NullReferenceException in
_recureHostedService.StopAsync(new CancellationToken());
I found out that the private member variable _recureHostedService is null even if I had assigned it in the Constructor of the SystemController. So it seems that the Service Injection of _recureHostedService is not working. So the line
_recureHostedService = hostedService as RecureHostedService;
in the Constructor of SystemController assigns null to _recureHostedService.
How can that be? I've a lot of other Services injected to other Controllers and all works fine.
Any idea from anyone, why it is not working with that Hosted Service?
Best
Mic
I used a set of static variables for myself
public class RecureHostedService : BackgroundService {
public static bool isPause = false;
/* for example
public static string ErrorText;
public static bool isError = false;
public static bool isWorked = false;
public static bool firstStart = true; */
protected async override Task ExecuteAsync(CancellationToken stoppingToken){
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
if (isPause)
continue;
var count = Interlocked.Increment(ref _executionCount);
log.InfoFormat(
"Timed Hosted Service is working. Count: {0}", count);
await Task.Delay(10000, stoppingToken);
}
}
}
public class SystemController : Controller
{
[HttpGet(ApiRoutes.System.Start)]
public IActionResult Start()
{
Console.WriteLine("Start Service");
RecureHostedService.isPause = false;
return Ok();
}
[HttpGet(ApiRoutes.System.Stop)]
public IActionResult Stop()
{
Console.WriteLine("Stop Service");
RecureHostedService.isPause = true;
return Ok();
}
}
You need to register you hosted service using AddHostedService instedad of AddSingleton
so it will be something like this
services.AddHostedService<IHostedService,RecureHostedService>();
If you perfer inject IHostedService on constructor, keep _recureHostedService property declared as type of IHostedService, instead of the concrete class RecureHostedService. It's not necessary down-casting here.
[Route("[controller]")]
public class SystemController : Controller
{
private readonly IHostedService _recureHostedService;
public SystemController(IHostedService hostedService)
{
_recureHostedService = hostedService ?? throw new ArgumentNullException(nameof(hostedService));
}
}
Update
I've tested this controller, no null exception any longer in either this way
services.AddSingleton<IHostedService, RecureHostedService>();
or
services.AddHostedService(p =>
{
return new RecureHostedService();
});
Using dependency injection to obtain an IHostedService will inject whichever hosted service was defined first. Which is likely to be the web server itself. Instead you should define two service definitions that return the same singleton;
services.AddSingleton<RecureHostedService>();
services.AddSingleton<IHostedService>(p => p.GetRequiredService<RecureHostedService>());
public class SystemController : Controller
{
private readonly RecureHostedService _recureHostedService;
public SystemController(RecureHostedService hostedService)
{
_recureHostedService = hostedService;
}
}
I have a Hub inside my ASP.NET Stateful project hosted on a local Service Fabric cluster, and I've overridden the OnConnected method. When putting a breakpoint on the OnConnected method, it's not entered, but at the same time, messages are properly broadcasted through the Hub.
Hub class:
public class NotificationHub : BaseHub
{
private IFragStateManager _stateManager;
private static IHubContext _context;
public NotificationHub(IFragStateManager stateManager) : this(stateManager, _context)
{
_stateManager = stateManager;
_context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
}
private NotificationHub(IFragStateManager stateManager, IHubContext context) : base(stateManager, context)
{ }
/// <summary>
/// Broadcasts payload to all clients
/// </summary>
/// <param name="payload"></param>
public void Broadcast(object payload)
{
try
{
_context.Clients.All.broadcast(payload);
}
catch (Exception e)
{
Log.Error(e.ToString());
}
}
//Investigate more why it's not hitting breakpoint while it actually connects and broadcasts messages
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
return base.OnDisconnected(stopCalled);
}
}
_context.Clients.All.broadcast(payload); works well. It delivers the payload successfully
Hub's base class:
public abstract class BaseHub : Hub
{
protected IPrincipal User {
get { return base.Context.User ?? _user; }
}
private ClaimsPrincipal _user;
private Guid _userId;
private IFragStateManager _stateManager;
private IHubContext _context;
public BaseHub(IFragStateManager stateManager, IHubContext context)
{
_stateManager = stateManager;
_context = context;
}
public override async Task OnConnected()
{
GetUserId();
var map = await _stateManager.GetOrAddAsync<FragDictionary<Guid, string>>("UserConnectionMap");
using (var tx = _stateManager.CreateTransaction())
{
var exisingValue = await map.TryGetValueAsync(tx, _userId);
await map.SetAsync(tx, _userId, Context.ConnectionId);
await tx.CommitAsync();
}
await base.OnConnected();
}
public override async Task OnDisconnected(bool stopCalled)
{
var map = await _stateManager.GetOrAddAsync<FragDictionary<Guid, string>>("UserConnectionMap");
using (var tx = _stateManager.CreateTransaction())
{
await map.TryRemoveAsync(tx, _userId);
await tx.CommitAsync();
}
await base.OnDisconnected(stopCalled);
}
private void GetUserId()
{
if (Context.User == null)
_user = (ClaimsPrincipal)(IPrincipal)Context.Request.Environment["server.User"];
else
_userId = new Guid(_user.FindFirst("AccountId").Value);
}
}
Hub client:
$.connection.hub.logging = true;
$.connection.hub.url = 'http://localhost:8298/api/realtime/70c604f8-7996-4107-9ec3-28d08c869899/131810464642593740/4b98412e-8f09-4460-9c90-1d155cb8efca/signalr';
var hub = $.connection.notificationHub;
$.extend(hub.client, {
broadcast: function (msg) {console.log(msg)}
});
$.connection.hub.start().done(function() {
console.log('Connected');
});
Client's scripts:
<script src="Scripts/jquery-1.6.4.js"></script>
<script src="Scripts/jquery.signalR-2.1.0.js"></script>
<script src="http://localhost:8298/api/realtime/70c604f8-7996-4107-9ec3-28d08c869899/131810464642593740/4b98412e-8f09-4460-9c90-1d155cb8efca/signalr/hubs"></script>
What has been checked already:
Downgraded SignalR. Went from 2.3 to 2.1. No results.
js client is already subscribing so probably no issues here.
When Hub is poked from the backend code, it broadcasts messages
properly.