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.
What about our console output?
Let’s make a POST
request to our other endpoint.
As we can see, both the HTML output and our console output have now changed. First, let’s look at the HTML output.
Now the updated console output.
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.