SignalR Core Hub Interact With BackgroundService .NET Core - .net-core

I have read documentation on how to send notifications from a background service to clients through a signalr core hub. How can I receive notifications from clients to the background service?
Background service should only be a singleton.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<QueueProcessor>();
services.AddSignalR();
}
public void Configure(IApplicationBuilder app)
{
app.UseSignalR(routes =>
{
routes.MapHub<AutoCommitHub>("/autocommithub");
});
}
}
public class QueueProcessor : BackgroundService
{
private int interval;
public QueueProcessor(IHubContext<AutoCommitHub> hubContext)
{
this.hub = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await BeginProcessingOrders();
Thread.Sleep(interval);
}
}
internal async Task BroadcastProcessStarted(string orderNumber)
{
await hub.Clients.All.SendAsync("ReceiveOrderStarted",
orderNumber);
}
internal void SetInterval(int interval)
{
this.interval = interval;
}
}
public class AutoCommitHub : Hub
{
private readonly QueueProcessor queueProcessor;
public AutoCommitHub(QueueProcessor _processor)
{
queueProcessor = _processor;
}
public void SetIntervalSpeed(int interval)
{
queueProcessor.SetInterval(interval);
}
}
I need to be able to call the SetInterval method from a client. Client is connected through the hub. I don't want another instance of the QueueProcessor to be instantiated either.

The way we solved this is adding a third service to the service collection as a singleton.
Here's the full sample PoC: https://github.com/doming-dev/SignalRBackgroundService
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<QueueProcessor>();
services.AddSingleton<HelperService>();
services.AddSignalR();
}
}
HelperService raises events that the background service can latch onto.
public class HelperService : IHelperService
{
public event Action OnConnectedClient = delegate { };
public event Action<int> SpeedChangeRequested = delegate { };
public void OnConnected()
{
OnConnectedClient();
}
public void SetSpeed(int interval)
{
SpeedChangeRequested(interval);
}
}
The hub now when clients send a message can call methods on the HelperService which in turn will raise events that the background service is handling.
public class MyHub : Hub
{
private readonly IHelperService helperService;
public MyHub(IHelperService service)
{
helperService = service;
}
public override async Task OnConnectedAsync()
{
helperService.OnConnected();
await base.OnConnectedAsync();
}
public void SetSpeed(int interval)
{
helperService.SetSpeed(interval);
}
}

You don't need another instance of QueueProcessor. The client can easily call SetIntervalSpeed from his code. Documentation with an example.
var connection = new signalR.HubConnectionBuilder().withUrl("/autocommithub").build();
connection.invoke("SetIntervalSpeed", interval)
SignalR provides an API for creating server-to-client RFC.

Related

Azure Service Bus not all messages received in hosted service web app

