39

I have a Web API's action where I need to run some task and forget about this task. This is how my method is organized now:

public async Task<SomeType> DoSth()
{
    await Task.Run(...);
    .....
    //Do some other work
}

The thing is that obviously it stops at the await line waiting when it's done and only then continues the work. And I need to "fire and forget" Should I just call Task.Run() without any async-await?

D. Jones
  • 399
  • 1
  • 3
  • 3
  • Possible duplicate. https://stackoverflow.com/questions/33834540/how-do-i-start-not-awaited-background-tasks-in-an-async-method/33835718#33835718 – William Xifaras Mar 31 '16 at 14:56
  • Possible duplicate of [Fire and forget async method in asp.net mvc](https://stackoverflow.com/questions/18502745/fire-and-forget-async-method-in-asp-net-mvc) – Michael Freidgeim Jul 28 '17 at 07:16

7 Answers7

40

And I need to "fire and forget"

I have a blog post that goes into details of several different approaches for fire-and-forget on ASP.NET.

In summary: first, try not to do fire-and-forget at all. It's almost always a bad idea. Do you really want to "forget"? As in, not care whether it completes successfully or not? Ignore any errors? Accept occasional "lost work" without any log notifications? Almost always, the answer is no, fire-and-forget is not the appropriate approach.

A reliable solution is to build a proper distributed architecture. That is, construct a message that represents the work to be done and queue that message to a reliable queue (e.g., Azure Queue, MSMQ, etc). Then have an independent backend that process that queue (e.g., Azure WebJob, Win32 service, etc).

Should I just call Task.Run() without any async-await?

No. This is the worst possible solution. If you must do fire-and-forget, and you're not willing to build a distributed architecture, then consider Hangfire. If that doesn't work for you, then at the very least you should register your cowboy background work with the ASP.NET runtime via HostingEnvironment.QueueBackgroundWorkItem or my ASP.NET Background Tasks library. Note that QBWI and AspNetBackgroundTasks are both unreliable solutions; they just minimize the chance that you'll lose work, not prevent it.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Is Task.Run() acceptable if the fire and forget task is non-critical to the web api functioning? eg. looking up values for additional logging. Plus if I know the app pool won't recycle itself that often? – sgarg Aug 02 '16 at 14:26
  • 1
    @sgarg: Why wouldn't you use `HostingEnvironment.QueueBackgroundWorkItem`? It's just as easy to use and at least a *little* safer than `Task.Run`. You still shouldn't use it for anything critical, but sure, unimportant work can go there. On a side note, you don't know how often the app pool will recycle - IIRC, it's every 19 hours *by default*, but ASP.NET will do it more often if it thinks your app isn't working right. – Stephen Cleary Aug 02 '16 at 15:20
  • Can't use `HostingEnvironment.QueueBackgroundWorkItem` since I don't have .NET 4.5.2 in all our environments. – sgarg Aug 02 '16 at 17:09
  • @sgarg: In that case, you can use my (very small) [ASP.NET Background Tasks](https://github.com/StephenCleary/AspNetBackgroundTasks) library. – Stephen Cleary Aug 02 '16 at 18:15
  • 4
    I don't like the sentiment that people are saying fire-and-forget is almost always a bad idea, but I do agree with the warning. In my case, I do requests and logging *beforehand* and the user is notified if the tasks succeeds, or a job lets them know if it fails. They can also check the status and be prohibited to continue if it did not succeed and they must try again. Why is that a bad idea? Just have the design in place to account for failures so they can be addressed. – Mark Sep 26 '16 at 14:45
  • 1
    @Mark: Fire-and-forget on ASP.NET is inherently unreliable. Your users may never get success *or* failure notifications for jobs that just disappear. Bottom line: ASP.NET recycles your process occasionally (waiting for existing requests to complete), so if your process is doing work outside of a request, it can just suddenly disappear. – Stephen Cleary Sep 26 '16 at 19:37
  • Thanks for responding. I understand that it is not possible to guarantee a response, but if a service was running to "check" on the status every so often, it could send a response more reliably. This way, the processing is immediate and the response is possibly delayed. There could also be a status check action to check its progress sooner. The alternative to this seems to be to write a service that actually accepts requests, or continuously hit a database. – Mark Sep 26 '16 at 21:54
  • @StephenCleary: To clarify, I want to have a web service that receives a request to process something, and respond that it has received the request while it does that work in the background. Maybe "fire-and-forget" is not the right terminology for this... Maybe if it is passed off to another request thread and not wait for a response it will work? I'm guessing that the framework wasn't designed for this type of process, but if its possible I'd love to avoid writing an entire service for this. – Mark Sep 26 '16 at 22:07
  • 1
    @Mark: Sorry, that's exactly the scenario we're talking about here. The *proper* (read: only truly-reliable) solution is to use a reliable queue with an independent backend process. You could look into Hangfire, which tries to lower the bar to entry (with tradeoffs). – Stephen Cleary Sep 26 '16 at 22:47
  • @StephenCleary: Thanks for referring me to Hangfire, looks very promising! I understand there are trade-offs, but with certain cases we are not worried about occasional failures because the apps are designed to make sure the process was done successfully before it can continue, meaning that the time saved in how quick things can be processed and the maintainability makes up for the possibility of failure, and can be re-tried anyway so long as the process accounts for the possibility (which it should anyway) – Mark Sep 27 '16 at 18:12
  • Has the situation changed now that .Net Core is out? BTW --the answer for me has always been "yes, I want to forget, no I don't care if it succeeds or fail". – jmoreno Aug 23 '19 at 20:12
  • 1
    @jmoreno: The specifics have changed (i.e., Core has a different way of registering background work), but the overall situation is the same. It **must** be the same. This situation must remain the same for any kind of hosted application that uses a request/response protocol. The host must know when it's safe to unload the application, and fire-and-forget prevents the host from knowing when it's safe to do so, by design. – Stephen Cleary Aug 23 '19 at 20:30
  • @StephenCleary, I might be asking a stupid question, but I'm going to ask it anyway. When you say "suddenly disappear", do you mean to say that that task is not completed? (Or will the task still continue executing in the background?) Thanks. – jflaga Jun 25 '21 at 13:32
  • 1
    @JeremiahFlaga: That's a fine question. I mean that the task would not be completed. No logs, no exception, no observable result except the task doesn't finish its work. – Stephen Cleary Jun 25 '21 at 13:45
  • I have one additional question (considering I indeed want to do fire and forged, and I will take care of the error handling and logging in the Task, and as well I will handle the scope of the dependency injection properly), is it safe technically to start a Task in a controller without awaiting it? My concern is that the Task continues its work while the request is already closed, so maybe aspnet core could abort the Task sometimes for no reason? Maybe because the Task suddenly loses its parent, etc. – Lukas K Sep 06 '22 at 20:14
  • 1
    @LukasK: There's never *no* reason, but loss of work *can* happen for a wide variety of reasons some developers may find surprising. [Shutdown and restarts *must* be considered normal behavior](https://blog.stephencleary.com/2021/01/asynchronous-messaging-2-durable-queues.html). IIS restarts the app pool periodically. Janitor trips over the server's power plug. Your cloud service will take your webapp / VM / container out of rotation to apply OS/runtime patches. You yourself will fix an app bug and perform a rolling upgrade. *Any* of these can cause lost work when the app does fire-and-forget. – Stephen Cleary Sep 06 '22 at 21:40
22

For fire and forget, use this

Task.Factory.StartNew(async () =>
{
    using (HttpClient client = new HttpClient())
    {
        await client.PostAsync("http://localhost/api/action", new StringContent(""));
    }
});
Catalin
  • 11,503
  • 19
  • 74
  • 147
  • 20
    Thanks, at least an answer that is just to the point, all those others screaming about "don't do it" seem to not see the benefits – CularBytes Feb 28 '19 at 09:53
  • 9
    Ok, use it as an example, but please, don't create a new HttpClient instance everytime like this. Remember HttpClient is meant to be re-used as a singleton, or you may face socket exhaustion. For more information check these two blog posts: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ https://www.nimaara.com/beware-of-the-net-httpclient/ – digaomatias Jul 17 '20 at 00:06
  • 1
    creating new HttpClient every time causes issues. https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests. Also thread will be preempted by GC after controller scope is completed. – Aditya Nadig Feb 14 '22 at 10:22
  • 1
    Why do you prefer `Task.Factory.StartNew` over `Task.Run`? The later is less cumbersome, and also it doesn't require passing an [explicit scheduler](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2008 "CA2008: Do not create tasks without passing a TaskScheduler") like the former. – Theodor Zoulias May 24 '22 at 12:33
12

True fire and forget tasks can be difficult in asp.net as they can often die along with the request that they were created as part of.

If you are using 4.5.2+ then you can use QueueBackgroundWorkItem to run the task. By registering tasks via this method the AppDomain will try to delay shutting down until they have all completed but there can still be instances when they will be killed before they are completed. This is probably the simplest thing to do but worth reading into to see exactly what instances can cause jobs to be cancelled.

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
{
  await Task.Run(...);
});

There is an tool called hangfire that uses a persistent store to ensure that a task has completed and has built-in retry and error recording functionality. This is more for "background tasks" but does suit fire and forget. This is relatively easy to setup and offers a variety of backing stores, I can't recall the exact details but some require a license and some don't (like MSSQL).

Fermin
  • 34,961
  • 21
  • 83
  • 129
5

I use HangFire.

This is best for me.

An easy way to perform background processing in .NET and .NET Core applications. No Windows Service or separate process required.

Backed by persistent storage. Open and free for commercial use.

Community
  • 1
  • 1
Majid gharaei
  • 281
  • 3
  • 11
2

I agree with others that you should not just forget about your call. However, to answer your question, if you remove await from the Task.Run() line, the call will not be blocking as shown here

public async Task<SomeType> DoSth()
{
    Task.Run(...);
    .....
    //Do some other work while Task.Run() continues in parallel.
}
Community
  • 1
  • 1
AvinashK
  • 3,309
  • 8
  • 43
  • 94
  • It's sometimes perfectly fine to forget about a call. For example if you are sending a heartbeat signal in an IOT app, you usually don't care about the response. – TidyDev Jun 25 '20 at 05:28
1

For invoking a fire and forget WebApi method, I used the following code to ensure that it returns an OK response. I my case, the bearer authorization token created at login is stored in a cookie:

...
FireAndForget().Wait();
...

private async Task FireAndForget()
    {
        using (var httpClient = new HttpClient())
        {
            HttpCookie cookie = this.Request.Cookies["AuthCookieName"];
            var authToken = cookie["AuthTokenPropertyName"] as string;
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
            using (var response = await httpClient.GetAsync("http://localhost/api/FireAndForgetApiMethodName"))
            {
                //will throw an exception if not successful
                response.EnsureSuccessStatusCode();
            }
        }
    }
CAK2
  • 1,892
  • 1
  • 15
  • 17
0

Never fire and forget, because then you won't get to see any errors, which makes for some very awkward troubleshooting if something goes wrong (having the task method do its own exception handling isn't guaranteed to work, because the task may not successfully start in the first place). Unless you really don't mind if the task does anything or not, but that's quite unusual (since, if you truly didn't care, why run the task in the first place)? At the very least, create your task with a continuation:

Task.Run(...)
  .ContinueWith(t => 
    logException(t.Exception.GetBaseException()),
    TaskContinuationOptions.OnlyOnFaulted
  )
;

You can make this more sophisticated as your needs dictate.

In the specific case of a web API, you may actually want to wait for your background tasks to finish before you complete your request. If you don't, you're leaving stuff running in the background that may misrepresent how much load your service can really take, or even stop working altogether if clients fire too many requests and you don't do anything to throttle them. You can gather tasks up and issue an await Task.WhenAll(...) at the end to achieve that; this way, you can continue to do useful work while your background tasks plod away, but you don't return until everything's done.

Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85
  • 2
    I want to fire and forget because I have actions that can get the status of the process. It is not critical for me because the essential database updates are done first, and THEN the tasks are run. If the tasks fail, the user cannot continue because the database was not updated, and they will be asked to try again. – Mark Sep 26 '16 at 14:39