2015-02-03 5 views
1

smtpclient가 실패 할 때 데이터베이스에 리트 로그로 비동기 메일을 보내려고합니다. WebAPI 2.2 + EF6 + Autofac을 사용하고 있습니다. 오류 메시지 :SmtpClient를 사용하여 메일을 보낸 후 처리 된 DbContext

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(); 
[...] 

내가 이것에 대한 해키 솔루션이 새 DbEntities 객체를 생성하고 대신 autofac 주입 객체를 사용할 수 있습니다.

+1

이것은 'InstancePerRequest'를 사용하기 때문에 발생합니다. 메일이 보내지기 전에 발생하는'HttpContext'로'Application_EndRequest'가 발생할 때'DbContext'를 처리 할 것입니다. 'DbEntities' 평생 동안 어떤 종류의 하이브리드 범위가 필요합니다. AutoFac이 이것을 제공하는지 확실하지 않지만 SimpleInjector는이를 제공합니다. http://simpleinjector.readthedocs.org/en/latest/lifetimes.html – danludwig

+0

Autofac은 하이브리드 수명을 제공합니다. 나는 일생의 범위와 처분이 어떻게 작동하는지 잘 모른다. 내 프로젝트를 단순 주입기로 옮기기를 원했는데, 간단하고 빠른 것처럼 보입니다. – 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 전송으로 이동합니다. 이 경우에는 실제 네트워크 (메일) 전송과 별도로 (전자 메일) 메시지를 데이터베이스에 저장한다고 가정합니다.

첫 번째 인터페이스는 웹 프로젝트와 같은 응용 프로그램에 의해 소비되고, 당신은이 이메일의 송신을 개시 할 필요가 어떤 데이터를 전달할 수 있습니다

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을 백그라운드 스레드로 오프로드합니다. 이렇게하면 사용자 정의 수명 범위를 시작하고 해당 범위 내에서 메일 보낸 사람을 확인할 수 있습니다. 컴포지션 루트에 배치하는 프록시 내부에이 인프라 스트럭처 논리 (수명 범위 생성)를 넣을 수 있습니다. ,

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을 구현할 수 있습니다

나는 Autofac이 작업을 수행하는 방법을 정확히 모르겠지만, 다음과 같이 간단한 인젝터로는 볼 것이다 더 청결하고 유지 보수가 용이합니다. 프록시를 추가하면 실제 발신자가 비동기로 작동합니다. 다음과 같이

을 등록 할 수 있습니다 :

container.RegisterSingle<IEmailSender, AsyncSmtpEmailSenderProxy>(); 
container.Register<IEmailSender, SMTPEmailSender>(); 
+0

Autofac 버전은 약간의 식별자 이름 변경 사항을 제외하고 거의 동일하게 보입니다. 컨테이너는 ILifetimeScope가되고 GetInstance는 Resolve가됩니다. –

+0

@JimBolla : Autofac 등록으로 내 대답을 자유롭게 업데이트하십시오. – Steven

+0

@JimBolla : 여기에 제시된 등록과 동일한 Autofac과 함께 자신의 답변을 추가 할 수 있습니다. 너는 내 업보트를 가지고있을거야. – Steven