I have a small link shortener that I run for myself. I built it mostly so I could update short links when an URL changes (which bitly didn’t let me do). It’s been running for a few months and I’m really happy with it (GitHub Project). One of the things that I missed was tracking the usage of short links to see how they were being used.
I considered a number of ways to handle this, but I had one hard requirement, I didn’t want to slow down the redirection for some I/O process. After digging into it, I ended up with a solution: BackgroundService in ASP.NET Core. The BackgroundService class allows you to have code running in the background. In my case, I wanted to be able to queue up an item to store the tracking information that didn’t block the code that did the redirection.
So, first I needed a way to fill a queue with work. I could have just used the ThreadPool, but wanted something that was going to be a little safer (so I could control the lifetime). But first I needed a queue.
My first attempt was to simply create a shared Queue object but it wasn’t thread-safe. Digging in, I found the Channel class. This is a class that is specifically about sending messages in a thread-safe way. Before I could use the BackgroundService, I needed to implement a wrapper around the Channel class:
public class AccumulatorQueue : IAccumulatorQueue
{
private readonly Channel<Redirect> _queue;
private readonly ILogger<AccumulatorQueue> _logger;
public AccumulatorQueue(ILogger<AccumulatorQueue> logger)
{
var opts = new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.Wait };
_queue = Channel.CreateBounded<Redirect>(opts);
_logger = logger;
}
// ...
}
The idea here was to have a channel for my Redirect model class so that I could queue up messages that could be processed by the BackgroundService service. I added methods to just add/remove from the queue (this way I could Mock the interface if I needed):
public class AccumulatorQueue : IAccumulatorQueue
{
// ...
public async ValueTask PushAsync([NotNull] Redirect redirect)
{
await _queue.Writer.WriteAsync(redirect);
_logger.LogInformation("Added Redirect to Queue");
}
public async ValueTask<Redirect> PullAsync(CancellationToken cancellationToken)
{
var result = await _queue.Reader.ReadAsync(cancellationToken);
_logger.LogInformation("Removed Redirect from Queue");
return result;
}
}
With that in place, I needed to add the queue as a singleton in my service collection:
var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddSingleton<IAccumulatorQueue, AccumulatorQueue>();
In the project, I have a LinkManager class that handles the the short links and redirection. When redirection is about to happen, I just use the queue to add the item:
// ...
await _queue.PushAsync(new Redirect()
{
Key = key,
Destination = dest,
Referer = ctx.Request.Headers.Referer.FirstOrDefault(),
Origin = ctx.Request.Headers.Origin.FirstOrDefault(),
QueryString = ctx.Request.QueryString.Value,
Time = DateTime.UtcNow
});
This call should be really fast and allow the redirection to happen quickly without waiting for the actual storage of the information. That’s where the BackgroundService comes in.
The basis of the BackgroundService class is the IHostedService interface. This simple interface just has a StartAsync and StopAsync methods:
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
This interface allows you to register hosted services that .NET can start. You do this by just adding it to the service collection’s AddHostedService:
builder.Services.AddHostedService<YOURSERVICENAME>();
To make this work, I needed to implement my own BackgroundService class. The BackgroundService implements this interface and exposes two methods that you would override to handle these calls:
public abstract class BackgroundService : IHostedService, IDisposable
{
// ...
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken);
public virtual Task StopAsync(CancellationToken cancellationToken);
}
With that information, I was ready to implement my solution. I wasn’t sure what I wanted to do in the background service (call an Azure pub/sub solution, store it in my local storage, etc.), but I knew that I needed it to be handled in the background. So how did I accomplish this. I first sub-classed the BackgroundService (it’s an abstract class) to a class I called AccumulatorBackgroundService. I first injected the queue I built earlier:
public class AccumulatorBackgroundService : BackgroundService
{
private readonly IAccumulatorQueue _queue;
public AccumulatorBackgroundService(IAccumulatorQueue queue,
...)
{
_queue = queue;
// ...
}
With that in place, I had to first override the abstract ExecuteAsync method:
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var redirect = await _queue.PullAsync(stoppingToken);
await _repo.InsertRedirect(redirect);
}
catch (Exception ex)
{
_logger.LogError($"Failure while processing queue {ex}");
}
}
}
The ExecuteAsync is a long-running method and I can continue to loop until the stoppingToken is used to cancel the process. Inside that, I just call PullAsync from the queue. This process waits until a new item is added to the queue. Note we’re not polling but waiting on the channel to return with a result. Once that happens, I use a repository object to save the redirect object to the data store.
The only other thing this class needs to do is handle the stopping of the method. This is done by calling the base class (BackgroundService) method for StopAsync with the cancellation token. This is called by the run-time when it needs to shut down the hosted service:
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping Background Service");
await base.StopAsync(cancellationToken);
}
Now that we have all the code, we just need to register our queue and hosted service:
builder.Services.AddSingleton<IAccumulatorQueue, AccumulatorQueue>();
builder.Services.AddHostedService<AccumulatorBackgroundService>();
You can see the code in the GitHub repository if you want to play with it:
Thanks!
This work by Shawn Wildermuth is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.
Based on a work at wildermuth.com.
If you liked this article, see Shawn's courses on Pluralsight.
Top comments (0)