Mohammad Akbari
Mohammad Akbari

Reputation: 4766

Correct way to implement fire and forget async method call

In my project, I have a network call to sending Email, I don't want to wait for the response, because the most likely Email provider sends the Emails successfully. Which of the following methods is better and what is the difference?

Method 1: await SendAsync and use Task.Run

public async Task<IActionResult> Index()
{
    await SendAsync();
    return View();
}

private Task SendAsync()
{
    _ = Task.Run(async () =>
    {
        _logger.LogInformation("Before");
        await Task.Delay(10000); // Email send
        _logger.LogInformation("After");
    });

    return Task.CompletedTask;
}

Method 2: Does not await SendAsync

public IActionResult Index()
{
    SendAsync();
    return View();
}

private async Task SendAsync()
{
    _logger.LogInformation("Before");
    await Task.Delay(10000);  // Email send
    _logger.LogInformation("After");
}

Both methods work with awaiting in Task.Delay(10000); line

Upvotes: 2

Views: 2440

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456467

In my project, I have a network call to sending Email, I don't want to wait for the response, because the most likely Email provider sends the Emails successfully.

Most email providers work by having you send into a queue, and then the actual email is sent later, when they process work out of that queue. So, sending into the queue is fast and very reliable.

So this means that your SendEmailAsync or whatever API should be quite fast, and shouldn't require returning early. Since SendEmailAsync actually sends to a queue, the only thing it represents is "please accept my request to send this email".

Which of the following methods is better and what is the difference?

Neither.

Since sending emails is just a queue write, and you don't want your request to be lost, the appropriate approach is not to use fire-and-forget at all:

public async Task<IActionResult> Index()
{
    await SendAsync();
    return View();
}

private async Task SendAsync()
{
    _logger.LogInformation("Before");
    await Task.Delay(10000);  // Email send
    _logger.LogInformation("After");
}

If, for some reason, you're using an email provider that doesn't queue, then you can create your own queue (Azure Storage Queue, Amazon SQS, RabbitMQ, etc) and change SendAsync to write a message to that queue. Then have a separate background process (Azure Function, Amazon Lambda, etc) read from that queue and send the emails to the email provider.

Upvotes: 2

Mo B.
Mo B.

Reputation: 5805

Both are equivalent. Even though there are many who advise against using this pattern - if you know what you are doing (like logging, taking care to handle all exceptions within the task, taking care that the background task behaves correctly during application shutdown etc.), from my experience it is actually ok to use this pattern, at least in .NET Core. There are cases where it's not possible or practical or feasible (performance-wise) to handle the background task in a persistent manner (with a DB or a queue).

In fact, Microsoft's implementation of BackgroundService works exactly like that:

        public virtual Task StartAsync(CancellationToken cancellationToken)
        {
            // Store the task we're executing
            _executingTask = ExecuteAsync(_stoppingCts.Token);

            // If the task is completed then return it, this will bubble cancellation and failure to the caller
            if (_executingTask.IsCompleted)
            {
                return _executingTask;
            }

            // Otherwise it's running
            return Task.CompletedTask;
        }

Here, ExecuteAsync is an abstract Task (to be overriden by the subclass) that is run without await. The task is stored in an instance field just so it can be cancelled with a CancellationToken.

Upvotes: 0

Related Questions