Alternative solution to HostingEnvironment.QueueBackgroundWorkItem in .NET Core - c#

We are working with .NET Core Web Api, and looking for a lightweight solution to log requests with variable intensity into database, but don't want client's to wait for the saving process.
Unfortunately there's no HostingEnvironment.QueueBackgroundWorkItem(..) implemented in dnx, and Task.Run(..) is not safe.
Is there any elegant solution?

As #axelheer mentioned IHostedService is the way to go in .NET Core 2.0 and above.
I needed a lightweight like for like ASP.NET Core replacement for HostingEnvironment.QueueBackgroundWorkItem, so I wrote DalSoft.Hosting.BackgroundQueue which uses.NET Core's 2.0 IHostedService.
PM> Install-Package DalSoft.Hosting.BackgroundQueue
In your ASP.NET Core Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddBackgroundQueue(onException:exception =>
{
});
}
To queue a background Task just add BackgroundQueue to your controller's constructor and call Enqueue.
public EmailController(BackgroundQueue backgroundQueue)
{
_backgroundQueue = backgroundQueue;
}
[HttpPost, Route("/")]
public IActionResult SendEmail([FromBody]emailRequest)
{
_backgroundQueue.Enqueue(async cancellationToken =>
{
await _smtp.SendMailAsync(emailRequest.From, emailRequest.To, request.Body);
});
return Ok();
}

QueueBackgroundWorkItem is gone, but we've got IApplicationLifetime instead of IRegisteredObject, which is being used by the former one. And it looks quite promising for such scenarios, I think.
The idea (and I'm still not quite sure, if it's a pretty bad one; thus, beware!) is to register a singleton, which spawns and observes new tasks. Within that singleton we can furthermore register a "stopped event" in order to proper await still running tasks.
This "concept" could be used for short running stuff like logging, mail sending, and the like. Things, that should not take much time, but would produce unnecessary delays for the current request.
public class BackgroundPool
{
protected ILogger<BackgroundPool> Logger { get; }
public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
{
if (logger == null)
throw new ArgumentNullException(nameof(logger));
if (lifetime == null)
throw new ArgumentNullException(nameof(lifetime));
lifetime.ApplicationStopped.Register(() =>
{
lock (currentTasksLock)
{
Task.WaitAll(currentTasks.ToArray());
}
logger.LogInformation(BackgroundEvents.Close, "Background pool closed.");
});
Logger = logger;
}
private readonly object currentTasksLock = new object();
private readonly List<Task> currentTasks = new List<Task>();
public void SendStuff(Stuff whatever)
{
var task = Task.Run(async () =>
{
Logger.LogInformation(BackgroundEvents.Send, "Sending stuff...");
try
{
// do THE stuff
Logger.LogInformation(BackgroundEvents.SendDone, "Send stuff returns.");
}
catch (Exception ex)
{
Logger.LogError(BackgroundEvents.SendFail, ex, "Send stuff failed.");
}
});
lock (currentTasksLock)
{
currentTasks.Add(task);
currentTasks.RemoveAll(t => t.IsCompleted);
}
}
}
Such a BackgroundPool should be registered as a singleton and can be used by any other component via DI. I'm currently using it for sending mails and it works fine (tested mail sending during app shutdown too).
Note: accessing stuff like the current HttpContext within the background task should not work. The old solution uses UnsafeQueueUserWorkItem to prohibit that anyway.
What do you think?
Update:
With ASP.NET Core 2.0 there's new stuff for background tasks, which get's better with ASP.NET Core 2.1: Implementing background tasks in .NET Core 2.x webapps or microservices with IHostedService and the BackgroundService class

You can use Hangfire (http://hangfire.io/) for background jobs in .NET Core.
For example :
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));

