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);
}
}
Related
Currently i'm designing a logger service to log HttpRequests made by HttpClient.
This logger service is Singleton and i want to have scoped contexts inside it.
Here's my logger:
public Logger
{
public LogContext Context { get; set; }
public void LogRequest(HttpRequestLog log)
{
context.AddLog(log);
}
}
I'm using the logger inside a DelegatingHandler:
private readonly Logger logger;
public LoggingDelegatingHandler(Logger logger)
{
this.logger = logger;
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await base.SendAsync(request, cancellationToken);
this.logger.LogRequest(new HttpRequestog());
}
Then when i make some request using HttpClient, i want to have the logs for this specific call:
private void InvokeApi()
{
var logContext = new LogContext();
this.Logger.LogContext = logContext;
var httpClient = httpClientFactory.CreateClient(CustomClientName);
await httpClient.GetAsync($"http://localhost:11111");
var httpRequestLogs = logContext.Logs;
}
The problem is, it works but it's not thread safe. If i have parallel executions of InvokeApi, the context will not be the correct.
How can i have a attached context for each execution properly?
I'm registering the HttpClient like this:
services.AddSingleton<Logger>();
services.AddHttpClient(CentaurusHttpClient)
.ConfigurePrimaryHttpMessageHandler((c) => new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
})
.AddHttpMessageHandler(sp => new LoggingDelegatingHandler(sp.GetRequiredService<Logger>()));
I'm testing this piece of code using this:
public void Test_Parallel_Logging()
{
Random random = new Random();
Action test = async () =>
{
await RunScopedAsync(async scope =>
{
IServiceProvider sp = scope.ServiceProvider;
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
using (var context = new HttpRequestContext(sp.GetRequiredService<Logger>()))
{
try
{
var httpClient = httpClientFactory.CreateClient();
await httpClient.GetAsync($"http://localhost:{random.Next(11111, 55555)}");
}
catch (HttpRequestException ex)
{
Output.WriteLine("count: " + context?.Logs?.Count);
}
}
});
};
Parallel.Invoke(new Action[] { test, test, test });
}
This logger service is Singleton and i want to have scoped contexts inside it.
The Logger has a singleton lifetime, so its LogContext property also has a singleton lifetime.
For the kind of scoped data you're wanting, AsyncLocal<T> is an appropriate solution. I tend to prefer following a "provider"/"consumer" pattern similar to React's Context, although the more common name in the .NET world for "consumer" is "accessor". In this case, you could make the Logger itself into the provider, though I generally try to keep it a separate type.
IMO this is most cleanly done by providing your own explicit scope, and not tying into the "scope" lifetime of your DI container. It's possible to do it with DI container scopes but you end up with some weird code like resolving producers and then doing nothing with them - and if a future maintainer removes (what appears to be) unused injected types, then the accessors break.
So I recommend your own scope, as such:
public Logger
{
private readonly AsyncLocal<LogContext> _context = new();
public void LogRequest(HttpRequestLog log)
{
var context = _context.Value;
if (context == null)
throw new InvalidOperationException("LogRequest was called without anyone calling SetContext");
context.AddLog(log);
}
public IDisposable SetContext(LogContext context)
{
var oldValue = _context.Value;
_context.Value = context;
// I use Nito.Disposables; replace with whatever Action-Disposable type you have.
return new Disposable(() => _context.Value = oldValue);
}
}
Usage (note the explicit scoping provided by using):
private void InvokeApi()
{
var logContext = new LogContext();
using (this.Logger.SetContext(logContext))
{
var httpClient = httpClientFactory.CreateClient(CustomClientName);
await httpClient.GetAsync($"http://localhost:11111");
var httpRequestLogs = logContext.Logs;
}
}
I have managed to configure scoped services together with scoped filters for consumers, meaning that I can set a value to a scoped service in a filter implementing IFilter<ConsumeContext<T>> and registering the filter with UseConsumeFilter. The filter sets a value in my scoped service and after that the scoped service can be injected into my consumer and still have the value set.
I have tried to do the same thing for activities using IFilter<ExecuteContext<TArguments>> and registering my filter with UseExecuteActivityFilter.
The values set in the ExecuteActivityContext are not reachable in the Activity. I think they become two different DI scopes. I'll share the code from my activity and consumer implementations and maybe there is something missing in the activity one. I have tried to only keep the important part so if there is illegal syntax somewhere it's from me trying to clean up the code for SO.
Is this me using DI in a wrong way or something thats bugged with DI for activities? I tried following the "Scoped Filters" documentation on masstransits website. I'm on .net core 3.1 and masstransit 7.0.4.
Scoped service used for testing
//Interface
public interface IContextService
{
string TenantId { get; set; }
}
//DI registration
services.AddScoped<IContextService, ContextService>();
Activity configuration, this is not working
//Filter
public class RetreiveContextExecuteFilter<TArguments> : IFilter<ExecuteContext<TArguments>>
where TArguments : class
{
public IContextService _contextService { get; }
public RetreiveContextExecuteFilter(IContextService contextService)
{
_contextService = contextService;
}
public async Task Send(ExecuteContext<TArguments> context, IPipe<ExecuteContext<TArguments>> next)
{
_contextService.tenantId = "test-tenant";
await next.Send(context);
}
public void Probe(ProbeContext context)
{
var scope = context.CreateFilterScope("testcontextinformation");
}
}
//Activity
public class ExampleActivity
: IExecuteActivity<ExampleActivityArguments>
{
private readonly IContextService _contextService;
public ExampleActivity(IContextService contextService)
{
_contextService = contextService;
}
public async Task<ExecutionResult> Execute(ExecuteContext<ExampleActivityArguments> context)
{
var tenant = _contextService.tenantId; //Empty
}
}
//DI
services.AddMassTransit(cfg =>
{
cfg.AddActivitiesFromNamespaceContaining<ExampleActivity>();
services.TryAddSingleton(KebabCaseEndpointNameFormatter.Instance);
cfg.UsingRabbitMq(ConfigureBus);
});
private static void ConfigureBus(IBusRegistrationContext context, IRabbitMqBusFactoryConfigurator configurator)
{
configurator.ConfigureEndpoints(context);
configurator.UseExecuteActivityFilter(typeof(RetreiveContextExecuteFilter<>), context);
}
Consumer configuration, this is working
//Filter definition
public class RetreiveContextConsumeFilter<T> : IFilter<ConsumeContext<T>>
where T : class
{
public IContextService _contextService { get; }
public RetreiveContextConsumeFilter(IContextService contextService)
{
_contextService = contextService;
}
public Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
_contextService.TenantId = "test tenant";
return next.Send(context);
}
public void Probe(ProbeContext context)
{
context.CreateFilterScope("contextinformation");
}
}
//Consumer
public class ExampleConsumer
: IConsumer<ExampleEvent>
{
private readonly IContextService _contextService;
public ExampleConsumer(IContextService contextService)
{
_contextService = contextService;
}
public async Task Consume(ConsumeContext<ExampleEvent> context)
{
var id = _contextService.TenantId(); //Correct value
}
}
//DI
services.AddMassTransit(cfg =>
{
cfg.AddConsumersFromNamespaceContaining<ExampleConsumer>();
services.TryAddSingleton(KebabCaseEndpointNameFormatter.Instance);
cfg.UsingRabbitMq(ConfigureBus);
});
private static void ConfigureBus(IBusRegistrationContext context, IRabbitMqBusFactoryConfigurator configurator)
{
configurator.ConfigureEndpoints(context);
configurator.UseConsumeFilter(typeof(RetreiveContextConsumeFilter<>), context);
}
First guess, is that your configuration order is incorrect. MassTransit builds pipelines, and you are configuring your endpoints before the filter, which is going to make the filter run after the endpoints. That's my guess.
Change the consumer to:
configurator.UseConsumeFilter(typeof(RetreiveContextConsumeFilter<>), context);
configurator.ConfigureEndpoints(context);
Change the activity to:
configurator.UseExecuteActivityFilter(typeof(RetreiveContextExecuteFilter<>), context);
configurator.ConfigureEndpoints(context);
I am currently using an async Task method to implement the IAuthenticationFilter interface. Upon successful login, I'll try to access an API that has this attribute and it will work fine. However once I go back and access the API again the exception will be thrown.
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
var token = context.Request.Headers.Authorization.Parameter;
var principal = await AuthenticateToken(token)
// Other code here ...
}
protected Task<IPrincipal> AuthenticateToken(string token)
{
var secretKey = _authenticationBusiness.GetSecretKey(); // error triggers here.
if (principal == null)
context.ErrorResult = new AuthenticationFailureResult("Invalid token", request);
else
context.Principal = principal;
}
//AuthenticationBusiness.cs
public string GetSecretKey()
{
using (_unitOfWork)
{
var token = _unitOfWork.Tokens.GetToken();
return token.SecretKey ?? string.Empty;
}
}
//Dependency Injection using Unity
container.RegisterType<IUnitOfWork, UnitOfWork>(new HierarchicalLifetimeManager());
container.RegisterType<IContext, Context>(new HierarchicalLifetimeManager());
//UnitOfWork.cs
private readonly IContext _context;
public UnitOfWork(IContext context, IJWTRepository tokens)
{
_context = context;
Tokens = tokens;
}
public IJWTRepository Tokens { get; private set; }
public void Dispose()
{
_context.Dispose();
}
//Context.cs
public class Context : DbContext, IContext
{
public new void SaveChanges()
{
base.SaveChanges();
}
public new void Dispose()
{
base.Dispose();
}
}
//JWTRepository.cs
public class JWTRepository : Repository<JsonWebToken>, IJWTRepository
{
public JWTRepository(Context context) : base(context) { }
public JsonWebToken GetToken()
{
return Context.Tokens
.OrderBy(jwt => jwt.Id)
.Take(1)
.SingleOrDefault();
}
private Context Context => _context as Context;
}
If I try to remove this attribute and access the API multiple times nothing wrong happens so I am assuming that this has something to do with the fact that the attribute has asynchronous methods?
When the lifetime of an IDisposable object is limited to a single
method, you should declare and instantiate it in the using statement.
The using statement calls the Dispose method on the object in the
correct way, and (when you use it as shown earlier) it also causes the
object itself to go out of scope as soon as Dispose is called. Within
the using block, the object is read-only and cannot be modified or
reassigned.
using Statement (C# Reference)
In your code, the problem is that you are wrapping GetSecretkey() into using() which will dispose _unitOfWork and when you will try to access it again it will show an error.
Hope this code works for you.
//AuthenticationBusiness.cs
public string GetSecretKey()
{
var token = _unitOfWork.Tokens.GetToken();
return token.SecretKey ?? string.Empty;
}
The problem in in your function AuthenticateToken. When using async-await, make sure you await every Task before returning, if the Task Disposes items. See what is the purpose of return await. The first answer focuses on disposable object
I assume you omitted parts of method AuthenticateToken, because I don't say the return value.
Solution: declare the method async, and await for the Task before returning
async Task<IPrincipal> AuthenticateToken(string token)
{
var secretKey = _authenticationBusiness.GetSecretKey();
...
// Somewhere there is a Task involved,
Task<IPrincipal> myTask = ...
// instead of return myTask:
return await myTask;
}
In my Owin Middleware I subscribe a callback to the OnSendingHeaders. In my UnitTests, the callback is sometimes called twice. Most of the time, the first TestCase succeeds and the second fails. Sometimes both succeed, sometimes both fail. I also encountered this problem in production.
As I understand the documentation, this Callback should only be called once. Do I misunderstand the documentation? Or is this a bug? And is there a “clean” way to deal with this behavior?
public class MyMiddleware
{
public void Initialize(Func<IDictionary<string, object>, Task> next) { }
public int Count { get; private set; }
public async Task Invoke(IDictionary<string, object> env)
{
IOwinContext context = new OwinContext(env);
context.Response.OnSendingHeaders(dummy =>
{
Count++;
}, null);
await context.Response.WriteAsync("something");
}
}
[TestFixture]
public class MyMiddlewareTests
{
[TestCase(1), TestCase(2)]
public async Task TheTest(int dummy)
{
var myMiddleware = new MyMiddleware();
using (var server = TestServer.Create(app => { app.Use(myMiddleware); }))
{
var response = await server.HttpClient.GetAsync("/");
Assert.AreEqual(1, myMiddleware.Count);
}
}
}
Edit: In the original question I stated that I haven't seen the problem occuring in production. This changed, so the problem isn't test-only.
Edit 2: We realized that we have a different problem on production, i.e. I can only confirm that the callback is called twice with the testserver.
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.