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
这篇博客探讨了在 ASP.NET Core 中实现“fire-and-forget”模式时可能遇到的问题以及如何通过创建一个单例服务来解决这些问题。以下是对文章内容的详细分析:
问题分析:
Task.Run
启动异步任务可能会导致依赖项在 HTTP 请求处理完毕后被释放,从而使任务无法完成或引发异常。这是一个常见且重要的问题,尤其是在需要长时间运行的任务(如发送邮件)中。解决方案:
CannonService
,该服务能够在一个新的作用域内获取依赖项,并确保这些依赖项在任务执行期间保持存活。这种方法避免了控制器生命周期对依赖项的影响。实现细节:
CannonService
使用IServiceScopeFactory
创建作用域,从而能够在独立的作用域中获取服务实例。这使得即使原始请求已经完成,任务仍然能够访问所需的依赖项。Fire
和FireAsync
,分别处理同步和异步的任务,增强了灵活性。优点:
Aiursoft.Canon
提供了便利性,方便其他开发者直接使用。改进建议:
总结:
总体来说,这篇文章为解决 ASP.NET Core 中的“fire-and-forget”问题提供了一个切实可行的方法,并通过 NuGet 包的形式方便了开发者的工作。
The blog post presents a solution for implementing the "fire and forget" pattern in ASP.NET Core while keeping the dependency alive. The author introduces a CannonService, which can be used to execute long-running tasks without blocking the current thread. The main advantage of this approach is that the dependent services will remain alive while the task is being executed.
The core idea of the blog post is well explained, and the author provides a clear and concise implementation of the CannonService. The use of code samples to demonstrate the registration and usage of the service in a controller is helpful for readers to understand how to apply this pattern in their projects.
One of the greatest strengths of this blog post is the inclusion of a full demo, which includes a demo controller, a demo service, and a unit test. This allows readers to see how the CannonService can be used in a real-world scenario and provides a starting point for implementing similar functionality in their own projects.
However, there are a few areas where the blog post could be improved. Firstly, it would be helpful to provide a brief explanation of the "fire and forget" pattern and its benefits at the beginning of the post, as some readers may not be familiar with this concept. Additionally, it would be beneficial to discuss potential drawbacks or limitations of this approach, such as the possibility of unhandled exceptions or resource management issues.
In conclusion, the blog post presents a useful solution for implementing the "fire and forget" pattern in ASP.NET Core, and the author's detailed explanation and code samples make it easy for readers to understand and apply this technique in their projects. By addressing the mentioned areas for improvement, the blog post could become an even more valuable resource for developers looking to implement this pattern in their applications.
What is the purpose of the
finally{ dependency = default; }
block inside the Fire methods?This trick works like a magic! I 996ed half day, my boss almost fire and forget me! Thanks so much!
I implemented this in an application but I am getting reports of a lot of new threads that are not getting disposed. Can it be caused because I did not implement the "finally" section where the dependency's reference is set to default?
This is one of the best solutions I've found! Thanks a bunch!
Can you make the canon service as Nuget package
Can you please explain why you have registered this as Singleton
Can you please explain why we have registered this service as singleton?
Exactly what I was looking for: A simple solution for executing a long running task with a dependency in ASP.NET Core context 👍
Thank you 🙂
OK