Inside a .net web app, I set up a hosted service to receive messages from an Azure Service Bus topic. The problem is that not all messages are received, only an arbitrary amount (e.g. of 20 messages only 12 are received). The rest of them ended up in the dead letter queue. This happens when the messages are send simultaneously.
I tried the following steps to solve this:
Increased the amount of maximum concurrent calls, which helped but didn't provide a guarantee
Added a prefetch count
I also tried to send messages via the functionality in the service bus resource in Azure. 500 messages, no interval time --> didn't work (for all messages). 500 messages, 1s interval time, all messages were received.
I just don't understand why the receiver is not recieving all of the messages.
I want to build a event-driven architecture and cannot make it a gamble if all messages will be processed.
Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IServiceBusTopicSubscription,ServiceBusSubscription>();
services.AddHostedService<WorkerServiceBus>();
}
...
WorkerService.cs
public class WorkerServiceBus : IHostedService, IDisposable
{
private readonly ILogger<WorkerServiceBus> _logger;
private readonly IServiceBusTopicSubscription _serviceBusTopicSubscription;
public WorkerServiceBus(IServiceBusTopicSubscription serviceBusTopicSubscription,
ILogger<WorkerServiceBus> logger)
{
_serviceBusTopicSubscription = serviceBusTopicSubscription;
_logger = logger;
}
public async Task StartAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Starting the service bus queue consumer and the subscription");
await _serviceBusTopicSubscription.PrepareFiltersAndHandleMessages().ConfigureAwait(false);
}
public async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Stopping the service bus queue consumer and the subscription");
await _serviceBusTopicSubscription.CloseSubscriptionAsync().ConfigureAwait(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual async void Dispose(bool disposing)
{
if (disposing)
{
await _serviceBusTopicSubscription.DisposeAsync().ConfigureAwait(false);
}
}
}
ServiceBusSubscription.cs
public class ServiceBusSubscription : IServiceBusTopicSubscription
{
private readonly IConfiguration _configuration;
private const string TOPIC_PATH = "test";
private const string SUBSCRIPTION_NAME = "test-subscriber";
private readonly ILogger _logger;
private readonly ServiceBusClient _client;
private readonly IServiceScopeFactory _scopeFactory;
private ServiceBusProcessor _processor;
public ServiceBusBookingsSubscription(IConfiguration configuration,
ILogger<ServiceBusBookingsSubscription> logger,
IServiceScopeFactory scopeFactory)
{
_configuration = configuration;
_logger = logger;
_scopeFactory = scopeFactory;
var connectionString = _configuration.GetConnectionString("ServiceBus");
var serviceBusOptions = new ServiceBusClientOptions()
{
TransportType = ServiceBusTransportType.AmqpWebSockets
};
_client = new ServiceBusClient(connectionString, serviceBusOptions);
}
public async Task PrepareFiltersAndHandleMessages()
{
ServiceBusProcessorOptions _serviceBusProcessorOptions = new ServiceBusProcessorOptions
{
MaxConcurrentCalls = 200,
AutoCompleteMessages = false,
PrefetchCount = 1000,
};
_processor = _client.CreateProcessor(TOPIC_PATH, SUBSCRIPTION_NAME, _serviceBusProcessorOptions);
_processor.ProcessMessageAsync += ProcessMessagesAsync;
_processor.ProcessErrorAsync += ProcessErrorAsync;
await _processor.StartProcessingAsync().ConfigureAwait(false);
}
private async Task ProcessMessagesAsync(ProcessMessageEventArgs args)
{
_logger.LogInformation($"Received message from service bus");
_logger.LogInformation($"Message: {args.Message.Body}");
var payload = args.Message.Body.ToObjectFromJson<List<SchedulerBookingViewModel>>();
// Create scoped dbcontext
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<dbContext>();
// Process payload
await new TestServiceBus().DoThings(payload);
await args.CompleteMessageAsync(args.Message).ConfigureAwait(false);
}
private Task ProcessErrorAsync(ProcessErrorEventArgs arg)
{
_logger.LogError(arg.Exception, "Message handler encountered an exception");
_logger.LogError($"- ErrorSource: {arg.ErrorSource}");
_logger.LogError($"- Entity Path: {arg.EntityPath}");
_logger.LogError($"- FullyQualifiedNamespace: {arg.FullyQualifiedNamespace}");
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
if (_processor != null)
{
await _processor.DisposeAsync().ConfigureAwait(false);
}
if (_client != null)
{
await _client.DisposeAsync().ConfigureAwait(false);
}
}
public async Task CloseSubscriptionAsync()
{
await _processor.CloseAsync().ConfigureAwait(false);
}
}
So this is how we solved the problem. It was related to the message lock duration, which is set for the Azure Resource in the portal. Previous Value: 30s. New Value: 3min.

Polly Circuit breaker not maintaining state with .net core HTTP Client

