使用SmtpClient发送邮件后处理DbContext

时间:2015-02-03 13:11:21

标签: entity-framework autofac smtpclient

当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注入对象。

2 个答案:

答案 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)

史蒂文很棒,但我不得不说我认为异步电子邮件传递可以。我相信这个问题可以通过添加一个或两个以上的接口来解决。这个解决方案更复杂,Steven的设计更简单,但无论如何我都会提供它:

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中找到对此的参考实现。