Reputation: 7637
For my ASP.NET Core MVC application I need a controller action which processes data. This processing takes some time so I do not want to block the request. In the controller action I rather start a background worker and end the request immediately telling the user that the processing is in progress. A second controller action is then used to access the processed data.
In the background worker I need to access the DbContext
for storing the processed data in my database. (Or any other service which was injected via Dependency Injection.) I found that creating a new, request-independent scope via an IServiceScopeFactory
works which in turn gives me a ServiceProvider
:
public class ProcessingController : Controller
{
private readonly IServiceScopeFactory mServiceProvider;
public HomeController(IServiceScopeFactory serviceProvider)
{
mServiceProvider = serviceProvider;
}
public IActionResult BeginProcessing(int id)
{
var longRunningScope = mServiceProvider.CreateScope();
var _ = Task.Run(() => {
try {
var context = longRunningScope.ServiceProvider.GetRequiredService<DbContext>();
var workItem = context.Items.First(i => i.Id == id)
...
}
finally {
longRunningScope.Dispose();
}
});
return Ok();
}
}
Is there a better (more ASP.NET-Core-style) way to do this? Please note that my "long-running" action only takes 2-5 seconds and that multiple users need to be handled simultaneously. A background thread which processes requests in sequence is not wanted.
Upvotes: 1
Views: 684
Reputation: 7637
I determined that a hosted services is not well suited for my needs because it needs to be implemented explicitly and injected, making it hard to pass data to it.
Here is a more flexible and easier to use solution ScopedBackgroundTaskRunner
, which runs an action in its own task and in its own scope while listening to the shutdown event. The action receives the corresponding cancellation token as well as a scoped ServiceProvider
to obtain any needed services.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace WebApplication2.Support
{
/// <summary>
/// Service class that executes tasks which run in their own thread with their own scope
/// and can thus continue executing after the web request ended.
/// </summary>
/// <remarks>
/// Register via:
/// services.AddTransient<ScopedBackgroundTaskRunner>();
/// </remarks>
public class ScopedBackgroundTaskRunner
{
private readonly ILogger<ScopedBackgroundTaskRunner> mLogger;
private CancellationTokenSource mStoppingCts;
private IServiceProvider mServiceProvider;
public ScopedBackgroundTaskRunner(IServiceProvider services,
ILogger<ScopedBackgroundTaskRunner> logger,
IHostApplicationLifetime lifetime)
{
mServiceProvider = services;
mLogger = logger;
lifetime.ApplicationStopping.Register(OnAppStopping);
}
private void OnAppStopping()
{
if (mStoppingCts != null)
{
mLogger.LogDebug($"Cancel due to app shutdown");
mStoppingCts.Cancel();
}
}
public void Execute(Action<IServiceProvider, CancellationToken> action, CancellationToken stoppingToken) {
Execute(action, "<unnamed>", stoppingToken);
}
public void Execute(Action<IServiceProvider, CancellationToken> action, string actionName, CancellationToken stoppingToken)
{
mStoppingCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
var scope = mServiceProvider.CreateScope();
var _ = Task.Run(() => {
mLogger.LogTrace($"Executing action '{actionName}' on thread {Thread.CurrentThread.ManagedThreadId}...");
try
{
action.Invoke(scope.ServiceProvider, mStoppingCts.Token);
}
finally {
mLogger.LogTrace($"Action '{actionName}' {(mStoppingCts.IsCancellationRequested ? "canceled" : "finished")}" +
$" on thread {Thread.CurrentThread.ManagedThreadId}");
scope.Dispose();
var mStoppingCtsCopy = mStoppingCts;
mStoppingCts = null;
mStoppingCtsCopy.Dispose();
}
}, mStoppingCts.Token);
}
}
}
Upvotes: 0
Reputation: 150108
Consider implementing your background task as a hosted service.
With that approach, the lifetime of your background service is managed (the runtime will request cancellation via a CancellationToken
if the hosting environment is shutting down, whereas, in your existing code, your task would not be politely stopped).
Upvotes: 1