Here is a tweaked version of Axel's answer that lets you pass in delegates and does more aggressive cleanup of completed tasks.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
namespace Example
{
public class BackgroundPool
{
private readonly ILogger<BackgroundPool> _logger;
private readonly IApplicationLifetime _lifetime;
private readonly object _currentTasksLock = new object();
private readonly List<Task> _currentTasks = new List<Task>();
public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
{
if (logger == null)
throw new ArgumentNullException(nameof(logger));
if (lifetime == null)
throw new ArgumentNullException(nameof(lifetime));
_logger = logger;
_lifetime = lifetime;
_lifetime.ApplicationStopped.Register(() =>
{
lock (_currentTasksLock)
{
Task.WaitAll(_currentTasks.ToArray());
}
_logger.LogInformation("Background pool closed.");
});
}
public void QueueBackgroundWork(Action action)
{
#pragma warning disable 1998
async Task Wrapper() => action();
#pragma warning restore 1998
QueueBackgroundWork(Wrapper);
}
public void QueueBackgroundWork(Func<Task> func)
{
var task = Task.Run(async () =>
{
_logger.LogTrace("Queuing background work.");
try
{
await func();
_logger.LogTrace("Background work returns.");
}
catch (Exception ex)
{
_logger.LogError(ex.HResult, ex, "Background work failed.");
}
}, _lifetime.ApplicationStopped);
lock (_currentTasksLock)
{
_currentTasks.Add(task);
}
task.ContinueWith(CleanupOnComplete, _lifetime.ApplicationStopping);
}
private void CleanupOnComplete(Task oldTask)
{
lock (_currentTasksLock)
{
_currentTasks.Remove(oldTask);
}
}
}
}

I know this is a little late, but we just ran into this issue too. So after reading lots of ideas, here's the solution we came up with.
/// <summary>
/// Defines a simple interface for scheduling background tasks. Useful for UnitTesting ASP.net code
/// </summary>
public interface ITaskScheduler
{
/// <summary>
/// Schedules a task which can run in the background, independent of any request.
/// </summary>
/// <param name="workItem">A unit of execution.</param>
[SecurityPermission(SecurityAction.LinkDemand, Unrestricted = true)]
void QueueBackgroundWorkItem(Action<CancellationToken> workItem);
/// <summary>
/// Schedules a task which can run in the background, independent of any request.
/// </summary>
/// <param name="workItem">A unit of execution.</param>
[SecurityPermission(SecurityAction.LinkDemand, Unrestricted = true)]
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
}
public class BackgroundTaskScheduler : BackgroundService, ITaskScheduler
{
public BackgroundTaskScheduler(ILogger<BackgroundTaskScheduler> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogTrace("BackgroundTaskScheduler Service started.");
_stoppingToken = stoppingToken;
_isRunning = true;
try
{
await Task.Delay(-1, stoppingToken);
}
catch (TaskCanceledException)
{
}
finally
{
_isRunning = false;
_logger.LogTrace("BackgroundTaskScheduler Service stopped.");
}
}
public void QueueBackgroundWorkItem(Action<CancellationToken> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
if (!_isRunning)
throw new Exception("BackgroundTaskScheduler is not running.");
_ = Task.Run(() => workItem(_stoppingToken), _stoppingToken);
}
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
if (!_isRunning)
throw new Exception("BackgroundTaskScheduler is not running.");
_ = Task.Run(async () =>
{
try
{
await workItem(_stoppingToken);
}
catch (Exception e)
{
_logger.LogError(e, "When executing background task.");
throw;
}
}, _stoppingToken);
}
private readonly ILogger _logger;
private volatile bool _isRunning;
private CancellationToken _stoppingToken;
}
The ITaskScheduler (which we already defined in our old ASP.NET client code for UTest test purposes) allows a client to add a background task. The main purpose of the BackgroundTaskScheduler is to capture the stop cancellation token (which is own by the Host) and to pass it into all the background Tasks; which by definition, runs in the System.Threading.ThreadPool so there is no need to create our own.
To configure Hosted Services properly see this post.
Enjoy!

