使用 SmtpClient 发送邮件后 DbContext 被处理

DbContext disposed after send a mail with SmtpClient

当 smtpclient 发送失败时,我正在尝试发送一封带有少量日志记录的异步邮件。我正在使用 WebAPI 2.2 + EF6 + Autofac。错误说:

The operation cannot be completed because the DbContext has been disposed.

我的主要代码:

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

我不确定异步发送此邮件是否是个好主意。由于 Web 请求中的性能问题,您可能开始使此方法异步。但由于发送邮件可能需要一些时间,因此 SendCompleted 回调与您的 Web 请求的生命周期重叠。由于 Autofac 控制着它创建的组件,它也会在它们的生命周期结束时处理它们。对于 DbContext 这通常意味着它在网络请求结束时被释放。

虽然以异步方式发送邮件,但这没什么大不了的,但是您有一个额外的要求,即在操作完成后执行 'some stuff',这使得您当前的方法不适合。

相反,一种更简单的方法是以同步方式使用 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>();

史蒂文很聪明,但我不得不说我认为异步电子邮件传递还可以。我相信可以通过添加一两个接口来解决问题。这个解决方案更复杂,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));
    }
}

...第三个将执行您需要的任何操作 post-交付,在 Web 请求完成并将结果返回给用户之后:

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.

中找到此的参考实现