Search code examples
c#asynchronousconsole-applicationsmtpclient

Getting error while sending bulk email "An asynchronous call is already in progress. It must be completed or canceled before you can call this method"


I have created one console application for migrating data from one database table to another in which records of customers are migrated so I have to notify them for password change.

public static async Task<bool> SendRegisterEmail(List<MailMessage> mailMessage)
{
    bool flag = true;
    try
    {
        var smtp = new SmtpClient();
        var taskEmails = mailMessage.Select(x => smtp.SendMailAsync(x));

        await Task.WhenAll(taskEmails); // **Error : An asynchronous call is already in progress. It must be completed or canceled before you can call this method**
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return flag;
}

emails are successfully sent asyncronously if I remove that await Task.WhenAll(taskEmails) but when migration operation completes,it not sends all the emails. many of them remain unsend on console app closed after operation completed and I have more than 1,00,000 records so how can I continue email sending process in background or application running until the all emails are successfully sent ?

Here is the code of data migration:

foreach (DataRow SourceReader in DS.Tables[0].Rows)
{
    insertCounter++;
    using (SqlCommand DestinationCommand = DestinationConnection.CreateCommand())
    {
        Console.WriteLine("inserting row...");
        var date = (SourceReader["LockedUntil"] == DBNull.Value ? Convert.ToDateTime("01/01/1753") : Convert.ToDateTime(SourceReader["LockedUntil"]));
        DestinationCommand.CommandText = string.Format(insertQuery1, (SourceReader["CustomerGUID"] == DBNull.Value ? null : SourceReader["CustomerGUID"].ToString()), (SourceReader["FirstName"] == DBNull.Value ? null : SourceReader["FirstName"].ToString()), (SourceReader["LastName"] == DBNull.Value ? null : SourceReader["LastName"].ToString()), (SourceReader["Email"] == DBNull.Value ? null : SourceReader["Email"].ToString()), 1, PasswordManager.Encrypt(SourceReader["FirstName"].ToString() + "1!"), (SourceReader["Phone"] == DBNull.Value ? null : SourceReader["Phone"].ToString()), 1, 0, 0, date.ToString("yyyy-MM-ddTHH:mm:ss"), Convert.ToInt16(SourceReader["BadLoginCount"] == DBNull.Value ? 0 : SourceReader["BadLoginCount"]), Convert.ToInt16(SourceReader["OkToEmail"] == DBNull.Value ? 0 : SourceReader["OkToEmail"]), (SourceReader["CustomerGUID"] == DBNull.Value ? null : SourceReader["CustomerGUID"].ToString()), 2);
        DestinationCommand.ExecuteNonQuery();
        Console.WriteLine("AspNetUser Row inserted...!!! ");
    }
    if ((insertCounter % 100) == emailCounter)
    {
        var message = Email.AddUserForEmail(new User() { Email = (SourceReader["Email"] == DBNull.Value ? null : SourceReader["Email"].ToString()), Password = (SourceReader["FirstName"].ToString() + "1!"), FirstName = (SourceReader["FirstName"] == DBNull.Value ? null : SourceReader["FirstName"].ToString()), LastName = (SourceReader["LastName"] == DBNull.Value ? null : SourceReader["LastName"].ToString()) });
        mailList.Add(message);
        var flag = Email.SendRegisterEmail(mailList);
        emailCounter++;
    }
    else if (insertCounter == totalCount)
    {
        var message = Email.AddUserForEmail(new User() { Email = (SourceReader["Email"] == DBNull.Value ? null : SourceReader["Email"].ToString()), Password = (SourceReader["FirstName"].ToString() + "1!"), FirstName = (SourceReader["FirstName"] == DBNull.Value ? null : SourceReader["FirstName"].ToString()), LastName = (SourceReader["LastName"] == DBNull.Value ? null : SourceReader["LastName"].ToString()) });
        mailList.Add(message);
        var flag = Email.SendRegisterEmail(mailList);
    }
    else
    {
        var message = Email.AddUserForEmail(new User() { Email = (SourceReader["Email"] == DBNull.Value ? null : SourceReader["Email"].ToString()), Password = (SourceReader["FirstName"].ToString() + "1!"), FirstName = (SourceReader["FirstName"] == DBNull.Value ? null : SourceReader["FirstName"].ToString()), LastName = (SourceReader["LastName"] == DBNull.Value ? null : SourceReader["LastName"].ToString()) });
        mailList.Add(message);
    }
    // var i = Email.SendRegisterEmail((SourceReader["Email"] == DBNull.Value ? null : SourceReader["Email"].ToString()), (SourceReader["FirstName"].ToString() + "1!"), (SourceReader["FirstName"] == DBNull.Value ? null : SourceReader["FirstName"].ToString()), (SourceReader["LastName"] == DBNull.Value ? null : SourceReader["LastName"].ToString()));
}

Solution

  • SmtpClient does not allow you to execute multiple asynchronous operations at the same time, that's what error message tells you. You are doing this with:

    var smtp = new SmtpClient();
    var taskEmails = mailMessage.Select(x => smtp.SendMailAsync(x));
    await Task.WhenAll(taskEmails);
    

    You are also not disposing SmtpClient, which doesn't help either.

    Instead, either send them one by one:

    using (var smtp = new SmtpClient()){
        foreach (var email in mailMessage) {
            await smtp.SendMailAsync(email);
        }
    }
    

    Or use separate SmtpClient for each send:

    Func<MailMessage, Task> sendFunc = async (x) => {
        using (var smtp = new SmtpClient()) {
            await smtp.SendMailAsync(x);
        }
    };
    var taskEmails = mailMessage.Select(sendFunc);
    await Task.WhenAll(taskEmails);
    

    It seems that you are also not awaiting those sends from your migration function:

    // flag is Task<bool> here
    var flag = Email.SendRegisterEmail(mailList);
    

    If that's not a typo - you need to await them either in place, or collect tasks in some list and await them after a loop all together (with await Task.WhenAll).

    Note that if you are sending a lot of emails, especially to the same domain, especially in parallel - SMTP server you use (or SMTP server of recipient) might be not very happy with that and might blacklist you for a while.

    It's better to use separate background process which will send your emails completely unrelated to your migration procedure. Just let migration procedure insert information about pending emails to some persistent storage (database table), and then let another application explore that table and send emails. That way migration procedure is not interrupted by email failure, and background process which sends email can retry them as necessary.