I have used Quartz.NET (does not require SQL Server) with the following extension method to easily set up and run a job:
public static class QuartzUtils
{
public static async Task<JobKey> CreateSingleJob<JOB>(this IScheduler scheduler,
string jobName, object data) where JOB : IJob
{
var jm = new JobDataMap { { "data", data } };
var jobKey = new JobKey(jobName);
await scheduler.ScheduleJob(
JobBuilder.Create<JOB>()
.WithIdentity(jobKey)
.Build(),
TriggerBuilder.Create()
.WithIdentity(jobName)
.UsingJobData(jm)
.StartNow()
.Build());
return jobKey;
}
}
Data is passed as an object that must be serializable. Create an IJob that processes the job like this:
public class MyJobAsync :IJob
{
public async Task Execute(IJobExecutionContext context)
{
var data = (MyDataType)context.MergedJobDataMap["data"];
....
Execute like this:
await SchedulerInstance.CreateSingleJob<MyJobAsync>("JobTitle 123", myData);

The original HostingEnvironment.QueueBackgroundWorkItem was a one-liner and very convenient to use.
The "new" way of doing this in ASP Core 2.x requires reading pages of cryptic documentation and writing considerable amount of code.
To avoid this you can use the following alternative method
public static ConcurrentBag<Boolean> bs = new ConcurrentBag<Boolean>();
[HttpPost("/save")]
public async Task<IActionResult> SaveAsync(dynamic postData)
{
var id = (String)postData.id;
Task.Run(() =>
{
bs.Add(Create(id));
});
return new OkResult();
}
private Boolean Create(String id)
{
/// do work
return true;
}
The static ConcurrentBag<Boolean> bs will hold a reference to the object, this will prevent garbage collector from collecting the task after the controller returns.

Related

ASP.NET BackgroundService - Only run sequential

I have a BackgroundService that I start from an API Controller.
There should never be more than one BackgroundService running.
How can I check if a job is already running? So I don't start a new?
API to start a new job and related code
[HttpPost]
public async Task<IActionResult> RunJob(JobMessage msg)
{
if (_queue.Count > 0)
{
return StatusCode(429, "DocumentDistributor are running. Try again later");
}
await _queue.Queue(msg);
return Ok("DocumentDistributor will start in about one minute.");
}
public interface IBackgroundTaskQueue
{
Task Queue(JobMessage message);
Task<JobMessage> Dequeue();
public int Count { get; }
}
public sealed class QueuedHostedService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public QueuedHostedService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _serviceProvider.CreateScope();
var calculator = scope.ServiceProvider.GetRequiredService<QueueDocumentDistributor>();
await calculator.RunService();
}
catch (OperationCanceledException)
{
// Prevent throwing if the Delay is cancelled
}
catch (Exception e)
{
Log.Error(e, "Error in QueuedHostedService");
}
// check queue every 1 minute
await Task.Delay(1000 * 60, stoppingToken);
}
}
}
public class QueueDocumentDistributor
{
private readonly IBackgroundTaskQueue _queue;
private readonly ReportService _service;
public QueueDocumentDistributor(IBackgroundTaskQueue queue, ReportService service)
{
_queue = queue;
_service = service;
}
public async Task RunService()
{
var message = await _queue.Dequeue();
if (message == null) return;
await _service.CreateReports(message);
}
}
AddHostedService adds singleton instance of IHostedService, so if there is no parallel processing in the implementation framework guarantees the single job execution.
There should never be more than one BackgroundService running.
There will be only single instance of background service per type running.
How can I check if a job is already running?
Depends on what do you mean by "job". If BackgroundService - then it is started by the framework. If your some custom payload in queue - then you will need to implement some monitoring manually.
So I don't start a new?
You don't start (usually) background service manually. If QueueDocumentDistributor.RunService gurantees single execution of your logic at a time - your are fine.
Based on provided implementation looks like a single queue element is processed at a time.
Read more:
Background tasks with hosted services in ASP.NET Core

How to make a webApi a Windows service by adding Start, Stop and Restart event handling in .Net 6?

How to make a webApi a Windows service by adding Start, Stop and Restart event handling in .Net 6?
In .Net 5 and before that, I usually make an AppStartup like this:
public interface IAppStartup
{
Task StartAsync();
Task StopAsync();
Task RestartService(IHostedService hostedService);
}
public class AppStartup : IAppStartup
{
private readonly string[] _args;
private readonly CancellationTokenSource _cancellationTokenSource = new();
private IHost _host;
private static AppStartup _appStartup;
private AppStartup(string[] args)
{
_args = args;
}
public async Task StartAsync()
{
try
{
_host = CreateHostBuilder(_args).Build();
var setting = _host.Services.GetService<AppSettings>();
if (setting != null)
{
///
}
await _host.RunAsync(_cancellationTokenSource.Token);
}
catch (Exception e)
{
///
throw;
}
}
public async Task StopAsync()
{
try
{
await _host.StopAsync(_cancellationTokenSource.Token);
}
catch (Exception e)
{
///
throw;
}
}
public async Task RestartService(IHostedService hostedService)
{
if (hostedService!=null)
{
await hostedService.StopAsync(_cancellationTokenSource.Token);
await hostedService.StartAsync(_cancellationTokenSource.Token);
}
}
}
Now in .Net 6, I am confused because there is no Startup method anymore to call.
Please kindly advise me of any solution you use to add Windows service functionalities into a web API project.
Both .NET 5 and .NET 6 have support for the Windows Service lifetime, which hooks into Start/Stop for you. So it's essentially just this in your Program.cs:
host = CreateHostBuilder(args)
.UseWindowsService(...)
.Build();
var setting = host.Services.GetService<AppSettings>();
if (setting != null)
{
///
}
await host.RunAsync();

