C# fire and forget might not be suitable in ASP.NET Core

To fire and forget in C#, it is really simple:

Task.Run(() => FireAway());

But the same approach might not be suitable in ASP.NET Core Controller.

Consider the following example:

public class MyController : Controller
{
    private readonly MyHeavyDependency _hd;

    public MyController(MyHeavyDependency hd)
    {
        _hd = hd;
    }

    public IActionResult MyAction()
    {
        Task.Run(() => _hd.DoHeavyAsyncWork());
        return Json("Your job is started!");
    }
}

In the controller, we triggered a heavy job. And the job is running in a dependency. Now the job will successfully get started as a fire-and-forget. But...

After processing the HTTP response, you controller might be disposed. Which means that your dependency might not be alive. It is hard for us to control that so your job may just not able to finish.

Keep the dependency alive in a new singleton service

To keep our dependency always alive while we are firing the job, we need a new service to contain it. And the new service must be singleton. For singleton service will never be disposed.

And we need to get the dependency in your service, but not in the controller. Because the life-cycle of controller depends on HTTP context while your singleton service dosen't.

Create a new class:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace YourNamespace.Services
{
    public class CannonService
    {
        private readonly ILogger<CannonService> _logger;
        private readonly IServiceScopeFactory _scopeFactory;

        public CannonService(
            ILogger<CannonService> logger,
            IServiceScopeFactory scopeFactory)
        {
            _logger = logger;
            _scopeFactory = scopeFactory;
        }

        public void Fire<T>(Action<T> bullet, Action<Exception> handler = null)
        {
            _logger.LogInformation("Fired a new action.");
            Task.Run(() =>
            {
                using var scope = _scopeFactory.CreateScope();
                var dependency = scope.ServiceProvider.GetRequiredService<T>();
                try
                {
                    bullet(dependency);
                }
                catch (Exception e)
                {
                    _logger.LogError(e,"Cannon crashed!");
                    handler?.Invoke(e);
                }
                finally
                {
                    dependency = default;
                }
            });
        }

        public void FireAsync<T>(Func<T, Task> bullet, Action<Exception> handler = null)
        {
            _logger.LogInformation("Fired a new async action.");
            Task.Run(async () =>
            {
                using var scope = _scopeFactory.CreateScope();
                var dependency = scope.ServiceProvider.GetRequiredService<T>();
                try
                {
                    await bullet(dependency);
                }
                catch (Exception e)
                {
                    _logger.LogError(e,"Cannon crashed!");
                    handler?.Invoke(e);
                }
                finally
                {
                    dependency = default;
                }
            });
        }
    }
}

To use it, register it as a singleton.

// Call it in StartUp.cs, ConfigureServices method.
services.AddSingleton<CannonService>();

Use cannon to fire a method

To use it in controller, simply inject your cannon to your controller:

    public class OAuthController : Controller
    {
        private readonly CannonService _cannonService;

        public OAuthController(
            CannonService cannonService)
        {
            _cannonService = cannonService;
        }

    }

And call it with a function which depends something and cost long time:

            // Send him an confirmation email here:
            _cannonService.FireAsync<EmailSender>(async (sender) =>
            {
                await sender.SendAsync(); // Which may be slow. Depends on the sender to be alive.
            });

And the method will not block current thread. You can just return a result to the client side. The sender will always be alive while sending the mail.

Nuget Package

Yes, I have published the code above as a nuget package.

See the source code here: Aiursoft.Canon

Download the package here: Nuget.org