Due to the emerging significance of running background tasks, .NET Core has given us new ways of achieving it. BackgroundService is another tool in our toolbox with methods ExecuteAsync() and StartAsync() being the central components.
In this article, we will compare these methods and demonstrate when and how to use them. We will first introduce BackgroundService, before diving into the details of each method.
Let’s begin!
Understanding BackgroundService
The BackgroundService
class belongs to the Microsoft.Extensions.Hosting
namespace that .NET provided, to facilitate the creation of long-running background tasks in our applications.
BackgroundService
gives us a structured way to implement background processing logic. It ensures proper lifecycle management and integration with the application’s host environment.
To create a task that runs in the background, we first create a new class that inherits from Microsoft.Extensions.Hosting.BackgroundService
. This is our background worker. Next, we override the default and abstract implementation of the core methods StartAsync()
, ExecuteAsync()
and StopAsync()
as per our needs. Finally, we register the new background worker with the services container.
BackgroundService
class in our article Different Ways to Run Background Tasks in ASP.NET Core which discusses it in great detail.Let’s now delve into the specifics of the methods ExecuteAsync()
and StartAsync()
.
The ExecuteAsync() Method
The ExecuteAsync()
method is perhaps the most important component of the BackgroundService
class. It is the method containing the logic of the long-running operation that the background worker executes. The method is invoked after the background worker has successfully started and performs all the operations required to get the job done.
One of the most common use cases of ExecuteAsync()
is to perform operations on a scheduled basis or as a response to events. The long-running nature of this method requires us to be extra diligent with resource management as any leaks can quickly build up.
Let’s now look at the definition of ExecuteAsync()
in .NET for a better understanding:
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
The ExecuteAsync()
method is abstract, meaning there is no default implementation in the BackgroundService
class. We have to provide our implementation depending on the intent of our worker. Also, we see that the method returns a Task
, so we understand that it is inherently asynchronous and we should avoid blocking code in its implementation.
Lastly, we notice that the method takes in a parameter of type CancellationToken. Therefore in our implementation of ExecuteAsync()
, we should check that no cancellation has been requested. We should pass the token into other asynchronous and cancellable operations that our logic will call and make sure that we can finish promptly when the cancellation token is fired.
Next, let’s examine the internal workings of the StartAsync()
method.
The StartAsync() Method
The StartAsync()
method is the initialization component of the BackgroundService
class. The runtime calls the method during the initialization of the background worker.
In contrast to ExecuteAsync()
method, which runs the long-running asynchronous operations, StartAsync()
aims to perform as quickly as possible all the set-up required (i.e. initialize dependencies) by the main processing and then exits. This also means that StartAsync()
should be lightweight to minimize startup times.
Let’s now look at the definition of StartAsync()
in .NET for a better understanding:
public virtual Task StartAsync(CancellationToken cancellationToken) { _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _executeTask = ExecuteAsync(_stoppingCts.Token); if (_executeTask.IsCompleted) { return _executeTask; } return Task.CompletedTask; }
Here, we see that StartAsync()
is a virtual method with an implementation that suits most use cases. We rarely need to override it and provide a different implementation. This is also why, although BackgroundService
with ExecuteAsync()
has been around since the early life of .NET Core, overriding StartAsync()
became available only with .NET 6.
Next, we see that StartAsync()
calls ExecuteAsync()
in its default implementation. This is the only way for the worker to call ExecuteAsync()
.
If we override StartAsync()
without calling ExecuteAsync()
, then the latter will never execute!
In addition, we see here that if the ExecuteAsync()
task has been completed, then the code exits by returning this task.
Lastly, we notice that StartAsync()
also takes in a parameter of type CancellationToken
, which we can use to handle cancellations gracefully.
Next, let’s see everything in action!
ExecuteAsync() and StartAsync() Methods in Action
Let’s now see the implementation of a background worker as a BackgroundService
that utilizes the methods ExecuteAsync()
and StartAsync()
.
We will build a worker that periodically calls an external StockPrices
API to retrieve and load the latest prices into an in-memory local database. The external StockPrice
API call is protected by a token we want to retrieve only once. Additionally, we will write an API that will retrieve the latest stock prices straight from our database.
Our model is the StockPrice
record and we will use it for both our database and external API calls:
public record StockPrice(Guid Id, string Symbol, decimal Price, DateTime ValueDateTime, DateTime MeasurementTimeStamp);
Let’s now look at the BackgroundService
implementation of our task:
public class StockPricesBackgroundService( IServiceProvider serviceProvider, IStockPriceService stockPriceService) : BackgroundService { private const int RunEveryMilliseconds = 10000; private ApiToken? _token = null; // ATTENTION // Credential and other sensitive information should ALWAYS be stored in storage // designed to store secrets (i.e Azure Key Vault, Hashicorp Vault, AWS Secrets Manager) private const string Username = "Secret1234!"; private const string Password = "Secret1234!"; public override async Task StartAsync(CancellationToken cancellationToken) { _token = await stockPriceService.AuthenticateAsync(Username, Password, cancellationToken); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using IServiceScope scope = serviceProvider.CreateScope(); var stockPriceRepository = scope.ServiceProvider.GetRequiredService<IStockPriceRepository>(); var prices = await stockPriceService.GetStockPricesAsync(_token!, stoppingToken); await stockPriceRepository.AddRangeAsync(prices, stoppingToken); await Task.Delay(RunEveryMilliseconds, stoppingToken); } } }
Here, we define the StockPricesBackgroundService
class and we implement both the StartAsync()
and ExecuteAsync()
methods.
We implement the StartAsync()
method to perform a one-off call to the API to authenticate and obtain a token. Following that, we call await base.StartAsync(cancellationToken)
to execute the base class’s implementation of the StartAsync()
method asynchronously.
ExecuteAsync()
is our long-running method which we use to fetch prices every 10 seconds. We retrieve new StockPrice
data and add them to our database. Notice here how we use the IServiceProvider
instance to resolve our repository from the dependency injection container. We do this trick because the StockPriceDbContext
has a scoped lifetime and StockPricesBackgroundService
is a singleton.
Resolving a scoped object within the context of a singleton is not permitted and we would get an exception!
Finally, we define a standard StockPriceController
controller with a GET
action to retrieve stock prices.
Let’s register our services in the Program
class:
builder.Services.AddDbContext<StockPricesDbContext>(options => options.UseInMemoryDatabase("StockPrices")); builder.Services.AddTransient<IStockPriceService, StockPriceService>(); builder.Services.AddScoped<IStockPriceRepository, StockPriceRepository>(); builder.Services.AddHostedService<StockPricesBackgroundService>();
Here, we register the StockPricesBackgroundService
class with the command with the AddHostedService()
method.
Observing the Effect of ExecuteAsync() in a Background Service
Now, let’s start the application. As the application starts, the BackgroundService
also begins its execution!
Next, let’s navigate to api/StockPrice
in the Swagger window and check out the output:
[ { "id": "ffc2f36f-dd0c-4782-bb3f-2c931846f2a3", "symbol": "MSFT", "price": 14.7052516685914, "valueDateTime": "2024-05-01T10:53:37.2982063Z", "measurementTimeStamp": "2024-05-01T10:53:36.7912319Z" }, { "id": "fc5dedfe-d3df-46f4-87ee-d6db6538c021", "symbol": "AAPL", "price": 40.9813619912797, "valueDateTime": "2024-05-01T10:53:37.2982087Z", "measurementTimeStamp": "2024-05-01T10:53:36.7912319Z" }, { "id": "e2e5b811-3874-408a-9fa6-2b0dcd9099c0", "symbol": "SAP", "price": 91.7500205684988, "valueDateTime": "2024-05-01T10:53:37.2982239Z", "measurementTimeStamp": "2024-05-01T10:53:36.7912319Z" } ]
We notice that the output changes between multiple invocations since our ExecuteAsync()
method keeps fetching new data in the background!
Conclusion
In this article, we’ve compared the ExecuteAsync() and StartAsync() methods of the BackgroundService class in .NET. We explained what a BackgroundService is and delved deeper into examining the uses of the ExecuteAsync() and StartAsync() methods. Finally, we built a .NET application that utilizes these methods to store and return up-to-date stock price data to our users.