I have implemented the polly retry and Circuit breaker policy (wrapped). when the call fails and the circuit is open for the previous call the next call again goes to the retry and hit the circuit breaker again instead of just throwing the circuitbreakexception. I think somehow the HTTP client is getting recreated again even though am using the typed client. I am not able to figure the issue. Here is the code
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddHttpClient<IIntCall, IntCall>().WrapResilientPolicies();
}
Interface
public interface IIntCall
{
Task<bool> DoSomething();
}
Implementation:
public class IntCall : IIntCall
{
private readonly HttpClient client;
public IntCall(HttpClient httpClient)
{
this.client = httpClient;
}
public async Task<bool> DoSomething()
{
var response = await client.GetAsync("http://www.onegoogle.com");
var content = await response.Content.ReadAsStringAsync();
return false;
}
}
Polly Implementation
public static class CBExtensions
{
public static void WrapResilientPolicies(this IHttpClientBuilder builder)
{
builder.AddPolicyHandler((service, request) =>
GetRetryPolicy().WrapAsync(GetCircuitBreakerPolicy()));
}
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions.HandleTransientHttpError()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30), (result, retryAttempt) =>
{
Debug.WriteLine("circuit broken");
},
() =>
{
Debug.WriteLine("circuit closed");
});
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions.HandleTransientHttpError()
.Or<Exception>(e => !(e is BrokenCircuitException))
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromMilliseconds(500),
onRetry: (context, attempt) =>
{
Debug.WriteLine("error");
}
);
}
}
I figured the issue. because I am fetching the request details the policy is injected every call and hence the state is renewed. I moved my code from
public static void WrapResilientPolicies(this IHttpClientBuilder builder)
{
builder.AddPolicyHandler((service, request) =>
GetRetryPolicy().WrapAsync(GetCircuitBreakerPolicy()));
}
to this
public static void WrapResilientPolicies(this IHttpClientBuilder builder)
{
builder.AddPolicyHandler(
GetRetryPolicy().WrapAsync(GetCircuitBreakerPolicy()));
}

SignalR Hub in C# class

Please tell me how I can use SignalR in not controller class.
I'm using AspNetCore.SignalR 1.0.2.
For example my Hub:
public class EntryPointHub : Hub
{
public async Task Sended(string data)
{
await this.Clients.All.SendAsync("Send", data);
}
}
In my job class (hangfire) SignalR doesn't work, my frontend not recieved messages.
public class UpdateJob
{
private readonly IHubContext<EntryPointHub> _hubContext;
public UpdateJob(IHubContext<EntryPointHub> hubContext)
{
_hubContext = hubContext;
}
public void Run()
{
_hubContext.Clients.All.SendAsync("Send", "12321");
}
}
But it In my controller works well.
...
public class SimpleController: Controller
{
private readonly IHubContext<EntryPointHub> _hubContext;
public SimpleController(IHubContext<EntryPointHub> hubContext)
{
_hubContext = hubContext;
}
[HttpGet("sendtoall/{message}")]
public void SendToAll(string message)
{
_hubContext.Clients.All.SendAsync("Send", message);
}
}
I think you are missing .net core DI mechanism for your Job Class. In Startup.cs file add that like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
services.AddScoped<UpdateJob>();
}
public void Configure(IApplicationBuilder app)
{
app.UseSignalR(routes =>
{
routes.MapHub<EntryPointHub>("ephub");
});
}
Then you need to install signalr-client for client end and calling like below in js file.
let connection = new signalR.HubConnection('/ephub');
connection.on('send', data => {
var DisplayMessagesDiv = document.getElementById("DisplayMessages");
DisplayMessagesDiv.innerHTML += "<br/>" + data;
});
Hope this will help you.
Solved: Thank for comments, I implement JobActivator and send to activator constructor ServiceProvider like this (in Startup.Configure):
IServiceProvider serviceProvider = app.ApplicationServices.GetService<IServiceProvider>();
GlobalConfiguration.Configuration
.UseActivator(new HangfireActivator(serviceProvider));
And add in ConfigureServices:
services.AddTransient<UpdateJob>();

How to send message to only caller client in SignalR?

