1

I'm using mvc with .net core, I need to run a task on start, and stop it when application stops. In Startup.cs I registered events for application start and stop. The problem is, I don't know how to run a task that has to be run in a specific class from startup. The task looks like this:

public void PreventStatusChange()
    {
        while (forceStatusChange)
        {
            foreach (var ext in GetExtensions())
            {
                ext.Status = StatusType.Available;
            }
            Thread.Sleep(1000);
        }
    }

Variable forceStatusChange is declared in the same class, so I don't see it from my Startup.cs. What is the best way to do it?

Jamil
  • 830
  • 2
  • 15
  • 34
  • 1
    Possible duplicate of [Asp.Net core long running task](https://stackoverflow.com/questions/45013054/asp-net-core-long-running-task) – alsami Aug 06 '18 at 10:28

2 Answers2

8

You need to create a class that implements IHostedService. This interface defines just two methods, StartAsync which is called when the application starts and StopAsync which is called when it terminates.

You need to register it as a hosted service with :

services.AddHostedService<TimedHostedService>();

Be careful to use AddHostedService, NOT AddSingleton. If you use AddSingleton the runtime won't know to call StartAsync and StopAsync when appropriate.

The article Background tasks with hosted services in ASP.NET Core shows how to implement a service using a timer :

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(10));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");
    }

    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();
    }
}

There's nothing particularly interesting in this code - just start a timer when StartAsync is called and stop it on StopAsync

Cancelling long-running tasks

When the runtime needs to recycle or stop, it will call the StopAsync method on all hosted services, wait a while for them to finish gracefully and then warn them to cancel immediatelly. After a short while it will go ahead and terminate the app or recycle it.

The cancellationToken parameter is used to signal that the service should stop immediatelly. Typically, that means that you'd have to write your own code to check it, warn your own task to terminate, wait for all of them to finish etc, similar to the code shown in this article

This is pretty much boilerplate though, which is why the BackgroundService class can be used to create a class that only needs to implement ExecuteAsync(CancellationToken). Starting and stopping that task is provided by BackgroundService, eg :

public class PollingService : BackgroundService
{

    private readonly ILogger _logger;

    public PollingService(ILogger<PollingService> logger)
    {
        _logger = logger;
    }

    protected async override Task ExecuteAsync(
        CancellationToken cancellationToken)
    {

        while (!cancellationToken.IsCancellationRequested)
        {

            try
            {
                await DoSomething(cancellationToken);
                await Task.Delay(1000,cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                   $"Error occurred executing {nameof(workItem)}.");
            }
        }

        _logger.LogInformation("Queued Hosted Service is stopping.");
    }        
}

In this case Task.Delay() itself will be cancelled as soon as the runtime raises the cancellation token. DoSomething() itself should be implemented in a way that checks the cancellation token, eg pass it to any asynchronous method that accepts it as a parameter, test the IsCancellationRequested property on every loop and exit it it's raised.

