Hosted services in asp.net core. Picture showing heavy load performed in a port to move containers around
5
(2)

If you’re working with ASP.Net Core chances are that sooner or later you’ll find yourself in a scenario where you would ask yourself “how could I run a background tasks in my ASP.Net Core app?” It doesn’t really matter if you’re working on an API or a powerful MVC application. It’s very often the case that your application will need to check for some updates in regular intervals, do some continuous processing outside of the MVC flow or simply perform some work when the web server fires up. Fortunately, we can easily run hosted services in ASP.Net Core, which are essentially background tasks.

Since I worked with hosted services in several occasions my goal with this article is to offer an overview on hosted services in ASP.Net Core and outline some aspects that you would probably need to take into consideration. If what you want is simply a “getting started”-style of article, the official Microsoft docs are better suited and you may want to return to this article later. So let’s get started!

Hosted Services in ASP.Net Core – short overview

A hosted service is a class with background task logic that implements the IHostedService interface. By implementing this interface you can create background tasks that run on a specific time interval, queued tasks that run sequentially or some continuous processing tasks. Implementing the interface is not very complicated since there are two methods that need to be implemented: StartAsync() and StopAsync(). As the name suggests, both are async methods so they simply need to return a Task. Here’s a very simple implementation of the StartAsync() method:

public Task StartAsync(CancellationToken cancellationToken)
{
    _timer = new Timer(SyncEmployees, null, TimeSpan.Zero,
        TimeSpan.FromSeconds(60));

    return Task.CompletedTask;
}

And for the StopAsync() method:

public Task StopAsync(CancellationToken cancellationToken)
{
    _timer?.Change(Timeout.Infinite, 0);

    return Task.CompletedTask;
}

Looking at the StartAsync() method it seems obvious that we’re creating a background task that will run in a regular time interval. Notice that when we’re creating the timer we simply specify the callback method as the first input parameter. This means that the callback method will be executed each time the timer reaches the defined point (each 60 seconds in this case). All the logic for the hosted services goes there.

Deeper Dive into Task Cancellation

At this point, one of the most important aspects to take a look at is how hosted services in ASP.Net Core work with Task cancellations. In other words, how hosted services in ASP.Net Core react to host stops and how we can leverage this as .NET Developers.

Generally cancellation tokens are used to indicate that a certain executing process was cancelled. This might be useful for classes that invoke some async methods. If the invoked async method is interrupted due to any reason, then the consuming class will want to react to these changes by performing some custom logic. Usually, we use cancellation tokens to just not wait for the completion of a certain task if that task was cancelled while executing. But you can implement more complex logic that would involve cleaning up unmanaged resources and so on.

When talking about hosted services in ASP.Net Core, cancellation tokens are used for the same purpose. To be more specific, the cancellation token  simply informs us that cancellation has been requested. Or, in other words, that the host is shutting down. If this is the case, we can implement logic in our hosted service to gracefully close connections or simply stop processing.

The cancellation token has a default five second timeout to indicate that the shutdown process should no longer be graceful. However, tasks aren’t abandoned after cancellation is requested. The caller awaits all tasks to complete. If the app shuts down unexpectedly (for example, the app’s process fails), StopAsync() might not be called. Therefore, any methods called or operations conducted in StopAsync() might not occur. This is something we need to be aware of!

It Can Get Even Easier Than This

Most of the hosted services that you’ll need to create have really similar cancellation needs. Therefore, meet the BackgroundService class in Microsoft.Extensions.Hosting. This class provides all the main work needed to set up the background task. It implements a standard cancellation token management logic and exposes an abstract method called ExecuteAsync() that you would need to implement in your own class.

So, if you want to run a hosted service you would simply need to create a class that inherits from the BackgroundService class and implement the ExecutAsync() method in your class. And that’s really it!

As a very short intermediary summary, if we want to run background tasks we have two options:

  1. Implement the IHostedService interface
  2. Inherit the BackgroundService abstract class and implement the ExecuteAsync() method