ASP .Net Core Queued background tasks parallel processing

I have an ASP .NET core Web API which uses Queued background tasks like described
here.
I've used the code sample provided and added the IBackgroundTaskQueue, BackgroundTaskQueue and QueuedHostedService exactly as described in the article.
In my Startup.cs, I'm registering only one QueuedHostedService instance as follows: services.AddHostedService<QueuedHostedService>();
Tasks coming from the WebApi's controller are enqueued and then dequeued and executed one by one by the QueuedHostedService.
I'll would like to allow more than one background processing thread that will dequeue and execute the incoming Tasks.
The most straight forward solution i can come up with is to register more than one instance of the QueuedHostedService in my Startup.cs. i.e, something like this:
int maxNumOfParallelOperations;
var isValid = int.TryParse(Configuration["App:MaxNumOfParallelOperations"], out maxNumOfParallelOperations);
maxNumOfParallelOperations = isValid && maxNumOfParallelOperations > 0 ? maxNumOfParallelOperations : 2;
for (int index = 0; index < maxNumOfParallelOperations; index++)
{
services.AddHostedService<QueuedHostedService>();
}
I've also noticed that thanks to the singal Semaphore in BackgroundTaskQueue, the QueuedHostedService instances are not really working all the time, but only awaken when a new Task is available in the queue.
This solution seems to works just fine in my tests.
But, In this particular use case - is it really a valid, recommended solution for parallel processing?
You can use an IHostedService with a number of threads to consume the IBackgroundTaskQueue.
Here is a basic implementation. I assume you're using the same IBackgroundTaskQueue and BackgroundTaskQueue described here.
public class QueuedHostedService : IHostedService
{
private readonly ILogger _logger;
private readonly Task[] _executors;
private readonly int _executorsCount = 2; //--default value: 2
private CancellationTokenSource _tokenSource;
public IBackgroundTaskQueue TaskQueue { get; }
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
ILoggerFactory loggerFactory,
IConfiguration configuration)
{
TaskQueue = taskQueue;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
if (ushort.TryParse(configuration["App:MaxNumOfParallelOperations"], out var ct))
{
_executorsCount = ct;
}
_executors = new Task[_executorsCount];
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Queued Hosted Service is starting.");
_tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
for (var i = 0; i < _executorsCount; i++)
{
var executorTask = new Task(
async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
#if DEBUG
_logger.LogInformation("Waiting background task...");
#endif
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
#if DEBUG
_logger.LogInformation("Got background task, executing...");
#endif
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem)
);
}
}
}, _tokenSource.Token);
_executors[i] = executorTask;
executorTask.Start();
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Queued Hosted Service is stopping.");
_tokenSource.Cancel(); // send the cancellation signal
if (_executors != null)
{
// wait for _executors completion
Task.WaitAll(_executors, cancellationToken);
}
return Task.CompletedTask;
}
}
You need to register the services in ConfigureServices on Startup class.
...
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
services.AddHostedService<QueuedHostedService>();
...
Aditionally, you can set the number of threads in configuration (appsettings.json)
...
"App": {
"MaxNumOfParallelOperations": 4
}
...

async void work around when 3rd party library uses it

