Trying to roughly follow MSDN, I've added a hosted service after my scoped services in StartUp
class.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<IUtilityService, UtilityService>();
services.AddHostedService<StartupService>();
...
}
I've implemented StartAsync
like this.
public class StartupService : IHostedService
{
private IServiceProvider Provider { get; }
public StartupService(IServiceProvider provider)
{
Provider = provider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
IServiceScope scope = Provider.CreateScope();
IUtilityService service = scope.ServiceProvider
.GetRequiredService<IUtilityService>();
service.Seed();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
I've read a number of articles and blogs but it's above my ability to understand what should be returned at the end of the methods. It seems to work for now but I can clearly see that I'm breaching the idea by not using asynchronous calls and returninig a dummy (not even that at stop!) so I can safely conclude that I'm doing it wrong (although not apparently but I'm sure it's coming to bite my behind in the future).
What should I return in the implementation to ensure I'm "working with" not agains the framework?
StartAsync needs to return a Task, which may or may not be running (but ideally it should be running, thats the point of a HostedService - an operation/task that runs for the lifetime of the application, or just for some extended period of time longer than normal).
It looks like you are trying to perform extra startup items using a HostedService, instead of just trying to run a task/operation that will last for the entire lifetime of the application.
If this is the case, you can have a pretty simple setup. The thing you want to return from your StartAsync() method is a Task. When you return a Task.CompletedTask, you are saying that the work is already done and there is no code executing - the task is completed. What you want to return is your code that is doing your extra startup items that is running inside of a Task object. The good thing about the HostedService in asp.net is that it does not matter how long the task runs for (since it is meant to run tasks for the entire lifetime of the app).
One important note before the code example - if you are using a Scoped service in your task, then you need to generate a scope with the IServiceScopeFactory, read about that in this StackOverflow post
If you refactor your service method to return a task, you could just return that:
public Task StartAsync(CancellationToken)
{
IServiceScope scope = Provider.CreateScope();
IUtilityService service = scope.ServiceProvider
.GetRequiredService<IUtilityService>();
// If Seed returns a Task
return service.Seed();
}
If you have multiple service methods that all return a task, you could return a task that is waiting for all of the tasks to finish
public Task StartAsync(CancellationToken)
{
IServiceScope scope = Provider.CreateScope();
IUtilityService service = scope.ServiceProvider
.GetRequiredService<IUtilityService>();
ISomeOtherService someOtherService = scope.ServiceProvider
.GetRequiredService<ISomeOtherService>();
var tasks = new List<Task>();
tasks.Add(service.Seed());
tasks.Add(someOtherService.SomeOtherStartupTask());
return Task.WhenAll(tasks);
}
If your startup tasks do alot of CPU bound work, just return a Task.Run(() => {});
public Task StartAsync(CancellationToken)
{
// Return a task which represents my long running cpu startup work...
return Task.Run(() => {
IServiceScope scope = Provider.CreateScope();
IUtilityService service = scope.ServiceProvider
.GetRequiredService<IUtilityService>();
service.LongRunningCpuStartupMethod1();
service.LongRunningCpuStartupMethod2();
}
}
To use your cancellation token, some of the example code below shows how it can be done, by Catching a TaskCanceledException in a Try/Catch, and forcefully exiting our running loop.
Then we move on to tasks that will run for the entire application lifetime. Heres the base class that I use for all of my HostedService implementations that are designed to never stop running until the application shuts down.
public abstract class HostedService : IHostedService
{
// Example untested base class code kindly provided by David Fowler: https://gist.github.com/davidfowl/a7dd5064d9dcf35b6eae1a7953d615e3
private Task _executingTask;
private CancellationTokenSource _cts;
public Task StartAsync(CancellationToken cancellationToken)
{
// Create a linked token so we can trigger cancellation outside of this token's cancellation
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Store the task we're executing
_executingTask = ExecuteAsync(_cts.Token);
// If the task is completed then return it, otherwise it's running
return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
// Signal cancellation to the executing method
_cts.Cancel();
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
// Throw if cancellation triggered
cancellationToken.ThrowIfCancellationRequested();
}
// Derived classes should override this and execute a long running method until
// cancellation is requested
protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
}
In this Base Class, you will see that when StartAsync is called, we invoke our ExecuteAsync() method which returns a Task that contains a while loop - the Task will not stop running until our cancellation token is triggered, or the application gracefully/forcefully stops.
The ExecuteAsync() method needs to be implemented by any class inheriting from this base class, which should be all of your HostedService's.
Here is an example HostedService implementation that inherits from this Base class designed to checkin every 30 seconds. You will notice that the ExecuteAsync() method enters into a while loop and never exits - it will 'tick' once every second, and this is where you can invoke other methods such as checking in to another server on some regular interval. All of the code in this loop is returned in the Task to StartAsync() and returned to the caller. The task will not die until the while loop exits or the application dies, or the cancellation token is triggered.
public class CounterHostedService : HostedService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILog _logger;
public CounterHostedService(IServiceScopeFactory scopeFactory, ILog logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
// Checkin every 30 seconds
private int CheckinFrequency = 30;
private DateTime CheckedIn;
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
int counter = 0;
var runningTasks = new List<Task>();
while (true)
{
// This loop will run for the lifetime of the application.
// Time since last checkin is checked every tick. If time since last exceeds the frequency, we perform the action without breaking the execution of our main Task
var timeSinceCheckin = (DateTime.UtcNow - CheckedIn).TotalSeconds;
if (timeSinceCheckin > CheckinFrequency)
{
var checkinTask = Checkin();
runningTasks.Add(checkinTask);
}
try
{
// The loop will 'tick' every second.
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
catch (TaskCanceledException)
{
// Break out of the long running task because the Task was cancelled externally
break;
}
counter++;
}
}
// Custom override of StopAsync. This is only triggered when the application
// GRACEFULLY shuts down. If it is not graceful, this code will not execute. Neither will the code for StopAsync in the base method.
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.Info($"HostedService Gracefully Shutting down");
// Perform base StopAsync
await base.StopAsync(cancellationToken);
}
// Creates a task that performs a checkin, and returns the running task
private Task Checkin()
{
return Task.Run(async () =>
{
// await DoTheThingThatWillCheckin();
});
}
}
Notice you can also override the StopAsync() method to do some logging, and anything else needed for your shutdown events. Try to avoid critical logic in StopAsync, as its not guaranteed to be called.