As a developer that’s seen most of the iterations of ASP.NET throughout their career, ASP.NET Core reduces the complexity of hosting a web application while giving developer’s the most power they’ve ever had. We can host ASP.NET Core applications within console applications, giving us some new and exciting scenarios.

This post will show how to implement a BackgroundService within an ASP.NET Core application and communicate with our background service from an ASP.NET Core HTTP request.

What’s A Background Service

These days, .NET ships with the IHostBuilder interface, allowing even the most straightforward console applications to have powerful features like dependency injection, logging, and more as part of a unified integration. Folks working with ASP.NET Core will likely be familiar with the following code found in their web application’s Program file.

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    private static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

A background service is a specific type that runs asynchronously within a console application host with the idea it does not interfere with the primary process. ASP.NET Developers might not realize that the ASP.NET Host is a background service, precisely the GenericWebHostService type. As we may guess, the type’s responsibility is to start listening for incoming HTTP requests and process them through ASP.NET Core’s pipeline. So, we’ve learned about a background service we’re likely already using, but what about creating our own?

Background services start by implementing the IHostedService interface, but we can get a more advanced jump-start by implementing the class BackgroundService found under the Microsoft.Extensions.Hosting namespace. Let’s look at an empty implementation designed to run a recurring action.

public class Worker: BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // keep looping until we get a cancellation request
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // todo: Add repeating code here
                // add a delay to not run in a tight loop
                await Task.Delay(1000, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // catch the cancellation exception
                // to stop execution
                return;
            }
        }
    }
}

Essential elements of a background service are the ExecuteAsync method, which we must override. Since the execution is asynchronous, we get access to a CancellationToken, which will send a signal when the process is shutting down. It is also crucial that we use the token with all async operations to gracefully stop their actions. In our example, we are looping our code as long as the application is up and running.

In what specific use cases could we use a BackgroundService?

  • Sending emails outside of the web thread
  • Determining changes to stateful resources
  • Long-running tasks, such as processing data queues

In general, a BackgroundService instance is perfect for processing information that we can perform in parallel without a user’s direct interaction.

To register an instance of a BackgroundService, we need to modify the CreateHostBuilder method found in Program.

private static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

We’ll notice that we’ve added a new call to ConfigureServices at the end of CreateDefaultBuilder. We can add our new Worker type within this builder call by invoking AddHostedService. All background services are registered as Singelton and resolved through the IHostedService interface. We’ll need to remember that for the next section.

Getting At The Background Service From ASP.NET Core

While a BackgroundService runs asynchronously in the background, it doesn’t mean we can’t access it from an ASP.NET Core request thread. Since all our background services, including ASP.NET Core’s service, run within the same host, they also share the same IServiceCollection.

Let’s write a new Worker that will store user emojis as they come in via an HTTP Request and print them to the console output.

public class Worker : BackgroundService
{
    private static List<string> Emojis { get; } = new() { "😀" };

    public string Html => string.Join(
        string.Empty, 
        Emojis.Select(e => HtmlEncoder.Default.Encode(e))
    );

    private string Output =>
        string.Join(string.Empty, Emojis);
    
    public void AddEmoji(string emoji) => Emojis.Add(emoji);
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                Console.WriteLine($"{DateTime.Now:u}: Hello, {Output}");
                await Task.Delay(1000, stoppingToken);
            }
            catch (OperationCanceledException )
            {
                return;
            }
        }
    }
}

Remember that all background services are singleton, so they must manage their state and dependencies. We can pass in values via constructor injection, but we need to realize those dependencies will exist for the application’s lifespan. In this example, the Worker will manage a List<string> for all emojis. Please do not use this example in production, as the unbounded list will cause a memory leak.

Before we dive into our ASP.NET Core endpoints, let’s discuss how we’ll retrieve our background service from our IServiceCollection through the IServiceProvider interface. What do we know?

  • There are potentially multiple IHostedService instances
  • They are registered explicitly as IHostedService.
  • They are all singletons.

Knowing these facts, let’s write an extension method to retrieve our concrete type of Worker more pleasant.

public static class ServiceProviderExtensions
{
    public static TWorkerType GetHostedService<TWorkerType>
        (this IServiceProvider serviceProvider) =>
        serviceProvider
            .GetServices<IHostedService>()
            .OfType<TWorkerType>()
            .FirstOrDefault();
}

We need to retrieve all instances of IHostedService and then filter them down using the OfType LINQ extension method. Let’s write some ASP.NET Core endpoints.

endpoints.MapGet("/", async context =>
{
    var worker = context.RequestServices.GetHostedService<Worker>();
    context.Response.ContentType = "text/html";
    await context.Response.WriteAsync($"<h1>Hello {worker?.Html}!</h1>");
});

Here our GET endpoint retrieves our Worker and outputs the response to the client. What about accepting input from a user? Here we’ll implement a POST endpoint.

endpoints.MapPost("/", async context =>
{
    var emoji = context.Request.Form["emoji"].FirstOrDefault();
    if (emoji is not null)
    {
        var worker = context.RequestServices.GetHostedService<Worker>(); 
        if (worker is not null)
        {
            worker.AddEmoji(emoji);
            context.Response.StatusCode = (int) HttpStatusCode.NoContent;
            return;
        }
    }
    
    context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
});

Since our Worker is a singleton, we can add elements to the internalized list using the AddEmoji method. When building a background service like this, it would be better to use a more durable storage mechanism like SQL Server, PostgreSQL, or even SQLite. Let’s see the result from our browser.

Emoji from background service in HTML

What about our console output?

Emoji from background service in console

Let’s make a POST request to our other endpoint.

HTTP Client in JetBrains Rider calling POST endpoint

As we can see, both the HTML output and our console output have now changed. First, let’s look at the HTML output.

Emoji from background service in HTML updated

Now the updated console output.

Emoji from background service in console updated

Conclusion

The IHostedService interface is an integral part of the .NET development experience, whether we realize it or not. We can implement our own BackgroundService and register it alongside the ASP.NET Core host service. Knowing that our service shares the same resources as ASP.NET Core, we can access our types using a straightforward extension method to retrieve an exact instance. Putting this knowledge together, we can do some pretty cool stuff.

I hope you enjoyed this blog post and learned something new. I would appreciate you sharing this post or leaving a comment below. As always, thanks for reading.