Looking for help after searching has not produced a good suggestion.
I always avoid having async void methods in code. I don't use event handlers. Sometimes a vendor or library gives you no choice, and their methods are implemented as async void.
If my method itself returns Task, but i have no choice but to call a 3rd party library method with async void, is there a way to safely wrap their method in such a way that I can keep my code free of the async void dangers, as listed here about terminating my process?
StackOverflow - why is async void bad
An example of my concern is as follows:
3rd party library method looks like this
public async void GetSomethingFromAService()
{
/// their implementation, and somewhere along the way it throws an exception, in this async void method --- yuck for me
}
My method say on a service controller:
public async Task<bool> MyMethod()
{
await ThirdPartyLibrary.GetSomethingFromAService();
return await Task.FromResult(true);
}
My method is fine except the 3rd party library is async void and throws an exception. My app is going to die. I don't want it to because my code is well written an not async void. But I can't control their code. Can i wrap the call to their async void method in such a way to protect my code from dying?
It's tricky and it might not work for all scenarios, but it may be possible to track the life-time of an async void method, by starting its execution on a custom synchronization context. In this case, SynchronizationContext.OperationStarted / SynchronizationContext.OperationCompleted will be called upon start and end of the asynchronous void method, correspondingly.
In case an exception is thrown inside an async void method, it will be caught and re-thrown via SynchronizationContext.Post. Thus, it's also possible to collect all exceptions.
Below is an a complete console app example illustrating this approach, loosely based on Stephen Toub's AsyncPump (warning: only slightly tested):
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncVoidTest
{
class Program
{
static async void GetSomethingFromAService()
{
await Task.Delay(2000);
throw new InvalidOperationException(nameof(GetSomethingFromAService));
}
static async Task<int> MyMethodAsync()
{
// call an ill-designed 3rd party async void method
// and await its completion
var pump = new PumpingContext();
var startingTask = pump.Run(GetSomethingFromAService);
await Task.WhenAll(startingTask, pump.CompletionTask);
return 42;
}
static async Task Main(string[] args)
{
try
{
await MyMethodAsync();
}
catch (Exception ex)
{
// this will catch the exception thrown from GetSomethingFromAService
Console.WriteLine(ex);
}
}
}
/// <summary>
/// PumpingContext, based on Stephen Toub's AsyncPump
/// https://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx
/// https://stackoverflow.com/q/49921403/1768303
/// </summary>
internal class PumpingContext : SynchronizationContext
{
private int _pendingOps = 0;
private readonly BlockingCollection<ValueTuple<SendOrPostCallback, object>> _callbacks =
new BlockingCollection<ValueTuple<SendOrPostCallback, object>>();
private readonly List<Exception> _exceptions = new List<Exception>();
private TaskScheduler TaskScheduler { get; }
public Task CompletionTask { get; }
public PumpingContext(CancellationToken token = default(CancellationToken))
{
var taskSchedulerTcs = new TaskCompletionSource<TaskScheduler>();
this.CompletionTask = Task.Run(() =>
{
SynchronizationContext.SetSynchronizationContext(this);
taskSchedulerTcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
try
{
// run a short-lived callback pumping loop on a pool thread
foreach (var callback in _callbacks.GetConsumingEnumerable(token))
{
try
{
callback.Item1.Invoke(callback.Item2);
}
catch (Exception ex)
{
_exceptions.Add(ex);
}
}
}
catch (Exception ex)
{
_exceptions.Add(ex);
}
finally
{
SynchronizationContext.SetSynchronizationContext(null);
}
if (_exceptions.Any())
{
throw new AggregateException(_exceptions);
}
}, token);
this.TaskScheduler = taskSchedulerTcs.Task.GetAwaiter().GetResult();
}
public Task Run(
Action voidFunc,
CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(() =>
{
OperationStarted();
try
{
voidFunc();
}
finally
{
OperationCompleted();
}
}, token, TaskCreationOptions.None, this.TaskScheduler);
}
public Task<TResult> Run<TResult>(
Func<Task<TResult>> taskFunc,
CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew<Task<TResult>>(async () =>
{
OperationStarted();
try
{
return await taskFunc();
}
finally
{
OperationCompleted();
}
}, token, TaskCreationOptions.None, this.TaskScheduler).Unwrap();
}
// SynchronizationContext methods
public override SynchronizationContext CreateCopy()
{
return this;
}
public override void OperationStarted()
{
// called when async void method is invoked
Interlocked.Increment(ref _pendingOps);
}
public override void OperationCompleted()
{
// called when async void method completes
if (Interlocked.Decrement(ref _pendingOps) == 0)
{
_callbacks.CompleteAdding();
}
}
public override void Post(SendOrPostCallback d, object state)
{
_callbacks.Add((d, state));
}
public override void Send(SendOrPostCallback d, object state)
{
throw new NotImplementedException(nameof(Send));
}
}
}

Autofac not working with background tasks

