How to setup a .NET core web api that
accepts a string value,
puts into a queue
and return flag that message is accepted (regardless it is processed).
Also, a routine which keeps checking the queue, and process the messages one by one.
As per the requirement, the api is going to act as the receiver of messages which may get hits as much as hundreds of times in a minute, while the messages it receives should be processed one by one.
I am bit new to web apis, so wonder if such setup is good to have and if yes how to put together different components.
Thanks in advance..
Honestly, I don't think that it makes sense to receive and process messages in one process, so I would recommend to use external messaging system like RabbitMQ or Kafka or any other existing system of your preference, where you can put your messages and another process would consume it. It's quite big topic, you can start from this tutorial
If you still want to have it in one process it's also possible, you can create a background task queue, put there your messages and create background task which will consume them from that queue.
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
Task<Func<CancellationToken, Task>> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
new ConcurrentQueue<Func<CancellationToken, Task>>();
private SemaphoreSlim _signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(
Func<CancellationToken, Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue(workItem);
_signal.Release();
}
public async Task<Func<CancellationToken, Task>> DequeueAsync(
CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem;
}
}
Background task:
public class QueuedHostedService : BackgroundService
{
private readonly ILogger _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
ILoggerFactory loggerFactory)
{
TaskQueue = taskQueue;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
}
public IBackgroundTaskQueue TaskQueue { get; }
protected async override Task ExecuteAsync(
CancellationToken cancellationToken)
{
_logger.LogInformation("Queued Hosted Service is starting.");
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
_logger.LogInformation("Queued Hosted Service is stopping.");
}
}
Registration:
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
}
Inject to controller:
public class ApiController
{
private IBackgroundTaskQueue queue;
public ApiController(IBackgroundTaskQueue queue)
{
this.queue = queue;
}
public IActionResult StartProcessing()
{
queue.QueueBackgroundWorkItem(async token =>
{
// put processing code here
}
return Ok();
}
}
You can modify BackgroundTaskQueue to fit your requirements, but I hope you understand the idea behind this.
Update for latecomers....
I used in Asp.net core 6, you can download sample here: https://learn.microsoft.com/en-us/dotnet/core/extensions/queue-service
Config Program
// and more...
#region Worker Services
builder.Host.ConfigureServices((context, services) =>
{
services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(_ =>
{
if (!int.TryParse(context.Configuration["QueueCapacity"], out var queueCapacity))
{
queueCapacity = 100;
}
return new BackgroundTaskQueue(queueCapacity);
});
});
#endregion
#region App
// App config
var app = builder.Build();
// Monitor worker config
var monitorLoop = app.Services.GetRequiredService<MonitorLoop>()!;
monitorLoop.StartMonitorLoop();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseHttpsRedirection();
}
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
await app.RunAsync();
#endregion
In Controller
// and more..
private readonly IMailService _mailService;
private readonly IBackgroundTaskQueue _queue;
// and more..
public AuthenticateController(
IMailService mailService,
IBackgroundTaskQueue queue)
{
_mailService = mailService;
_queue = queue;
}
[HttpPost]
[Route("forgot-password")]
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordModel model)
{
// and more...
// Queue processing
await _queue.QueueBackgroundWorkItemAsync(async (token) =>
{
await _mailService.SendAsync(mailData, token);
});
return Ok();
}
Hope this help !
Related
I'm developing an asp.net project using .NET 5.0; in this project i use Entity Framework, code first, with a sql lite file database.
In startup.cs file i use the following code to create and update db schema programmatically:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
// migrate any database changes on startup (includes initial db creation)
dataContext.Database.Migrate();
...
}
Then i have a service that i use to clean old data from some tables of database. It cleans them on startup and periodically:
public class TimedDbCleanerService : IHostedService, IDisposable
{
...
}
With those functions:
public Task StartAsync(CancellationToken stoppingToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromHours(_dbCleanerSettings.Hours));
return Task.CompletedTask;
}
private void DoWork(object state)
{
// create scoped dbcontext
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
// check and remove stuff
dbContext.SaveChanges();
}
}
Registered with this line of code in startup.cs:
services.AddHostedService<TimedDbCleanerService>();
The problem here is that if the database file doesn't exist before executing the project, the cleaner service tries to access a table of database that doesn't exist yet.
Using debugger i can see that the Database.Migrate() is called before the service access the database, but the migration seems to be an async task that takes time to be completed.
There's a way to wait to migration to fully execute its job, before creating and starting the cleaner service?
A better alternative could be to perform the migrations before you start the app, which would simplify things quite a bit. No need for synchronization between threads.
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}
// database migration is completed
host.Run();
}
Since this is done before the background service starts running, there's no risk of a race condition.
Now the background service becomes even simpler:
class CleanerService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public CleanerService(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await PerformCleanup();
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
await PerformCleanup();
}
}
private async Task PerformCleanup()
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// ... clean things up
}
}
Create a class to orchestrate the migration. This will perform the migration and help others wait for the completion using a SemaphoreSlim.
Inside this class we inject the DbContext and run the migration, then let the threads waiting for the migration continue execution.
public class DatabaseMigrator
{
private readonly AppDbContext _dbContext;
private static readonly SemaphoreSlim _migrationEvent = new SemaphoreSlim(0);
public DatabaseMigrator(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public void Migrate()
{
_dbContext.Database.Migrate();
_migrationEvent.Release(1);
}
public Task WaitForMigrationAsync(CancellationToken cancellationToken = default)
{
return _migrationEvent.WaitAsync(cancellationToken);
}
}
Register this class to DI:
services.AddSingleton<DatabaseMigrator>();
In your Main function or Startup.Configure, inject this function and run the migration:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DatabaseMigrator databaseMigrator)
{
databaseMigrator.Migrate();
// ...
}
In the background service, we don't inject the migrator, because it depends on scoped services like DbContext. Instead, we resolve one from a scope we create, wait for the migration to complete.
class CleanerService : BackgroundService
{
private IServiceScopeFactory _serviceScopeFactory;
public CleanerService(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var migrator = scope.ServiceProvider.GetRequiredService<DatabaseMigrator>();
await migrator.WaitForMigrationAsync(stoppingToken);
}
await PerformCleanup();
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
await PerformCleanup();
}
}
private async Task PerformCleanup()
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// ... clean things up
}
}
One small point: Instead of using a Timer in an async context that forces you to use void, which bring a horde of problems when working async, you can call Task.Delay in a loop, which will release the thread to threadpool to let it execute other tasks while waiting:
await PerformCleanup();
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
await PerformCleanup();
}
In my asp.net core 5.0 app, I call an async method that might take some time to be processed.
await someObject.LongRunningProcess(cancelationToken);
However, I want that method to be timeout after 5 seconds. I know that instead of "cancelationToken" passed by asp.net core action, I can use "CancellationTokenSource" :
var s_cts = new CancellationTokenSource();
s_cts.CancelAfter(TimeSpan.FromSeconds(5);
await someObject.LongRunningProcess(s_cts );
Is it possible to use "CancellationTokenSource" as a default "Cancelation Token" policy for all asp.net core requests ? I mean override the one which is passed as a parameter of the action ?
Or is it possible to change the default timeout for all request in asp.net core 5.0 ?
[Update]
Customizing the CancellationToken passed to actions
You need to replace the default CancellationTokenModelBinderProvider that binds HttpContext.RequestAborted token to CancellationToken parameters of actions.
This involves creating a custom IModelBinderProvider. Then we can replace the default binding result with our own.
public class TimeoutCancellationTokenModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context?.Metadata.ModelType != typeof(CancellationToken))
{
return null;
}
var config = context.Services.GetRequiredService<IOptions<TimeoutOptions>>().Value;
return new TimeoutCancellationTokenModelBinder(config);
}
private class TimeoutCancellationTokenModelBinder : CancellationTokenModelBinder, IModelBinder
{
private readonly TimeoutOptions _options;
public TimeoutCancellationTokenModelBinder(TimeoutOptions options)
{
_options = options;
}
public new async Task BindModelAsync(ModelBindingContext bindingContext)
{
await base.BindModelAsync(bindingContext);
if (bindingContext.Result.Model is CancellationToken cancellationToken)
{
// combine the default token with a timeout
var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(_options.Timeout);
var combinedCts =
CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
// We need to force boxing now, so we can insert the same reference to the boxed CancellationToken
// in both the ValidationState and ModelBindingResult.
//
// DO NOT simplify this code by removing the cast.
var model = (object)combinedCts.Token;
bindingContext.ValidationState.Clear();
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(model);
}
}
}
}
class TimeoutOptions
{
public int TimeoutSeconds { get; set; } = 30; // seconds
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}
Then add this provider to Mvc's default binder provider list. It needs to run before all others, so we insert it at the beginning.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.Configure<MvcOptions>(options =>
{
options.ModelBinderProviders.RemoveType<CancellationTokenModelBinderProvider>();
options.ModelBinderProviders.Insert(0, new TimeoutCancellationTokenModelBinderProvider());
});
// remember to set the default timeout
services.Configure<TimeoutOptions>(configuration => { configuration.TimeoutSeconds = 2; });
}
Now ASP.NET Core will run your binder whenever it sees a parameter of CancellationToken type, which combines HttpContext.RequestAborted token with our timeout token. The combined token is triggered whenever one of its component is cancelled (due to timeout or request abortion, whichever is cancelled first)
[HttpGet("")]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); // throws TaskCanceledException after 2 seconds
return Ok("hey");
}
References:
https://github.com/dotnet/aspnetcore/blob/348b810d286fd2258aa763d6eda667a83ff972dc/src/Mvc/Mvc.Core/src/ModelBinding/Binders/CancellationTokenModelBinder.cs
https://abdus.dev/posts/aspnetcore-model-binding-json-query-params/
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0#custom-model-binder-sample
One approach to solve this problem is wrapping that logic inside a class. Write a class that runs a task with a configurable timeout.
Then register it in DI, then use it anywhere you want to reuse the configuration.
public class TimeoutRunner
{
private TimeoutRunnerOptions _options;
public TimeoutRunner(IOptions<TimeoutRunnerOptions> options)
{
_options = options.Value;
}
public async Task<T> RunAsync<T>(Func<CancellationToken, Task<T>> runnable,
CancellationToken cancellationToken = default)
{
// cancel the task as soon as one of the tokens is set
var timeoutCts = new CancellationTokenSource();
var token = timeoutCts.Token;
if (cancellationToken != default)
{
timeoutCts.CancelAfter(_options.Timeout);
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
token = combinedCts.Token;
}
return await runnable(token);
}
}
internal static class ServiceCollectionExtensions
{
public static IServiceCollection AddTimeoutRunner(this IServiceCollection services,
Action<TimeoutRunnerOptions> configure = null)
{
if (configure != null)
{
services.Configure<TimeoutRunnerOptions>(configure);
}
return services.AddTransient<TimeoutRunner>();
}
}
public class TimeoutRunnerOptions
{
public int TimeoutSeconds { get; set; } = 10;
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}
you'd then register this in Startup class,
public void ConfigureServices(IServiceCollection services)
{
services.AddTimeoutRunner(options =>
{
options.TimeoutSeconds = 10;
});
}
then consume it wherever you need that global option:
public class MyController : ControllerBase
{
private TimeoutRunner _timeoutRunner;
public MyController(TimeoutRunner timeoutRunner)
{
_timeoutRunner = timeoutRunner;
}
public async Task<IActionResult> DoSomething(CancellationToken cancellationToken)
{
await _timeoutRunner.RunAsync(
async (CancellationToken token) => {
await Task.Delay(TimeSpan.FromSeconds(20), token);
},
cancellationToken
);
return Ok();
}
}
Running a task before every action dispatch
Method 1: Action filters
We can use action filters to run a task before/after every request.
public class ApiCallWithTimeeotActionFilter : IAsyncActionFilter
{
private TimeoutRunner _runner;
public ApiCallWithTimeeotActionFilter(TimeoutRunner runner)
{
_runner = runner;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var result = await _runner.RunAsync(
async (CancellationToken token) =>
{
await Task.Delay(TimeSpan.FromSeconds(20), token);
return 42;
},
default
);
await next();
}
}
then to use it annotate a class with [TypeFilter(typeof(MyAction))]:
[TypeFilter(typeof(ApiCallWithTimeeotActionFilter))]
public class MyController : ControllerBase { /* ... */ }
Method 2: Middlewares
Another option is to use a middleware
class ApiCallTimeoutMiddleware
{
private TimeoutRunner _runner;
public ApiCallTimeoutMiddleware(TimeoutRunner runner)
{
_runner = runner;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// run a task before every request
var result = await _runner.RunAsync(
async (CancellationToken token) =>
{
await Task.Delay(TimeSpan.FromSeconds(20), token);
return 42;
},
default
);
await next(context);
}
}
then attach the middleware in Startup.Configure method:
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ApiCallTimeoutMiddleware>();
app.UseRouting();
app.UseEndpoints(e => e.MapControllers());
}
I have asp core app. I use IHostedService for periodic task. I need send periodic http request. This is my code:
class MyService: IHostedService
{
private readonly IServiceScopeFactory _scopeFactory;
public MyService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
private async void DoWork(object state)
{
using (var scope = _scopeFactory.CreateScope())
{
var clientFactory = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>();
var request = new HttpRequestMessage(HttpMethod.Get,
"https://api.github.com/repos/aspnet/AspNetCore.Docs/branches");
request.Headers.Add("Accept", "application/vnd.github.v3+json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
var client = clientFactory.CreateClient();
var response = await client.SendAsync(request);
...
}
}
}
Also I added it in my Startup.cs
services.AddHttpClient();
This is working. But I'm not sure if this is the best performance solution. Any tips?
I am new to the asp core. This is an example that I found on the net. how do i do it right?
If you want to run background service to send the request in asp.net core, I suggest you could directly inject the httpClientFactory instead of using ServiceProvider.GetRequiredService to inject it again.
More details, you could refer to below codes:
Make sure you have add the AddHttpClient service into the startup.cs
services.AddHttpClient();
2.Add below codes into the worker.cs:
public class Worker : IHostedService
{
private IHttpClientFactory httpClientFactory;
public Worker(IHttpClientFactory _httpClientFactory) {
httpClientFactory = _httpClientFactory;
}
public Task StartAsync(CancellationToken cancellationToken)
{
DoWork("test");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private async void DoWork(object state)
{
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:9012/");
//request.Headers.Add("Accept", "application/vnd.github.v3+json");
//request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
var client = httpClientFactory.CreateClient();
var response = await client.SendAsync(request);
int i = 0;
}
}
Result:
More details about how to use asp.net core background service, you could refer to this article.
I'm trying to gracefully terminate a ASP.Net Core 3.1 service (which will run in Kubernetes). When Kubernetes stops a service, it will send a SIGTERM event to the application, at which point I want in-flight requests to complete (which may take several seconds) before terminating... I think I can catch this in a hostedservice, as below, and hence not stop immediately.
The following works, but with a timeout of 5 seconds or longer, I receive an OperationCanceledException. Could anyone shed any light on why I get an OperationCanceledException or how shed any light on an alternative way to delay a SIGTERM event, to allow a graceful shutdown?
public static int Main(string[] args)
{
var logger = NLogBuilder
.ConfigureNLog("nlog.config")
.GetCurrentClassLogger();
try
{
CreateHostBuilder(args)
.ConfigureServices((hostBuilderContext, services) => { services.AddHostedService<LifetimeEventsHostedService>(); })
.Build()
.Run();
return 0;
}
catch (Exception e)
{
logger.Fatal(e, "Stopping due to exception");
return -1;
}
finally
{
LogManager.Shutdown();
}
}
This is the hosted service...
internal class LifetimeEventsHostedService : IHostedService
{
private readonly Microsoft.Extensions.Logging.ILogger _logger;
private readonly IHostApplicationLifetime _appLifetime;
public LifetimeEventsHostedService(
ILogger<LifetimeEventsHostedService> logger,
IHostApplicationLifetime appLifetime)
{
_logger = logger;
_appLifetime = appLifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnStarted()
{
_logger.LogInformation("OnStarted has been called.");
// Perform post-startup activities here
}
private void OnStopping()
{
_logger.LogInformation("OnStopping has been called.");
// Perform on-stopping activities here
// This works, but a timeout of 5 seconds or more subsequently causes an OperationCanceledException
Thread.Sleep(5000);
}
private void OnStopped()
{
_logger.LogInformation("OnStopped has been called.");
// Perform post-stopped activities here
}
}
I'm open to alternative approaches to graceful shutdown with ASP.Net Core 3.1, as it stands, I'm using a hosted service.
Within the .Net Core app, I was setting ShutdownTimeout on the webhost, however, setting the ShutdownTimeout on the generic host, does allow me to gracefully wait a number of seconds (more than the default sigterm, which is 5 seconds) prior to shutdown. The hint from #PmanAce helped me work that out.
As such, the following codes allow me to gracefully terminate. One caveat, the Thread.Sleep in LifetimeEventsHostedService must be less than option.ShutdownTimeout.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostBuilderContext, services) =>
{
services.AddHostedService<LifetimeEventsHostedService>();
services.Configure<HostOptions>(option =>
{
option.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel();
webBuilder.UseStartup<Startup>();
});
}
The following LifetimeEventsHostedService
public class LifetimeEventsHostedService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public LifetimeEventsHostedService(IHostApplicationLifetime hostApplicationLifetime)
{
_hostApplicationLifetime = hostApplicationLifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
_hostApplicationLifetime.ApplicationStopping.Register(OnStopping);
_hostApplicationLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnStopped()
{
Console.WriteLine("OnStopped");
}
private void OnStopping()
{
Console.WriteLine("OnStopping");
Console.WriteLine(DateTime.Now.ToLongTimeString());
Thread.Sleep(15000);
Console.WriteLine("Sleep finished");
Console.WriteLine(DateTime.Now.ToLongTimeString());
}
private void OnStarted()
{
Console.WriteLine("OnStarted");
}
}
Working on building signalR hub, I'm able to get data from hub to the client but I'm, not sure how do I push it every 1 second.
I'm not sure where do I set the timer in the controller where getApps method exists or in the hub?
Hub:
public class nphub : Hub
{
public readonly sbController _sbcontroller;
public nphub(sbController sbcontroller)
{
_sbcontroller = sbcontroller;
}
public async Task NotifyConnection()
{
IActionResult result = await _sbcontroller.getApps();
await Clients.All.SendAsync("TestBrodcasting", result);
}
}
In Controller:
public async Task<IActionResult> getApps()
{
// var request = new HttpRequestMessage(HttpMethod.Get, "apps");
// var response = await _client_NP.SendAsync(request);
// var json = await response.Content.ReadAsStringAsync();
return Ok($"Testing a Basic HUB at {DateTime.Now.ToLocalTime()}");
}
Client:
let connection = new signalR.HubConnectionBuilder()
.withUrl("/nphub").build();
connection.start().then(function () {
TestConnection();
}).catch(function (err) {
return console.error(err.toString());
});
function TestConnection() {
connection.invoke("NotifyConnection").catch(function (err) {
return console.error(err.toString());
});
}
connection.on("TestBrodcasting", function (time) {
document.getElementById('broadcastDiv').innerHTML = time.value;
document.getElementById('broadcastDiv').style.display = "block";
});
Just for the test purpose to see realtime changes, I'm trying to return time. I'm able to see time on the client but it's not changing.
You need to use a hosted service, as described in the docs. Add a class like:
internal class SignalRTimedHostedService : IHostedService, IDisposable
{
private readonly IHubContext<nphub> _hub;
private readonly ILogger _logger;
private Timer _timer;
public SignalRTimedHostedService(IHubContext<nphub> hub, ILogger<SignalRTimedHostedService> logger)
{
_hub = hub;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(1));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
// send message using _hub
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Note: A hosted service lives in singleton scope. You can inject IHubContext<T> directly, though, because it too is in singleton scope.
Then in ConfigureServices:
services.AddHostedService<SignalRTimedHostedService>();