Connect with SingalR client with IHubContext provided in diffrent thread - c#

I am using ASP.NET Core, and I am adding some users to a collection via SingalR hub endpoint:
public class MatchMakingHub : Hub
{
//....
// called by client
public async Task EnlistMatchMaking(int timeControlMs)
{
Guid currentId = Guid.Parse(this.Context.User.GetSubjectId());
GetPlayerByIdQuery getPlayerByIdQuery = new GetPlayerByIdQuery(currentId);
Player currentPlayer = await requestSender.Send<Player>(getPlayerByIdQuery);
var waitingPlayer = new WaitingPlayer(currentPlayer, timeControlMs);
this.matchMakePool.Add(waitingPlayer);
}
}
matchMakePool being a singleton collection.
Later, I have an ASP.NET Core background service fetch the users from the collection, and notify them about being fetched:
public class MatchMakingBackgroundService : BackgroundService
{
private readonly MatchMakePoolSingleton matchMakePoolSingleton;
private readonly IServiceProvider serviceProvider;
private const int RefreshTimeMs = 1000;
public MatchMakingBackgroundService(MatchMakePoolSingleton matchMakePoolSingleton, IServiceProvider serviceProvider)
{
this.matchMakePoolSingleton = matchMakePoolSingleton;
this.serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while(!stoppingToken.IsCancellationRequested)
{
var result = matchMakePoolSingleton.RefreshMatches();
var tasks = new List<Task>();
foreach(var match in result)
{
tasks.Add(StartGameAsync(match));
}
await Task.WhenAll(tasks);
await Task.Delay(RefreshTimeMs, stoppingToken);
}
}
private async Task StartGameAsync(MatchMakeResult match)
{
using var scope = serviceProvider.CreateScope();
var sender = scope.ServiceProvider.GetRequiredService<ISender>();
var hubContext = serviceProvider.GetRequiredService<IHubContext<MatchMakingHub>>();
CreateNewGameCommand newGameCommand = new CreateNewGameCommand(match.WhitePlayer.Id, match.BlackPlayer.Id, TimeSpan.FromMilliseconds(match.TimeControlMs));
Guid gameGuid = await sender.Send(newGameCommand);
await hubContext.Clients.User(match.WhitePlayer.Id.ToString()).SendAsync("NotifyGameFound", gameGuid);
await hubContext.Clients.User(match.BlackPlayer.Id.ToString()).SendAsync("NotifyGameFound", gameGuid);
}
}
My problem is that NotifyGameFound is not being called in the client side. When I notified them straight from the hub itself it was received, but for some reason it doesn't when I call it through the provided IHubContext<MatchMakingHub>. I suspect that this is because it runs on another thread.
Here is the client code:
// in blazor
protected override async Task OnInitializedAsync()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if(tokenResult.TryGetToken(out var token))
{
hubConnection
= new HubConnectionBuilder().WithUrl(NavigationManager.ToAbsoluteUri("/hubs/MatchMaker"), options =>
{
options.AccessTokenProvider = () => Task.FromResult(token.Value);
}).Build();
await hubConnection.StartAsync();
hubConnection.On<Guid>("NotifyGameFound", id =>
{
//do stuff
});
await MatchMakeRequast();
}
}
async Task MatchMakeRequast() =>
await hubConnection.SendAsync("EnlistMatchMaking", Secs * 1000);

I use injection to achieve this.
In my servers Startup.cs ConfigureServices mothod I have:
services.AddScoped<INotificationsBroker, NotificationsBroker>();
In your case I am assuming you are injecting MatchMakingBackgroundService
Something like:
services.AddScoped<MatchMakingBackgroundService>();
In my NotificationsBroker constructor I inject the context:
private readonly IHubContext<NotificationsHub> hub;
public NotificationsBroker(IHubContext<NotificationsHub> hub)
=> this.hub = hub;
I then inject the broker into any service I require it and the service can call the hubs methods I expose via the interface.
You don't have to go the extra step, I do this for testing, you could inject the context directly into your MatchMakingBackgroundService.

Related

Unit test .net 6 console app using IHostApplicationLifeTime

