2015-02-03 5 views
1

Im пытается отправить асинхронный почтовый ящик с litte, регистрирующимся в базе данных, когда smtpclient не удается отправить. Я использую WebAPI 2.2 + EF6 + Autofac. Ошибка говорит:DbContext удаляется после отправки почты с SmtpClient

Операция не может быть выполнена, так как был удален 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 впрыскивается объекта.

+1

Это происходит потому, что вы используете 'InstancePerRequest'. Это будет уничтожать «DbContext», когда «Application_EndRequest» происходит с «HttpContext», который встречается * до того, как ваша почта будет отправлена. Вам понадобится какая-то гибридная область для вашей жизни DbEntities. Не уверен, что AutoFac предлагает это, но SimpleInjector делает. http://simpleinjector.readthedocs.org/en/latest/lifetimes.html – danludwig

+0

Autofac предлагает гибридные сроки службы. Я не знаю, как работает срок службы и утилизация. Я просто пытался перенести свои проекты на simpleinjector, кажется, проще и быстрее. – Daniel

+1

SimpleInjector - это удовольствие работать с IMO, это отличный инструмент IoC. – danludwig

ответ

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. В этом случае я предполагаю, что вы храните (электронную почту) сообщения в базе данных отдельно от своей физической сети (Mail).

Первый интерфейс потребляется вашим приложением, как ваш веб-проект, и вы можете передать его любые данные, которые он должен инициировать отправку электронной почты:

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 в реализации третьего интерфейса будет разрешен за пределами веб-запроса и получит пользовательскую область видимости. Эталонная реализация этого метода может быть найдена в the Tripod project.

+0

Спасибо, danludwig! Ваша помощь была очень полезной. Я адаптировал ваше решение к своему проекту и сконфигурировал срок службы контейнера, проверяя ваш треножный код (я только вчера переключил этот проект на простой инжектор). Все работает безупречно. Я проверю ваш проект треноги, это очень интересно в моем первом взгляде. – Daniel

3

Я не уверен, что это хорошая идея отправить эту почту асинхронно. Вероятно, вы начали использовать этот метод асинхронно из-за проблем с производительностью в своих веб-запросах. Но поскольку отправка почты может занять некоторое время, обратный вызов SendCompleted перекрывает срок службы вашего веб-запроса. Поскольку 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>(); 
+0

Версия Autofac будет выглядеть почти идентично, за исключением нескольких изменений имени идентификатора. Контейнером будет ILifetimeScope, и GetInstance будет решаться. –

+0

@ JimBolla: Не стесняйтесь обновлять мой ответ с помощью регистрации Autofac. – Steven

+0

@ JimBolla: Или, конечно же, не стесняйтесь добавить свой собственный ответ с эквивалентом Autofac регистраций, представленных здесь. У вас будет мой взнос для этого. – Steven