【问题标题】:DbContext disposed after send a mail with SmtpClient使用 SmtpClient 发送邮件后处理的 DbContext
【发布时间】:2015-02-03 13:11:20
【问题描述】:

当 smtpclient 发送失败时,我正在尝试发送带有少量日志记录的异步邮件。我正在使用 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 注入的对象。

【问题讨论】:

  • 发生这种情况是因为您使用的是InstancePerRequest。当Application_EndRequestHttpContext 发生时,这将处理DbContext,这发生在您的邮件发送之前。您的DbEntities 生命周期需要某种混合作用域。不确定 AutoFac 是否提供此功能,但 SimpleInjector 提供。 simpleinjector.readthedocs.org/en/latest/lifetimes.html
  • Autofac 提供混合生命周期。我不太清楚生命周期范围和处置是如何工作的。我只是想将我的项目迁移到 simpleinjector,它似乎更简单、更快。
  • SimpleInjector 很高兴与 IMO 合作,它是一个很棒的 IoC 工具。
  • 虽然我不确定建议 OP 在这里切换工具是否合适,但我同意 @danludwig 关于 Simple Injector 的看法 :-)
  • 问题已解决,但不是直接解决。由于 autofac 容器配置和 hangfire 的问题,我刚刚将 ioc 容器切换为简单的注入器。 ioc 很容易配置生命周期范围,并且操作对我来说花费的时间很少。

标签: entity-framework autofac smtpclient


【解决方案1】:

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

第二个界面打开transport提交消息:

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

【讨论】:

  • 感谢丹路德维希!您的帮助非常有用。我已经根据我的项目调整了您的解决方案并配置了检查您的三脚架代码的容器生命周期(我昨天刚刚将该项目切换为简单的注入器)。一切都完美无缺。我会检查你的三脚架项目,我第一次看很有趣。
【解决方案2】:

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

虽然异步发送邮件并没有什么大不了的,但您在操作完成后需要做一些额外的事情,这使得您当前的方法不适合。

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

【讨论】:

  • Autofac 版本看起来几乎相同,只是保存了一些标识符名称更改。 Container 是 ILifetimeScope,GetInstance 是 Resolve。
  • @JimBolla:请随时使用 Autofac 注册更新我的答案。
  • @JimBolla:当然,您也可以使用与此处介绍的注册等效的 Autofac 添加您自己的答案。你会得到我的支持。
  • 我认为您的答案已经足够接近,以至于 op 应该能够弄清楚他需要改变什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-12-04
  • 2011-01-29
  • 1970-01-01
  • 1970-01-01
  • 2014-08-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多