I have a task that needs to be run in a separate thread in the background, and I am using SignalR to report progress. This worked some time ago, and I had made some code modifications, but I am at a complete loss as to the error I receive now:
"No scope with a Tag matching 'AutofacWebRequest' is visible from the scope in which the instance was requested. This generally indicates that a component registered as per-HTTP request is being requested by a SingleInstance() component (or a similar scenario.) Under the web integration always request dependencies from the DependencyResolver.Current or ILifetimeScopeProvider.RequestLifetime, never from the container itself."
Any help is greatly appreciated!
public ActionResult DoAction(IEnumerable<string> items){
//...
Func<CancellationToken, Task> taskFunc = CancellationToken => performAction(items);
HostingEnvironment.QueueBackgroundWorkItem(taskFunc);
//...
}
private async Task performAction(IEnumerable<string> items){
var svc = AutofacDependencyResolver.Current.AppicationContainer.BeginLifetimeScope().Resolve<MyService>();
svc.Method(items);
}
public class MyService{
private EntityContext db;
public MyService(EntityContext db){
this.db = db;
}
}
In my Startup.Container.cs file:
builder.RegisterType<MyService>().As<MyService>().InstancePerLifetimeScope();
builder.RegisterType<EntityContext>().InstancePerRequest();
I recently implemented something similar using help from this answer and this answer. You need to create a new lifetime scope - it sounds like your doing this in a web application, so you need to create the scope via the per-request tag (example below).
Another (non-StackOverflow) answer provides similar advice.
public Task Run<T>(Action<T> action)
{
Task.Factory.StartNew(() =>
{
using (var lifetimeScope = _container.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag))
{
var service = lifetimeScope.Resolve<T>();
action(service);
}
});
return Task.FromResult(0);
}
I did something similar to #Chima Osuji but I think something is off in his answer so I'm gonna describe my solution and explain it.
public class BackgroundTaskFactory : IBackgroundTaskFactory
{
private ILifetimeScope lifetimeScope;
public BackgroundTaskFactory(ILifetimeScope lifetimeScope)
{
this.lifetimeScope = lifetimeScope;
}
public Task Run<T>(Action<T> action)
{
Task task = Task.Factory.StartNew(() =>
{
using (var lifetimeScope = this.lifetimeScope.BeginLifetimeScope())
{
var service = lifetimeScope.Resolve<T>();
action(service);
}
});
return task;
}
}
It's important to point out that my Run method is returning the task that was created on Task.Factory.StartNew. That way someone waits for the result, he gets the right task. In the other solutions they are returning Task.FromResult(0) which returns a dummy task.
BeginLifetimeScope creates a new scope as a child of the injected scope. If the injected scope is an InstancePerLifetimeScope associated to a web request, as soon as the web request scope is disposed, this new scope will also be disposed and it will error out. Child scopes cannot live longer than its parent scopes. Solution? Register BackgroundTaskFactory as singleton. When you do that, the injected lifetime scope will be the root scope, which doesn't get disposed until the app is disposed.
containerBuilder.RegisterType< BackgroundTaskFactory >()
.As< IBackgroundTaskFactory >()
.SingleInstance();
An updated answer based on the code above:
Usage:
public class ServiceModule :Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<AutoFac.AsyncRunner>().As<AutoFac.IAsyncRunner>().SingleInstance();
}
}
public class Controller
{
private AutoFac.IAsyncRunner _asyncRunner;
public Controller(AutoFac.IAsyncRunner asyncRunner)
{
_asyncRunner = asyncRunner;
}
public void Function()
{
_asyncRunner.Run<IService>((cis) =>
{
try
{
//do stuff
}
catch
{
// catch stuff
throw;
}
});
}
}
The Interface:
public interface IAsyncRunner
{
Task Run<T>(Action<T> action);
}
The class:
public class AsyncRunner : IAsyncRunner
{
private ILifetimeScope _lifetimeScope { get; set; }
public AsyncRunner(ILifetimeScope lifetimeScope)
{
//Guard.NotNull(() => lifetimeScope, lifetimeScope);
_lifetimeScope = lifetimeScope;
}
public Task Run<T>(Action<T> action)
{
Task.Factory.StartNew(() =>
{
using (var lifetimeScope = _lifetimeScope.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag))
{
var service = lifetimeScope.Resolve<T>();
action(service);
}
});
return Task.FromResult(0);
}
}

Categories