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("[email protected]", 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();
}
Однако я получаю странное поведение, когда почта не отправляется, но только в одном конкретном экземпляре, когда хост каким-то образом недоступен. Пример config:
<system.net>
<mailSettings>
<smtp deliveryMethod="Network">
<network host="unreachablehost" defaultCredentials="true" port="25" />
</smtp>
</mailSettings>
</system.net>
В этом случае запрос оказывается в тупике, никогда не возвращая ничего клиенту. Если почта не отправляется по какой-либо другой причине (например, хост активно отказывается от подключения), исключение обрабатывается нормально, и я получаю YSOD.
При взгляде на журналы событий Windows кажется, что InvalidOperationException
выбрасывается вокруг одного и того же таймфрейма с сообщением "Асинхронный модуль или обработчик завершен, пока асинхронная операция все еще находится в ожидании"; Я получаю то же сообщение в YSOD, если попытаюсь поймать SmtpException
в контроллере и вернуть ViewResult
в блок catch. Поэтому я считаю, что операция await
-ed не завершается в любом случае.
Насколько я могу судить, я следую всем рекомендациям async/wait, описанным в других сообщениях SO (например, HttpClient.GetAsync(...) никогда не возвращается, когда используя await/async), в основном "используя async/await all up up". Я также пробовал использовать 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();
}
}
}