Search code examples
asp.net-mvcasync-awaitbackground-process

SendEmailAsync in background process


The MVC project comes with partially built-in email services. I have implemented the EmailService class that is working fine. However, I need to send the emails in bulk to multiple users thus I would like to run the process in the background and this is where I'm stuck. I have tried the following codes:

//To simplify the example, I will just use the loop
for (int i = 1; i <= 10; i++)
{
    //Version 1:
    await UserManager.SendEmailAsync(userId + i, "Test subject line", "Sample email body");

    //Version 2:
    UserManager.SendEmailAsync(userId + i, "Test subject line", "Sample email body");

    //Version 3:
    ThreadPool.QueueUserWorkItem(o => UserManager.SendEmailAsync(userId + i, "Test subject line", "Sample email body"));

    //Version 4:
    Task.Run(async () => { await UserManager.SendEmailAsync(userId + i, "Test subject line", "Sample email body"); });

    //Version 5:
    await Task.Run(() => { UserManager.SendEmailAsync(userId + i, "Test subject line", "Sample email body"); });
}

Version 1 is the only working code. But the await will block the code which causes long delay before moving to the next page.

Version 2, 3 and 4 don't work at all.

Version 5 has a very odd behavior. I always get one less email. Using the i to track which email I'm missing, it's always the last one that is missing. (in above example, I will always receive email from userId 1 to 9 only.) And if I try to send only one email, then I can never receive one.

What went wrong?

Edited Thanks everyone for your response, but I probably should mention that:

  1. At the time I want to "fire and forget', I am almost certain that I have everything I need, and any process after that does not depend on this process.

  2. I do NOT care the success/failure of this process. (Will probably log the error, but would not care much if it fails)


Solution

  • One solution to this problem is to asynchronously fire all of your async email requests, and put those tasks into a collection, then await the collection of tasks. This will probably be a bit faster since each request won't have to wait for the email request before it to complete, instead they can all happen concurrently.

    Example:

    var myTasks = new List<Task>();
    myTasks.Add(UserManager.SendEmailAsync(userId, "Test subject line", "Sample email body"));
    myTasks.Add(UserManager.SendEmailAsync(userId, "Test subject line", "Sample email body"));
    myTasks.Add(UserManager.SendEmailAsync(userId, "Test subject line", "Sample email body"));
    await Task.WhenAll(myTasks);
    

    If you want a full blown fire and forget, which is probably going to be risky no matter how you slice it, check out this post by Stephen Cleary on using QueueBackgroundWorkItem. I would try out putting all of the tasks into a collection though and awaiting them to judge performance before going nuclear option.