Even tough not complicated in itself, the first option is a little bit more complex since we would need to also implement our cancellation management processes. The second option is straightforward as the only thing you need to do is to provide the logic for your background task.

Using scoped services in a hosted service

In virtually all scenarios you will need to use services from the DI container. Transient and singleton services are really easy to use. You simply need to inject them in your hosted service class. Scoped services are, however, trickier. Why? Well, because scoped services are created on a per request basis. This means that as a request comes in to your application, ASP.Net core creates a scope for that specific request and resolves all scoped services. This is why you won’t have any problems to inject scoped services in your controllers.

Hosted services in ASP.Net Core on the other side are singleton services. Therefore, injecting scoped services into a hosted service means that you need to inject a scoped service into a singleton one. Obviously, this will throw an exception.

To overcome this difficulty you would need to create a scope in your hosted service. This is not difficult at all since you can inject the IServiceProvider in your hosted service. And you can use it to create a scope and resolve the necessary scoped services. Here’s a sample that illustrates the concept:

private async void SyncEmployees(object state)
{
    using (IServiceScopeFactory scope = _services.CreateScope())
    {
        _db = scope.ServiceProvider.GetRequiredService<IEmployeeDbProcessor>();
        _client = scope.ServiceProvider.GetRequiredService<IEmployeeApiClient>();
        _rabbit = scope.ServiceProvider.GetRequiredService<IMessagePublisher>();
        bool syncSuccess = await StartSynchronization();
    }
}

How hosted services might affect your app’s performance

We need to be aware that hosted services un in the same process as your web application. This means that your background tasks run in the same context and compete for the same resources your web application uses to service requests. In a scenario with very high load, where your application gets a lot of requests and where you have some heavy hosted services, the overall performance of your app will degrade.

That’s the point where the ubiquitous “it depends” comes in. If you find yourself in a situation like the one mentioned above, you will start to ask yourself: do I really need a background service? If you’re running your application in Microsoft Azure, you can take advantage of auto-scaling, but this would probably increase the overall costs of running your application.

Going serverless with Azure Functions or AWS Lambdas might be tempting in scenarios like this and you wouldn’t be in the wrong. If you have a lot of traffic on your web app and heavy background services, moving those services outside of the process that runs your web app is actually a good idea. And that’s what serverless gives you. Sure, running heavy job on serverless can become expensive as hell. That’s the point where you’d need to start do the math, lay down all the pros and the cons for each approach and choose what’s sensible for your scenario.

A possible approach for microservices setups

For a couple of months I am working on a microservices project. The cool thing is that we started it from scratch. So from a microservices point of view you’ll soon notice that you would need hosted services in a lot of different places. So initially we just started to create them where we needed. We soon realized that this approach might not be very helpful because as the project grows you loose the overview on what services are used and where.

The solution here would be to create a separate microservice or container only for the hosted services. Each hosted service would put the relevant information on the message bus and other services would consume that information as needed. By doing this you always know that all your hosted services are in the same place and it’s easier to maintain them. Separating the background tasks host from your regular API hosts would also take the performance burden away from the shoulders of your APIs. You obviously have the freedom to also scale them independently, no matter if you’re using the cloud provider’s scaling options or if you do this via your K8s orchestration.

That’s it! The blog post is surely longer than I would have expected but I think it contains all relevant information for those who are new to hosted services in ASP.Net Core. If you have other things to add, feel free to drop a comment.

Happy coding!

P.S: If you enjoyed this article, you might also want to check my YouTube channel for a lot more and visual .NET content. I also run a newsletter that’s different to any other newsletter as it focuses more on things that will help you become a better engineer, not just a better coder. You might want to subscribe to it as well!

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 2

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

By Dan Patrascu

I solve business problems through quality code, smart architecture, clever design and cloud sorcery. Over more than 8 years I've worked and led projects of all shapes and sizes: from regular monoliths to complex microservices and everything in between. I'm also running the Codewrinkles channel and a newsletter.