I have a class (CronClass) that inherits from IHostedService with 2 methods which are StartAsync(CancellationToken) and StopAsync(CancellationToken).
How do you go about unit testing the StartAsync method to verify that the code was executed, using Moq Library?
For example:
public class CronClass: IHostedService
{
private readonly IHostedApplicationLifetime applicationLifetime;
private readonly IService service;
// IHostedApplicationLifetime/IService are injected DI to via the constructor
public Task StartAsync(CancellationToken cancellationToken)
{
applicationLifeTime.ApplicationStarted.Register(() =>
{
Task.Run(async () =>
{
log.LogInformation("Cron Started");
await service.Process();
});
});
}
//...
}
I would start with creating a mock of IHostApplicationLifetime
public class MockHostApplicationLifetime : IHostApplicationLifetime, IDisposable
{
internal readonly CancellationTokenSource _ctsStart = new CancellationTokenSource();
internal readonly CancellationTokenSource _ctsStopped = new CancellationTokenSource();
internal readonly CancellationTokenSource _ctsStopping = new CancellationTokenSource();
public MockHostApplicationLifetime()
{
}
public void Started()
{
_ctsStart.Cancel();
}
CancellationToken IHostApplicationLifetime.ApplicationStarted => _ctsStart.Token;
CancellationToken IHostApplicationLifetime.ApplicationStopping => _ctsStopping.Token;
CancellationToken IHostApplicationLifetime.ApplicationStopped => _ctsStopped.Token;
public void Dispose()
{
_ctsStopped.Cancel();
_ctsStart.Dispose();
_ctsStopped.Dispose();
_ctsStopping.Dispose();
}
public void StopApplication()
{
_ctsStopping.Cancel();
}
}
In your unit test create a mock of IService. Create instance of CronClass and call cronClass.StartAsync. Then start MockHostApplicationLifetime. It will trigger registered callback ApplicationStarted.Register. Then verify that Process() was called.
You are starting the task in Register method, so it can happen that the unit test can finish before the task is created and service.Process is called. In that case I would wait some time before verification.
[Test]
public async Task Test1()
{
var hal = new MockHostApplicationLifetime();
var mockService = new Mock<IService>();
var cronClass = new CronClass(hal, mockService.Object);
await cronClass.StartAsync(CancellationToken.None);
hal.Started();
// maybe not needed, test passed without the delay
//await Task.Delay(500);
mockService.Verify(mock => mock.Process());
}

Message is published from API #1, how do I create a listener in another API?

I've two APIs.
When one of the endpoints are called in API #1, message is sent to queue in Azure Service Bus.
API #2 should listen and make some changes in DB after this message appears in queue.
Message is sent to queue successfully.
Listener part doesn't work because the listener method is never called (and I do not understand how to call it).
Listener in API #2 :
public class MessageConsumer : IMessageConsumer
{
const string connectionString = "stringTakenFromAzure";
private static IQueueClient queueClient;
private CartingDbContext context;
public MessageConsumer(CartingDbContext context)
{
this.context = context;
}
public async Task Consume()
{
queueClient = new QueueClient(connectionString, "cartqueue");
var options = new MessageHandlerOptions(ExceptionReceivedHandler)
{
MaxConcurrentCalls = 1,
AutoComplete = false
};
queueClient.RegisterMessageHandler(ProcessMessageAsync, options);
await queueClient.CloseAsync();
}
private async Task ProcessMessageAsync(Microsoft.Azure.ServiceBus.Message message, CancellationToken cancellationToken)
{
var jsonBody = Encoding.UTF8.GetString(message.Body);
var categoryItem = JsonSerializer.Deserialize<CategoryItem>(jsonBody);
//update item in DB.
var categoryItemInDb = context.CategoryItems.Where(x => x.Id == categoryItem.Id).FirstOrDefault();
if (categoryItemInDb == null)
{
context.CategoryItems.Add(categoryItem);
}
else
{
context.CategoryItems.Update(categoryItem);
}
context.SaveChanges();
await queueClient.CompleteAsync(message.SystemProperties.LockToken);
}
private static Task ExceptionReceivedHandler(ExceptionReceivedEventArgs args)
{
return Task.CompletedTask;
}
}
Program.cs
builder.Services.AddTransient<IMessageConsumer, MessageConsumer>();

Some services are not able to be constructed Error while validating the service descriptor

