'Why do I need to use Wait when Sending email using SendGrid C# client library in a console app

I am using the C# client library for SendGrid version 9.27.0.

The only way I can get it to send an email is by adding Wait() to end of my method.

I understand the await operator is a promise to return back to the point in code after the asynchronous method is finished.

But why do I need to add Wait()? Isn't that converting the asynchronous method to synchronous? If so, what's the point in making it async?

Program.cs

static void Main(string[] args) {
    //var customerImport = new CustomerImport();
    //customerImport.DoImport();

    var mailClient = new MailClient();
    var recipients = new List<string>();
    recipients.Add("[email protected]");

    //Never sends an email
    var response = mailClient.SendMail("[email protected]", recipients, "Test email", "This is a test of the new SG client", false);

    //Will send an email
    mailClient.SendMail("[email protected]", recipients, "Test email", "This is a test of the new SG client", false).Wait();
}

MailClient.cs

public async Task SendMail(string emailFrom, List<string> emailTo, string subject, string body, bool isPlainText) {

    try {
        var apiKey = Utils.GetConfigValue("sendgridAPIKey");
        var emails = new List<EmailAddress>();

        foreach (string email in emailTo) {
            emails.Add(new EmailAddress(email));
        }

        var plainTextContent = "";
        var htmlContent = "";

        if (!isPlainText) {
            htmlContent = body;
        } else {
            plainTextContent = body;
        }

        var message = MailHelper.CreateSingleEmailToMultipleRecipients(new EmailAddress(emailFrom, "LobbyCentral"), emails, subject, plainTextContent, htmlContent);

        //if (metaData != null)
        //    message.AddCustomArgs(metaData);

        foreach (string filename in FileAttachments) {
            if (System.IO.File.Exists(filename)) {
                using (var filestream = System.IO.File.OpenRead(filename)) {
                    await message.AddAttachmentAsync(filename, filestream);
                }
            }
        }

        foreach (PlainTextAttachmentM plainTextM in PlainTextAttachments) {
            byte[] byteData = Encoding.ASCII.GetBytes(plainTextM.Content);

            var attachment = new Attachment();
            attachment.Content = Convert.ToBase64String(byteData);
            attachment.Filename = plainTextM.AttachmentFilename;
            attachment.Type = "txt/plain";
            attachment.Disposition = "attachment";

            message.AddAttachment(attachment);
        }
        
        var client = new SendGridClient(apiKey);
        var response = await client.SendEmailAsync(message);

        if (response.IsSuccessStatusCode) {

            if (DeleteAttachmentsAfterSend && FileAttachments.Count > 0) {
                foreach (string filename in FileAttachments) {
                    if (System.IO.File.Exists(filename)) {
                        System.IO.File.Delete(filename);
                    }
                }
            }
        } else {
            Utils.DebugPrint("error sending email");
        }


    } catch (Exception ex) {
        throw new Exception(string.Format("{0}.{1}: {2} {3}", System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName, System.Reflection.MethodBase.GetCurrentMethod().Name, ex.Message, ex.StackTrace));
    }
}


Solution 1:[1]

Calling mailClient.SendMail starts a Task, but doesn't wait for its completion.

If that's the last instruction of you program, the program simply ends before the tasks can finish.

You want your last instructions to start and wait for the task's completion. You can do that by using either of the following.

