我正在尝试根据VS2013项目模板中的示例AccountController为ASP.NET MVC5网站设置电子邮件确认。我已经使用SmtpClient
实现了IIdentityMessageService
,并试图使其尽可能简单:
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
,并显示消息";异步模块或处理程序已完成,而异步操作仍处于挂起状态";如果我试图在控制器中捕获SmtpException
并在catch块中返回ViewResult
,那么我会在YSOD中得到相同的消息。因此,我认为await
-ed操作在任何一种情况下都无法完成。
据我所知,我正在遵循SO上其他帖子中概述的所有异步/等待最佳实践(例如,HttpClient.GetAsync(…)在使用wait/async时从不返回),主要是";一直使用async/await";。我也尝试过使用ConfigureAwait(false)
,没有任何更改。由于只有在抛出特定异常时,代码才会死锁,我认为在大多数情况下,通用模式是正确的,但内部发生了一些事情,导致这种情况下不正确;但由于我对并发编程还很陌生,我觉得我可能错了。
我做错什么了吗?我总是可以在SendAsync方法中使用同步调用(即SmtpClient.Send()
),但感觉应该按原样工作。
尝试这个实现,只需使用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();
}
}
}