I have a ASP.NET Core 5.0 MVC solution,
public abstract class HostedService : IHostedService, IDisposable
{
private Task _currentTask;
private readonly CancellationTokenSource _cancellationTokenSource = new
CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken token);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_currentTask = ExecuteAsync(cancellationToken);
return _currentTask.IsCompleted ? _currentTask : Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_currentTask == null) return;
try
{
_cancellationTokenSource.Cancel();
}
finally
{
await Task.WhenAny(_currentTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public virtual void Dispose()
{
_cancellationTokenSource.Cancel();
}
}
Gets the exchange rates from URL
public class ExchangeSyncManager : HostedService
{
private readonly CurrencyServices _currencyServices;
private readonly ExchangeRateServices _exchangeRateServices;
public ExchangeSyncManager(CurrencyServices currencyServices, ExchangeRateServices
exchangeRateServices)
{
_currencyServices = currencyServices;
_exchangeRateServices = exchangeRateServices;
}
protected override async Task ExecuteAsync(CancellationToken token)
{
// işlem iptal edilmemişse…
if (!token.IsCancellationRequested)
{
var url = "http://www.tcmb.gov.tr/kurlar/today.xml";
XmlDocument xmlVerisi = new XmlDocument();
List<ExchangeRate> list = new List<ExchangeRate>();
xmlVerisi.Load(url);
foreach (var currency in _currencyServices.GetCurrencies())
{
var format = string.Format("Tarih_Date/Currency[#Kod='{0}']/ForexSelling", currency.Name);
var selectAndReplace = xmlVerisi.SelectSingleNode(format).InnerText.Replace('.', ',');
decimal value = Convert.ToDecimal(selectAndReplace);
list.Add(new ExchangeRate
{
Date = DateTime.Now,
Value = value,
CurrencyId = currency.Id
});
}
_exchangeRateServices.AddRange(list);
await Task.Delay(TimeSpan.FromDays(1), token);
}
}
}
And in startup :
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//other services here
......................................................
.....................................................
services.AddScoped<CurrencyServices, CurrencyServices>();
services.AddScoped<ExchangeRateServices>();
services.AddHostedService<ExchangeSyncManager>();
}
Still I am getting this error :
Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: IPMMS.Business.Managers.ExchangeSyncManager': Cannot consume scoped service 'IPMMS.Business.Services.CurrencyServices' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)'
What is wrong ?
You cannot inject a scoped services inside a singleton. They are bound to HTTP requests.
A HostedService's lifetime is singleton.
However you can use the IServiceProvider to create a scope and retrieve an instance of your scoped service.
You will find how to fix your issue here :
https://learn.microsoft.com/en-us/dotnet/core/extensions/scoped-service

How can I create a BackGround service that runs a function every given period of time ? Using C# (asp.net core 3.1.1)

