当smtpclient发送失败时,我尝试使用数据库中的litte日志记录发送邮件异步。我使用的是WebAPI 2.2 + EF6 + Autofac。错误说:
由于已经处理了DbContext,因此无法完成操作。
我的主要代码:
public class SMTPEmailSender : IEmailSender
{
[...]
public void SendMailAsync(string templateKey, object model, string subject, MailAddress fromAddress, List<MailAddress> toAddresses,
List<MailAddress> ccAddresses = null, List<MailAddress> replyTo = null)
{
try
{
var htmlBody = GenerateHtmlBody(templateKey, model);
var client = new SmtpClient();
var message = new MailMessage
{
From = fromAddress,
Subject = subject,
IsBodyHtml = true,
Body = htmlBody
};
toAddresses.ForEach(m => message.To.Add(m));
if (ccAddresses != null) ccAddresses.ForEach(m => message.CC.Add(m));
if (replyTo != null) replyTo.ForEach(m => message.ReplyToList.Add(m));
client.SendCompleted += SendCompletedCallback;
client.SendAsync(message, message);
}
catch (Exception ex)
{
throw new Exception("Error: " + ex.Message + "<br/><br/>Inner Exception: " + ex.InnerException);
}
}
private void SendCompletedCallback(object s, AsyncCompletedEventArgs e)
{
SmtpClient callbackClient = s as SmtpClient;
MailMessage callbackMailMessage = e.UserState as MailMessage;
var regData = SenderMailLogModel(callbackMailMessage);
if (e.Cancelled)
{
try
{
callbackClient.Send(callbackMailMessage);
}
catch (Exception ex)
{
regData.EmailSenderStatus = EmailSenderStatuses.Cancelled;
regData.Exception = ex.Message;
}
}
if (e.Error != null)
{
regData.EmailSenderStatus = EmailSenderStatuses.Error;
regData.Exception = e.Error.ToString() + " in SendCompletedHandlerEvent";
}
_dbContext.EmailSenderLogs.Add(regData); //here fails
_dbContext.SaveChanges();
callbackClient.Dispose();
callbackMailMessage.Dispose();
}
[...]
}
我的DataContext由Autofac注入。我的容器构建器配置:
[...]
containerBuilder.RegisterType<DbEntities>().AsSelf().InstancePerRequest();
containerBuilder.RegisterType<SMTPEmailSender>().As<IEmailSender>().InstancePerRequest();
[...]
我有一个hacky解决方案,您可以创建一个新的DbEntities对象并使用它而不是autofac注入对象。
答案 0 :(得分:3)
我不确定以异步方式发送此邮件是个好主意。您可能已开始使此方法异步,因为Web请求中存在性能问题。但由于发送邮件可能需要一些时间,因此SendCompleted
回调会覆盖Web请求的生命周期。由于Autofac可以控制它所创建的组件,因此它也会在它们的生命周期结束时处理它们。对于DbContext
,这通常意味着在Web请求结束时将其处理。
虽然以异步方式发送邮件,但这不是一件大事,但是你需要做一些额外的事情来做一些事情&#39;操作完成后,使您当前的方法不适合。
相反,更简单的方法是以同步方式使用SmtpClient
,但将SMTPEmailSender
卸载到后台线程。这样,您可以启动自定义生命周期范围并解析该范围内的邮件发件人。您可以将此基础结构逻辑(生命周期范围的创建)放在组合根目录中的代理中。
我不确定如何使用Autofac执行此操作,但使用Simple Injector时,它将如下所示:
public class AsyncSmtpEmailSenderProxy : IEmailSender
{
private readonly Container container;
public AsyncSmtpEmailSenderProxy(Container container) {
this.container = container;
}
public void void SendMail(string templateKey, object model, ...) {
Task.Factory.StartNew(() => {
try {
using (container.BeginLifetimeScope()) {
var sender = container.GetInstance<SMTPEmailSender>();
sender.SendMail(templateKey, model, ...);
}
} catch (Exception ex) {
// Log exception here. Don't let it bubble up: that would
// end the application.
}
});
}
}
现在,您可以以同步方式实现SMTPEmailSender
,这样更容易,更清晰,更易于维护。只需添加代理,我们就会让真正的发送者表现出异步。
这可以注册如下:
container.RegisterSingle<IEmailSender, AsyncSmtpEmailSenderProxy>();
container.Register<IEmailSender, SMTPEmailSender>();
答案 1 :(得分:3)
public interface IDeliverEmailMessage
{
void Deliver(int emailMessageId);
}
public interface IDeliverMailMessage
{
void Deliver(MailMessage mailMessage,
SendCompletedEventHandler sendCompleted = null,
object userState = null);
}
public interface IDeliveredEmailMessage
{
void OnDelivered(int emailMessageId, Exception error, bool cancelled);
}
此处的命名约定是Email
表示根据您的应用程序发送的电子邮件,而Mail
表示根据低级System.Net.Mail传输的电子邮件。在这种情况下,我假设您将(电子邮件)消息与其物理网络(邮件)传输分开存储在数据库中。
您的应用程序(如Web项目)会使用第一个界面,您可以将其传递给发送电子邮件所需的任何数据:
public class ActiveEmailMessageDelivery : IDeliverEmailMessage
{
private readonly MyDbContext _entities;
private readonly IDeliverMailMessage _mail;
private readonly IDeliveredEmailMessage _email;
public ActiveEmailMessageDelivery(MyDbContext entities,
IDeliverMailMessage mail, IDeliveredEmailMessage email)
{
_entities = entities;
_mail = mail;
_email = email;
}
public void Deliver(int emailMessageId)
{
var entity = _entities.Set<EmailMessage>()
.AsNoTracking()
.Include(x => x.EmailAddress)
.Single(x => x.Id == emailMessageId)
;
// don't send the message if it has already been sent
if (entity.SentOnUtc.HasValue) return;
// don't send the message if it is not supposed to be sent yet
if (entity.SendOnUtc > DateTime.UtcNow) return;
var from = new MailAddress(entity.From);
var to = new MailAddress(entity.EmailAddress.Value);
var mailMessage = new MailMessage(from, to)
{
Subject = entity.Subject,
Body = entity.Body,
IsBodyHtml = entity.IsBodyHtml,
};
var sendState = new SendEmailMessageState
{
EmailMessageId = emailMessageId,
};
_mail.Deliver(mailMessage, OnSendCompleted, sendState);
}
private class SendEmailMessageState
{
public int EmailMessageId { get; set; }
}
private void OnSendCompleted(object sender, AsyncCompletedEventArgs e)
{
var state = (SendEmailMessageState) e.UserState;
_email.OnDelivered(state.EmailMessageId, e.Error, e.Cancelled);
}
}
第二个界面打开传输以提交消息:
public class SmtpMailMessageDelivery : IDeliverMailMessage, IDisposable
{
public SmtpMailMessageDelivery()
{
SmtpClientInstance = new SmtpClient();
}
public void Dispose()
{
SmtpClientInstance.Dispose();
}
protected SmtpClient SmtpClientInstance { get; private set; }
public virtual void Deliver(MailMessage message,
SendCompletedEventHandler sendCompleted = null,
object userState = null)
{
if (sendCompleted != null)
SmtpClientInstance.SendCompleted += sendCompleted;
Task.Factory.StartNew(() =>
SmtpClientInstance.SendAsync(message, userState));
}
}
......第三个会做任何你需要的交付后,在网络请求完成并且结果已经返回给用户之后的方式:
public class OnEmailMessageDelivery : IDeliveredEmailMessage
{
private readonly MyDbContext _entities;
public OnEmailMessageDelivery(MyDbContext entities)
{
_entities = entities;
}
public void OnDelivered(int emailMessageId, Exception error, bool cancelled)
{
var entity = _entities.Find<EmailMessage>(emailMessageId);
entity.LastSendError = error != null ? error.Message : null;
entity.CancelledOnUtc = cancelled
? DateTime.UtcNow : (DateTime?)null;
if (error == null && !cancelled)
entity.SentOnUtc = DateTime.UtcNow;
_entities.SaveChanges();
}
}
第三个接口实现中的DbContext实例将在Web请求之外解析,并获得自定义生命周期范围。可以在the Tripod project中找到对此的参考实现。