SmtpClient.SendMailAsync вызывает взаимоблокировку при создании определенного исключения

Я пытаюсь настроить подтверждение электронной почты для ASP.NET веб-сайт MVC5, основанный на примере AccountController из шаблона проекта VS2013. Я реализовал IIdentityMessageService используя SmtpClient, пытаясь сохранить его как можно проще:

public class EmailService : IIdentityMessageService
{
    public async Task SendAsync(IdentityMessage message)
    {
        using(var client = new SmtpClient())
        {
            var mailMessage = new MailMessage("some.guy@company.com", message.Destination, message.Subject, message.Body);
            await client.SendMailAsync(mailMessage);
        }
    }
}

код контроллера, который вызывает его, прямо из шаблона (извлечен в отдельное действие, так как я хотел исключить другие возможные причины):

public async Task<ActionResult> TestAsyncEmail()
{
    Guid userId = User.Identity.GetUserId();

    string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId);
    var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme);
    await UserManager.SendEmailAsync(userId, "Confirm your account", "Please confirm your account by clicking <a href="" + callbackUrl + "">here</a>");

    return View();
}

однако я получаю странное поведение, когда почта не удается отправить, но только в одном конкретном случае, когда хозяин как-то недостижимым. Пример конфигурации:

<system.net>
    <mailSettings>
        <smtp deliveryMethod="Network">
            <network host="unreachablehost" defaultCredentials="true" port="25" />
        </smtp>
    </mailSettings>
</system.net>

в этом случае запрос кажется взаимоблокировкой, никогда ничего не возвращая клиенту. Если почта не отправляется по какой-либо другой причине (например, хост активно отказывается от соединения), исключение обрабатывается нормально, и я получаю YSOD.

глядя на журналы событий Windows, кажется, что InvalidOperationException бросается вокруг того же таймфрейма, с сообщением " An асинхронный модуль или обработчик завершен, пока асинхронная операция еще не завершена."; Я получаю то же самое сообщение в YSOD, если пытаюсь поймать SmtpException в контроллере и вернуть ViewResult в блоке catch. Поэтому я считаю await-операция ed не завершается в любом случае.

насколько я могу судить, я следую всем рекомендациям async/await, изложенным в других сообщениях на SO (например,С помощью HttpClient.С getasync(...) никогда не возвращается при использовании await/async), в основном "использование async/await полностью". Я также пробовал использовать ConfigureAwait(false), без изменения. Поскольку код блокируется только в случае возникновения конкретного исключения, я считаю, что общий шаблон верен для большинства случаев, но что-то происходит внутри, что делает его неправильным в этом случае; но поскольку я довольно новичок в параллельном программировании, у меня есть чувство, что я могу ошибаться.

Я что-то делаю неправильно ? Я всегда могу использовать синхронный вызов (т. е. SmtpClient.Send()) в методе SendAsync, но похоже, что это должно работать как есть.

1 ответов


попробуйте эту реализацию, просто используйте client.SendMailExAsync вместо client.SendMailAsync. Дайте нам знать, если это имеет какое-либо значение:

public static class SendMailEx
{
    public static Task SendMailExAsync(
        this System.Net.Mail.SmtpClient @this,
        System.Net.Mail.MailMessage message,
        CancellationToken token = default(CancellationToken))
    {
        // use Task.Run to negate SynchronizationContext
        return Task.Run(() => SendMailExImplAsync(@this, message, token));
    }

    private static async Task SendMailExImplAsync(
        System.Net.Mail.SmtpClient client, 
        System.Net.Mail.MailMessage message, 
        CancellationToken token)
    {
        token.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<bool>();
        System.Net.Mail.SendCompletedEventHandler handler = null;
        Action unsubscribe = () => client.SendCompleted -= handler;

        handler = async (s, e) =>
        {
            unsubscribe();

            // a hack to complete the handler asynchronously
            await Task.Yield(); 

            if (e.UserState != tcs)
                tcs.TrySetException(new InvalidOperationException("Unexpected UserState"));
            else if (e.Cancelled)
                tcs.TrySetCanceled();
            else if (e.Error != null)
                tcs.TrySetException(e.Error);
            else
                tcs.TrySetResult(true);
        };

        client.SendCompleted += handler;
        try
        {
            client.SendAsync(message, tcs);
            using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false))
            {
                await tcs.Task;
            }
        }
        finally
        {
            unsubscribe();
        }
    }
}