前情提要
這篇的誕生是因為筆者在工作上專案時有寄信的需求,且會有各式各樣的服務皆可能需要寄信服務,各式各樣服務包含應用程式端、資料庫端像Store Procedure運算完需寄信通知,目標要讓不同服務都有相同的接口去對應。
我們的想法是設計一個資料表,有寄信需求則往該資料表新增一筆資料,再由一個專門pooling的排程服務依照Status,一一的把信件寄出去,由於因為是靠Status在運作,更新Status變成是一個很重要的任務。
大概運作方式就是,排程服務時間到要運行時,會將 Status = 'N'
符合的資料撈出來,一筆一筆進行寄信作業,但由於在foreach
中若用同步的方式寄信則會大塞車,當然要考慮最差狀況,因此必須要使用非同步方式進行寄信。但非同步方式的話,只要 SendMailAsync()
後會與主程式脫鉤,無法掌握寄信成功與否,更新其對應的紀錄,將Status
更新為Y
或 F
。
內容
筆者第一步是想到SmtpClient
本身應該會有提供相對應的事件,查一下就會看到有SendCompleted
可以觸發,但此call back function
帶回的參數,為object sender, AsyncCompletedEventArgs e
,只知道寄信結果及SmtpClient(Sender)
相關資訊,無法取得寄信資料表對應的資料,才有辦法做更新作業。
SmtpClient.SendCompleted 事件 (System.Net.Mail)
筆者接下去思考,自己必須要包一個類別,把寄信資料表對應的資料接進來變內部變數,對外開放Event
,使外部使用端可以接受寄信結果完成事件觸發,再由該Event
時丟出
寄信需求資料
來實作吧,首先先規劃出寄信需求資料結構,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class MailToDoItem { public int SeqNo { get; set; } public string SourceId { get; set; } public string Mail_To { get; set; } public string Mail_Cc { get; set; } public string Subject { get; set; } public string Message { get; set; } public string Status { get; set; } public string SendUser { get; set; } public DateTime? SendTime { get; set; } public string ErrMessage { get; set; } }
|
寄信服務類別
接著自己的寄信服務類別:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public class MySmtpClient : IDisposable { private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private MailToDoItem _mailToDo; private SmtpClient _smtpClient; public event Action<MailToDoItem, AsyncCompletedEventArgs> MySendCompleted; public MySmtpClient(MailToDoItem mailToDo, EmailSetting emailSetting) { _mailToDo = mailToDo; _smtpClient = new SmtpClient(emailSetting.SmtpServer); _smtpClient.SendCompleted += new SendCompletedEventHandler(_smtpClient_SendCompleted); }
private void _smtpClient_SendCompleted(object sender, AsyncCompletedEventArgs e) { MySendCompleted(_mailToDo, e); _smtpClient.SendCompleted -= _smtpClient_SendCompleted; this.Dispose(); }
public void SendMailProcess(string systemId , byte[] attach, string attachName) { MailMessage mailMessage = new MailMessage();
var mailFrom = new MailAddress($"{systemId}@company.com.tw", systemId); mailMessage.From = mailFrom; foreach (var item in _mailToDo.Mail_To?.Split(";")) { if (string.IsNullOrEmpty(item) == false) mailMessage.To.Add(item); } foreach (var item in _mailToDo.Mail_Cc?.Split(';')) { if (string.IsNullOrEmpty(item) == false) mailMessage.CC.Add(item); } mailMessage.Body = _mailToDo.Message; mailMessage.Subject = _mailToDo.Subject;
if (attach != null && attach?.Length > 0) { Attachment attachment = new Attachment(new MemoryStream(attach), attachName); mailMessage.Attachments.Add(attachment); } _smtpClient.SendMailAsync(mailMessage); }
public void Dispose() { _logger.Info($"[MySmtpClient Dispose]!!"); this._smtpClient.Dispose(); GC.SuppressFinalize(this); } }
|
其中EmailSetting
,要再宣告一下,也是從外面傳進去,基本上是寄信相關的設定物件
1 2 3 4 5 6 7
| public class EmailSetting { public string SmtpServer { get; set; } public int SmtpPort { get; set; } public string Account { get; set; } public string Password { get; set; } }
|
以上 MySmtpClient
包裝SmtpClient
,達到物件導向設計三大特性中的封裝,你不需要知道我怎麼寄信,把寄信資料表對應的資料
及SmtpServer
相關的資訊給我,我幫你完成寄信,且寄信完成後會再由你自定義的事件觸發讓你知道已完成。
使用端使用方式也很簡單,第一步宣告事件對應的Action
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| protected void MailSendCompleted(MailToDoItem mailToDoItem, AsyncCompletedEventArgs e) { _logger.Info($"[Send Completed]: {JsonConvert.SerializeObject(mailToDoItem)}"); using (var _conn = new SqlConnection(_connStr)) { var result = e.Error != null ? "F" : "Y"; var errMsg = e.Error != null ? e.Error.ToString() : ""; _conn.Execute(@"UPDATE MailToDo SET Status = @Status, ErrMessage = @ErrMessage , SendUser = @SendUser, SendTime = @SendTime WHERE SeqNo = @SeqNo" , new { Status = result, mailToDoItem.SeqNo, SendUser = "SYS", SendTime = DateTime.Now, ErrMessage = errMsg }); } }
|
寄信Client端
再寄信需求程式片段中呼叫:
1 2 3
| var mySmtpClient = new MySmtpClient(mailToDoItem, emailSetting); mySmtpClient.MySendCompleted += MailSendCompleted; mySmtpClient.SendMailProcess("MySystem.Admin", null, null);
|
結論
以上,筆者在實際掛排程服務跑過,是沒有問題的,確實感受到非同步的好處,以及 event, action
的搭配,堪稱是完美。要記住event, action
的搭配,筆者會在爾後會常使用到該技巧。