Jack Miller
Jack Miller

Reputation: 7637

Use (DI injected) DbContext after request ended in ASP.NET Core

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

Answers (2)

Jack Miller
Jack Miller

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

Eric J.
Eric J.

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

Related Questions