For example :

    protected async override Task ExecuteAsync(
        CancellationToken cancellationToken)
    {

        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                foreach (var ext in GetExtensions())
                {
                    //Oops, time to cancel
                    if(cancellationToken.IsCancellationRequested)
                    {
                        break;
                    }
                    //Otherwise, keep working
                    ext.Status = StatusType.Available;
                }
                await Task.Delay(1000,cancellationToken);
            }
            catch (Exception ex)
            {
                ...
            }
        }

        _logger.LogInformation("Hosted Service is stopping.");
    }        
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thank you for very informative answer. Is there any way to do it with AddSingleton? Because I need that service as a singleton. – Jamil Aug 06 '18 at 11:48
  • @Jamil Why? I already explained one reason why you *can't*, the linked article explains more. Check Phil Haack's [The dangers of implementing recurring tasks in ASP.NET](https://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx/). Scott Hanselman explains [various methods to properly execute recurring tasks](https://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx). Hosted services are a new way to do the same in .NET Core – Panagiotis Kanavos Aug 06 '18 at 12:01
  • @Jamil do you want to post data to the service perhaps? Check the [QueuedHostedService](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#queued-background-tasks) example in the docs. Instead of injecting the hosted service, it creates and injects a *singleton queue* that can be used to post data to the service. This way clients don't need to know whether the service is running or stopping. – Panagiotis Kanavos Aug 06 '18 at 12:04
  • No, this service runs in background and processes incoming/outgoing calls and logs data about them, that's why I need a singleton instance. – Jamil Aug 06 '18 at 12:17
  • @Jamil that's the job of a filter and a logger, not a long-running task. You still don't need a singleton instance though, unless you wanted to inject it as a dependency – Panagiotis Kanavos Aug 06 '18 at 12:19
  • Info about calls comes from 3rd party, after that I can call logger. This service is mainly a wrapper for the 3rd party voip soft. So, if this solution won't work with singleton, is there any way to do it? I'm not 100% sure that I need singleton there, but it looks like that. – Jamil Aug 06 '18 at 12:25
1

You can use BackgroundService

public class LongRunningService : BackgroundService
{ 
    public LongRunningService()
    {
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested && forceStatusChange)
        {
            foreach (var ext in GetExtensions())
            {
                ext.Status = StatusType.Available;
            }

            await Task.Delay(1000, stoppingToken);
        }  
    }

    protected override async Task StopAsync (CancellationToken stoppingToken)
    {
        // Run your graceful clean-up actions
    }
}

And register it:

public void ConfigureServices(IServiceCollection services)
{
   ...
   services.AddSingleton<IHostedService, LongRunningService>();
   ...
}
Alex Riabov
  • 8,655
  • 5
  • 47
  • 48
  • The problem is that I already have an interface for my service: .AddSingleton() Is it mandatory to set service type IHostedService? – Jamil Aug 06 '18 at 11:44
  • @Jamil, you can inject it to constructor of BackgroundService, though your service should be slightly modified – Alex Riabov Aug 06 '18 at 11:51
  • @AlexRiabov that's not how hosted services work. Without the call to `AddHostedService` this is just another singleton. – Panagiotis Kanavos Aug 06 '18 at 11:57
  • @PanagiotisKanavos not according to this article https://blogs.msdn.microsoft.com/cesardelatorre/2017/11/18/implementing-background-tasks-in-microservices-with-ihostedservice-and-the-backgroundservice-class-net-core-2-x/ – Alex Riabov Aug 06 '18 at 12:13
  • @AlexRiabov that's the article I linked to and you misunderstood what it shows. It shows how to *replicate* the same functionality in your own code. The *DI infrastructure* still has to call `StartAsync`, `StopAsync`, which it can't do if it doesn't know about the IHostedService interface – Panagiotis Kanavos Aug 06 '18 at 12:13
  • @PanagiotisKanavos, `AddHostedServices` just registers `IHostedService` as Singleton: https://github.com/dotnet-architecture/Messaging/blob/5c337696349679136ebdd6b6fd425535fa78b25a/src/Microsoft.Extensions.Hosting/ServiceCollectionExtensions.cs – Alex Riabov Aug 06 '18 at 12:20
  • @AlexRiabov a more recent article would be [Background tasks with hosted services in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1). In the last year the classes and extensions were added to .NET Core itself and the `Microsoft.Extensions.*` packages. – Panagiotis Kanavos Aug 06 '18 at 12:20
  • @AlexRiabov that's *NOT* the implementation. That's source code from a samples repo. Actually, a *branch* from a samples repo – Panagiotis Kanavos Aug 06 '18 at 12:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/177492/discussion-between-alex-riabov-and-panagiotis-kanavos). – Alex Riabov Aug 06 '18 at 12:22
  • @AlexRiabov AHA! Execution is handled by the [HostedServiceExecutor](https://github.com/aspnet/Hosting/blob/master/src/Microsoft.AspNetCore.Hosting/Internal/HostedServiceExecutor.cs) class, which accepts *all* registered IHostedServices as a dependency. That's why `AddHostedService` registers a transient - the executor is a singleton and so are all its dependencies. – Panagiotis Kanavos Aug 06 '18 at 12:37
  • @AlexRiabov There was a [PR in May](https://github.com/aspnet/Hosting/pull/1405) that explicilty introduced `AddHostedService` and replaced calls to `AddSingleton` and `AddTransient` in the samples, they updated the docs but not earlier guides. I suspect they moved to Transient to ensure the lifetime is controlled by the HostedServiceExecutor, rather than individual registrations – Panagiotis Kanavos Aug 06 '18 at 12:43