Below is my SignalR Hub class code.
public class ChatHub : Hub
{
public void Send(string name, string message)
{
// Call the addNewMessageToPage method to update clients.
Clients.All.addNewMessageToPage(name, message);
}
public async void webAPIRequest()
{
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts");
//Clients.All.addWebAPIResponseToPage(response);
Clients.Caller.addWebAPIResponseToPage(response);
await Task.Delay(1000);
response = await client.GetAsync("http://www.google.com");
Clients.Caller.addWebAPIResponseToPage(response);
//Clients.All.addWebAPIResponseToPage(response);
await Task.Delay(1000);
response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts?userId=1");
//Clients.All.addWebAPIResponseToPage(response);
Clients.Caller.addWebAPIResponseToPage(response);
}
}
As per my understanding ,
Clients.Caller.addWebAPIResponseToPage(response);
sends message only to caller client , whereas
Clients.All.addWebAPIResponseToPage(response);
sends the message to all the clients.
Is my understanding correct ?
If No , then what method needs to be called to send message only to caller client.
Yes your understanding is correct. Read it here
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-server#selectingclients
You can use caller, you can provide current user connection id and send message to that or I have seen a group called self in some places which keeps user logged in from various devices and send message to that.
For example if you are logged in on a desktop and on mobile as well then you will have two connection IDs but you are same user. You can add this user to a self_username_unique_group_name kind of group and then send a message to that group which will be sent to all devices where user is connected.
You can also manage connection IDs for a single user in a separate table and send message to all of those connection IDs if you want.
Too much flexibility and magic
Enjoy
I found this to work quite well where ConnectionMapping is described in https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/mapping-users-to-connections
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<SomeService>();
services.AddScoped<SessionService>();
services.AddScoped<ProgressHub>();
}
}
public class SomeService
{
ProgressHub _hub;
public SomeService(ProgressHub hub)
{
_hub = hub;
}
private async Task UpdateProgressT(T value)
{
_hub.Send(value);
}
}
public class ProgressHub : Hub
{
private readonly static ConnectionMapping<string> _connections = new ConnectionMapping<string>();
private readonly IHubContext<ProgressHub> _context;
private readonly SessionService _session;
public ProgressHub(IHubContext<ProgressHub> context, SessionService session)
{
_context = context;
_session = session;
}
public override Task OnConnectedAsync()
{
_connections.Add(_session.SiteId, Context.ConnectionId);
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
_connections.Remove(_session.SiteId, Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
public async Task Send(object data)
{
foreach (var connectionId in _connections.GetConnections(_session.SiteId))
{
await _context.Clients.Client(connectionId).SendAsync("Message", data);
}
}
}
public class SessionService
{
private readonly ISession _session;
public SessionService(IHttpContextAccessor accessor)
{
_session = accessor.HttpContext.Session;
if (_session == null) throw new ArgumentNullException("session");
}
public string SiteId
{
get => _session.GetString("SiteId");
set => _session.SetString("SiteId", value);
}
}

Signalr - Associating usernames with connectionIds

Here is my hub:
[HubName("marketWatch")]
public class MarketWatchHub : Hub
{
public override Task OnConnected()
{
SocketCommunicator.Instance.UserConnected(Context.ConnectionId, Context.User.Identity.Name);
return base.OnConnected();
}
public override Task OnDisconnected()
{
SocketCommunicator.Instance.UserDisconnected(Context.ConnectionId);
return base.OnDisconnected();
}
public override Task OnReconnected()
{
// TODO: implement...
return base.OnReconnected();
}
public List<MarketDataResponse> GetAllMarketWatchData()
{
return SocketCommunicator.Instance.MarketDataList;
}
}
And here is the simplified version of SocketCommunicator class:
public class SocketCommunicator
{
private static SocketCommunicator _Instance = new SocketCommunicator();
public static SocketCommunicator Instance
{
get { return SocketCommunicator._Instance; }
}
private Socket socket { get; set; }
private readonly object lockObj = new object();
private IHubContext hubContext;
private List<UserDetail> connectedUsers;
public List<MarketDataResponse> MarketDataList;
private SocketCommunicator() { }
public void UserConnected(string connectionId, string username)
{
lock (lockObj)
{
connectedUsers.Add(new UserDetail() { ConnectionId = connectionId, UserName = username });
}
}
public void UserDisconnected(string connectionId)
{
lock (lockObj)
{
connectedUsers.RemoveAll(ud => ud.ConnectionId == connectionId);
}
}
public void GetMarketData()
{
// Do something and set this.MarketDataList
}
}
When I hit F5 and debug my application it works like a charm. When user logs in, my OnConnected method in my hub is called and when user logs off OnDisconnected method is called. But if user logs in and close his/her browser, OnDisconnected method is not being called. This means in time, my connectedUsers list will contain enormous number of UserDetail objects that are not really connected. How can I avoid this situation? Is there a better way to store user - connection id association?
Thanks in advance,
How long are you waiting for OnDisconnect to get called? It isn't always instantaneous, especially if the client doesn't close 'cleanly' (i.e. closing the browser). It should get called eventually, once the connection times-out.
I have a similar setup and it works fine.

Resources