By Notanywho Notanywho


2016-04-29 18:31:37 8 Comments

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?

4 comments

@Axel Heer 2017-04-03 11:51:09

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

@Shazi 2017-10-15 11:32:13

In your ApplicationStopped.Register delegate you don't actually wait for the task returned from "Task.WaitAll(currentTask.ToArray());". Making that call kind of pointless.

@Axel Heer 2017-10-15 12:05:36

WaitAll does already wait. Maybe you mean WhenAll?

@Shazi 2017-10-16 13:10:40

Yes, you are correct, my mistake.

@Scott Chamberlain 2017-05-23 17:06:48

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);
            }
        }
    }
}

@Shazi 2017-10-15 11:33:32

Just like in Axel's answer you don't actually wait for the task returned from "Task.WaitAll(currentTask.ToArray());".

@DalSoft 2018-01-07 10:43:40

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();
}

@ycrumeyrolle 2016-09-02 06:26:31

You can use Hangfire (http://hangfire.io/) for background jobs in .NET Core.

For example :

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget!"));

Related Questions

Sponsored Content

1 Answered Questions

[SOLVED] Safety of AsyncLocal in ASP.NET Core

4 Answered Questions

[SOLVED] Add client certificate to .net core Httpclient

2 Answered Questions

[SOLVED] DI in .NET Core without ASP.NET Core

3 Answered Questions

[SOLVED] Wiring and injected NLog into a .Net Core console application

  • 2017-06-30 04:06:06
  • Matt R
  • 4670 View
  • 11 Score
  • 3 Answer
  • Tags:   c# .net-core nlog

10 Answered Questions

[SOLVED] What is ".NET Core"?

  • 2014-11-13 11:38:32
  • Petr Abdulin
  • 104776 View
  • 280 Score
  • 10 Answer
  • Tags:   .net .net-core

1 Answered Questions

[SOLVED] When should I use Task.Run in Asp.Net Core?

  • 2018-07-10 02:38:55
  • JohanP
  • 821 View
  • 0 Score
  • 1 Answer
  • Tags:   c# asp.net-core

3 Answered Questions

1 Answered Questions

[SOLVED] ASP .Net Core web app calling a Web API

1 Answered Questions

[SOLVED] How to switch between ASP.NET 4.5 and ASP.NET Core?

Sponsored Content