Make your Main async. (That's what I would personally do.)

// Signature changed to async Task instead of void.
static async Task Main(string[] args) {
        // (...)
        
        // Added await. Removed Wait.
        await mailClient.SendMail("[email protected]", recipients, "Test email", "This is a test of the new SG client", false);
}

Use Wait like you're doing.

static void Main(string[] args) {
        // (...)
        
        var task = mailClient.SendMail("[email protected]", recipients, "Test email", "This is a test of the new SG client", false);
        task.Wait();
}

Solution 2:[2]

Question : But why do I need to add Wait()? Isn't that converting the asynchronous method to synchronous? If so, what's the point in making it async?

Answer : It's not mandatory to Wait() a Task. What's the point in making it async? It gives you two great benefits.

  1. You can return Task, which means it's awaitable, and which means again, you can choose either wait it or just forget about it and let the task scheduled and picked up by a threadPool thread instead of synchronously running it in the same thread.
  2. You can use async/await pattern, which means you can gracefully avoid blocking the current thread

Below code obviously will block the current thread.

static void Main(string[] args) 
{
    .
    .
    //Will send an email
    mailClient.SendMail("[email protected]", recipients, "Test email", "This is a test of the new SG client", false)
   .Wait(); // <- block the current thread until the task completes.
}

If SendMail is something you can fire&forget in your real codebase, you can just get rid of .Wait() and move on without checking the Task state. But your application should be up and running for a certain, enough time to finish the scheduled task. If it's not, you'd better think of using async/await pattern instead of blocking your valuable thread by using .Wait().

static async Task Main(string[] args) 
{
    .
    .
    //Will send an email and get to know when the task is done.
    await mailClient.SendMail("[email protected]", recipients, "Test email", "This is a test of the new SG client", false);
}

Solution 3:[3]

You need to await it in this console app because the very last thing that the Main() does before it returns (and the app exits) is send the mail. I'd say there is slim to no chance at all that the mail sending task will complete before the app exits; it involves a lot of IO, whereas it will take nanoseconds for the app to quit, taking the incomplete Task with it

Anything you do to make it wait long enough will suffice; turning it synchronous with a call to Wait, making the Main as async Task Main(...), even adding a Console.ReadLine will all mean the mail sending will have time to complete ..

.. though I question why a console app that only does one thing then exits even needs to be async anything - it simply has no other jobs to do while it waits for this IO to complete so there seems little point using any asyncronous facilities

Solution 4:[4]

TL;DR Update your code to use async, await, and Task's throughout the whole call chain of your method, including your Main method, otherwise your program won't wait for the asynchronous Task's to complete and will not run all your code.

Your code should look like this:

static async Task Main(string[] args) {
    //var customerImport = new CustomerImport();
    //customerImport.DoImport();

    var mailClient = new MailClient();
    var recipients = new List<string>();
    recipients.Add("[email protected]");

    //Never sends an email
    var response = await mailClient.SendMail("[email protected]", recipients, "Test email", "This is a test of the new SG client", false);
}

In C# .NET the Task class represents an operation that usually is performed asynchronously on another thread. When you use the await keyword or the .Wait() method, the program will wait for the operation to finish and then continue executing the next statements.

The difference between using the await keyword and the .Wait() method is that the await keyword will release the current thread so other work can be done on the current thread, while the .Wait() method will block the current thread, leaving the thread unutilized until the task is complete. When the task is complete, when using the async keyword, the program will find an available thread (not necessarily the same thread as you started on) and will continue to run the instructions subsequent to your await. The .Wait() blocks the current thread, so it'll keep using the current thread when the task is complete.

To use await you need to make your method async. Sometimes, you don't need to wait on the Task and you can simply return it, in that case you don't need to mark the method as async although it is still recommended for easier debugging. However, once you start using async and Task's, you need to do so all the way up to the call chain.

This sample using .Wait needs to be converted to using async/await.

public static class Program
{
    public static void Main(string[] args)
    {
        DoSomeWork();
    }

    public static void DoSomeWork()
    {
        DoFileOperations();
    }

    public static void DoFileOperations()
    {
        File.ReadAllTextAsync("/path/to/file").Wait();
    }
}

Making this program asynchronous looks like this:

public static class Program
{
    public static async Task Main(string[] args)
    {
        await DoSomeWork();
    }

    public static Task DoSomeWork()
    {
        // you can return a Task without awaiting it, as a result no need for async keyword.
        return DoFileOperations();
        
        // But the following is recommended for easier debugging, at cost of a negligible performance hit. (Also need to mark method is async for this)
        // return await DoFileOperations(); 
    }

    public static async Task DoFileOperations()
    {
        await File.ReadAllTextAsync("/path/to/file");
    }
}

For a console program, you need to update the Main method signature to use async Task or Task for this to work. If you don't, the Task will start running but the program will exit immediately and not wait for the Task to be complete.

(Non-console applications will provide other APIs to do async/await.)

In your application, your Main method does not have the async Task or Task return signature, and you're not awaiting the Task returned from the mailClient.SendMail method.

What likely is happening inside of your mailClient.SendMail method is

  1. The first asynchronous Task is started, created by the message.AddAttachmentAsync
  2. This Task runs on a separate thread, meanwhile the current thread goes back to run the code after mailClient.SendMail in the Main method
  3. The Main method finishes and the console application stops running.

At this point, the Task from message.AddAttachmentAsync is likely finished already, but the subsequent code is never run, including the client.SendEmailAsync method, because the Task from mailClient.SendMail wasn't awaited.

There's a lot more nuance to async, await, and Task's, but those details aren't too relevant to your questions, so if you're interested, here's more info on Asynchronous programming with async and await from the Microsoft docs.
I'd also recommend reading David Fowler's guidance on async/await which lists some pitfalls and best practices.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Batesias
Solution 2
Solution 3 Caius Jard
Solution 4 Swimburger