Fire and forget async method in asp.net mvc

45,487

Solution 1

First off, let me point out that "fire and forget" is almost always a mistake in ASP.NET applications. "Fire and forget" is only an acceptable approach if you don't care whether DeleteFooAsync actually completes.

If you're willing to accept that limitation, I have some code on my blog that will register tasks with the ASP.NET runtime, and it accepts both synchronous and asynchronous work.

You can write a one-time wrapper method for logging exceptions as such:

private async Task LogExceptionsAsync(Func<Task> code)
{
  try
  {
    await code();
  }
  catch(Exception exception)
  {
    m_log.Error("Call failed: " + exception.ToString());
  }
}

And then use the BackgroundTaskManager from my blog as such:

BackgroundTaskManager.Run(() => LogExceptionsAsync(() => DeleteFooAsync()));

Alternatively, you can keep TaskScheduler.UnobservedTaskException and just call it like this:

BackgroundTaskManager.Run(() => DeleteFooAsync());

Solution 2

As of .NET 4.5.2, you can do the following

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await LongMethodAsync());

But it only works within ASP.NET domain

The HostingEnvironment.QueueBackgroundWorkItem method lets you schedule small background work items. ASP.NET tracks these items and prevents IIS from abruptly terminating the worker process until all background work items have completed. This method can't be called outside an ASP.NET managed app domain.

More here: https://msdn.microsoft.com/en-us/library/ms171868(v=vs.110).aspx#v452

Solution 3

The best way to handle it is use the ContinueWith method and pass in the OnlyOnFaulted option.

private void button1_Click(object sender, EventArgs e)
{
    var deleteFooTask = DeleteFooAsync();
    deleteFooTask.ContinueWith(ErrorHandeler, TaskContinuationOptions.OnlyOnFaulted);
}

private void ErrorHandeler(Task obj)
{
    MessageBox.Show(String.Format("Exception happened in the background of DeleteFooAsync.\n{0}", obj.Exception));
}

public async Task DeleteFooAsync()
{
    await Task.Delay(5000);
    throw new Exception("Oops");
}

Where I put my message box you would put your logger.

Share:
45,487
acarlon
Author by

acarlon

Updated on July 08, 2022

Comments

  • acarlon
    acarlon almost 2 years

    The general answers such as here and here to fire-and-forget questions is not to use async/await, but to use Task.Run or TaskFactory.StartNew passing in the synchronous method instead.
    However, sometimes the method that I want to fire-and-forget is async and there is no equivalent sync method.

    Update Note/Warning: As Stephen Cleary pointed out below, it is dangerous to continue working on a request after you have sent the response. The reason is because the AppDomain may be shut down while that work is still in progress. See the link in his response for more information. Anyways, I just wanted to point that out upfront, so that I don't send anyone down the wrong path.

    I think my case is valid because the actual work is done by a different system (different computer on a different server) so I only need to know that the message has left for that system. If there is an exception there is nothing that the server or user can do about it and it does not affect the user, all I need to do is refer to the exception log and clean up manually (or implement some automated mechanism). If the AppDomain is shut down I will have a residual file in a remote system, but I will pick that up as part of my usual maintenance cycle and since its existence is no longer known by my web server (database) and its name is uniquely timestamped, it will not cause any issues while it still lingers.

    It would be ideal if I had access to a persistence mechanism as Stephen Cleary pointed out, but unfortunately I don't at this time.

    I considered just pretending that the DeleteFoo request has completed fine on the client side (javascript) while keeping the request open, but I need information in the response to continue, so it would hold things up.

    So, the original question...

    for example:

    //External library
    public async Task DeleteFooAsync();
    

    In my asp.net mvc code I want to call DeleteFooAsync in a fire-and-forget fashion - I don't want to hold up the response waiting for DeleteFooAsync to complete. If DeleteFooAsync fails (or throws an exception) for some reason, there is nothing that the user or the program can do about it so I just want to log an error.

    Now, I know that any exceptions will result in unobserved exceptions, so the simplest case I can think of is:

    //In my code
    Task deleteTask = DeleteFooAsync()
    
    //In my App_Start
    TaskScheduler.UnobservedTaskException += ( sender, e ) =>
    {
        m_log.Debug( "Unobserved exception! This exception would have been unobserved: {0}", e.Exception );
        e.SetObserved();
    };
    

    Are there any risks in doing this?

    The other option that I can think of is to make my own wrapper such as:

    private void async DeleteFooWrapperAsync()
    {
        try
        {
            await DeleteFooAsync();
        }
        catch(Exception exception )
        {
            m_log.Error("DeleteFooAsync failed: " + exception.ToString());
        }
    }
    

    and then call that with TaskFactory.StartNew (probably wrapping in an async action). However this seems like a lot of wrapper code each time I want to call an async method in a fire-and-forget fashion.

    My question is, what it the correct way to call an async method in a fire-and-forget fashion?

    UPDATE:

    Well, I found that the following in my controller (not that the controller action needs to be async because there are other async calls that are awaited):

    [AcceptVerbs( HttpVerbs.Post )]
    public async Task<JsonResult> DeleteItemAsync()
    {
        Task deleteTask = DeleteFooAsync();
        ...
    }
    

    caused an exception of the form:

    Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. at System.Web.ThreadContext.AssociateWithCurrentThread(BooleansetImpersonationContext)

    This is discussed here and seems to be to do with the SynchronizationContext and 'the returned Task was transitioned to a terminal state before all async work completed'.

    So, the only method that worked was:

    Task foo = Task.Run( () => DeleteFooAsync() );
    

    My understanding of why this works is because StartNew gets a new thread for DeleteFooAsync to work on.

    Sadly, Scott's suggestion below does not work for handling exceptions in this case, because foo is not a DeleteFooAsync task anymore, but rather the task from Task.Run, so does not handle the exceptions from DeleteFooAsync. My UnobservedTaskException does eventually get called, so at least that still works.

    So, I guess the question still stands, how do you do fire-and-forget an async method in asp.net mvc?