I'm trying to make call to a function every specified interval of time, for that m using Background service, here is what I have done:
Here is the Alerting controller where I have the function:
public class AlertingController : ControllerBase
{
private readonly DatabaseContext _context;
private readonly IMapper _mapper;
public AlertingController(DatabaseContext context, IMapper mapper)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public AlertingController()
{
}
//function that adds in the DB
public async Task<AlertingResponse> GetAlertingToDB()
{
AlertingResponse dataGet;
using (var httpClient = new HttpClient())
{
using (var response = await httpClient
.GetAsync(MetricApiLink))
{
string apiResponse = await response.Content.ReadAsStringAsync();
dataGet = JsonConvert.DeserializeObject<AlertingResponse>(apiResponse);
}
}
if (dataGet.data.alerts != null || dataGet.data.alerts.Count > 0)
{
foreach (var alert in dataGet.data.alerts)
{
CreateAlertQuery QueryAlert = new CreateAlertQuery();
QueryAlert.Name = alert.labels.alertname;
QueryAlert.Instance = alert.labels.instance;
QueryAlert.Serverity = alert.labels.severity;
QueryAlert.Summary = alert.annotations.summary;
QueryAlert.State = alert.state;
QueryAlert.ActiveAt = alert.activeAt;
var _Alert = _mapper.Map<AlertingDataModel>(QueryAlert);
_context.Alertings.Add(_Alert);
await _context.SaveChangesAsync();
}
}
return null;
}
}
I have tested the method with a HTTPGET request, it works fine, add the alerts into my database:
I have created a scooped service where I called the function GetAlertingToDB():
internal interface IScopedAlertingService
{
Task DoWork(CancellationToken stoppingToken);
}
public class ScopedAlertingService : IScopedAlertingService
{
private int executionCount = 0;
private readonly ILogger _logger;
public ScopedAlertingService(ILogger<ScopedAlertingService> logger)
{
_logger = logger;
}
public async Task DoWork(CancellationToken stoppingToken)
{
AlertingController _AlertingToDB = new AlertingController();
while (!stoppingToken.IsCancellationRequested)
{
executionCount++;
_logger.LogInformation(
"Scoped Processing Service is working. Count: {Count}", executionCount);
await _AlertingToDB.GetAlertingToDB();
await Task.Delay(10000, stoppingToken);
}
}
}
I have also created the Class that will consume my service, and will run in the BackGround:
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedAlertingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await Task.CompletedTask;
}
}
I injected the dependencies on the Startup Class and added the hosted service:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedAlertingService, ScopedAlertingService>();
The functions are working just fine untill a call the GetAlertingToDB() function and it doesn't work.
Any help would be great, thanks everyone :)
Personally I would rearrange your solution so that your background service doesn't need to create a Controller. Instead the controller, if you still need it at all, should call into your ScopedAlertingService where the work is performed once. Your background service can simply loop forever, with an await Task.Delay().
public class ScopedAlertingService : IScopedAlertingService
{
public async Task DoWork(CancellationToken stoppingToken)
{
// move contents of your AlertingController.GetAlertingToDB here
}
}
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly IServiceProvider _services;
public ConsumeScopedServiceHostedService(IServiceProvider services)
{
_services = services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(10000, stoppingToken);
using (var scope = _services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedAlertingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
}
}
Hangfire RecurringJob would be an option for you case. you can check it here https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html.
The benefit of using it is: you have a dashboard to check when the task will be fired and the result of the task.
There are several options for doing this.
Please read the following link from the Microsoft Documentation which has several examples on how to do this in .NET Core and ASP.NET Core:
Worker Service In NET Core
It is called Worker Services.
You basically implement two interfaces: IHostedService, IDisposable
Then you register your service inside you Startup class in your ConfigureServices method like this:
services.AddHostedService<MyCoolWorkerServiceClass>();
For a Complete Example
One last sugestion. The example uses System.Threading.Timer... But I think it is better to use a System.Timers.Timer with AutoReset = false.
The reason is to avoid overlapping runs of your service. Once a run is done then you start your timer again.
But then again it all depends on what you want to achieve.

SignalR - Multitenant Dependency Injection

I need to resolve a DbContext based on tenant's owin value. But in the pipeline of method OnDisconnected of hub, the HttpContext is not accessible.
My hub class:
public class UserTrackingHub : Hub
{
private readonly UserContext _context;
public UserTrackingHub(UserContext context) { ... }
public override async Task OnConnected() { /* OK HERE...*/ }
public override async Task OnDisconnected(bool stopCalled)
{
// NEVER FIRES WITH IF I USE THE CTOR INJECTION.
var connection = await _context.Connections.FindAsync(Context.ConnectionId);
if (connection != null)
{
_context.Connections.Remove(connection);
await _context.SaveChangesAsync();
}
}
}
Here's my Autofac config:
public static IContainer Register(IAppBuilder app)
{
var builder = new ContainerBuilder();
// Other registers...
builder.Register<UserContext>(c =>
{
// Details and conditions omitted for brevity.
var context = HttpContext.Current; // NULL in OnDisconnected pipeline.
var owinContext = context.GetOwinContext();
var tenant = owinContext.Environment["app.tenant"].ToString();
var connection = GetConnectionString(tenant);
return new UserContext(connection);
});
var container = builder.Build();
var config = new HubConfiguration
{
Resolver = new AutofacDependencyResolver(container)
};
app.MapSignalR(config);
return container;
}
Can someone help me to identify the tenant OnDisconnected in this or any other way?
Thanks!
For anyone interested, I end up injecting a context factory instead the context itself:
public class UserTrackingHub : Hub
{
private readonly Func<string, UserContext> _contextFactory;
public UserTrackingHub(Func<string, UserContext> contextFactory) { ... }
public override async Task OnConnected() { ... }
public override async Task OnDisconnected(bool stopCalled)
{
var tenant = Context.Request.Cookies["app.tenant"].Value;
using (var context = _contextFactory.Invoke(tenant))
{
var connection = await context.Connections.FindAsync(Context.ConnectionId);
if (connection != null)
{
context.Connections.Remove(connection);
await context.SaveChangesAsync();
}
}
}
}
And Autofac's config:
// Resolve context based on tenant
builder.Register<Func<string, UserContext>>(c => new Func<string, UserContext>(tenant => UserContextResolver.Resolve(tenant)));

Categories

Resources