Asp.Net Core 快速邮件队列设计与实现
發(fā)送郵件幾乎是軟件系統(tǒng)中必不可少的功能,在Asp.Net Core 中我們可以使用MailKit發(fā)送郵件,MailKit發(fā)送郵件比較簡單,網(wǎng)上有許多可以參考的文章,但是應該注意附件名長度,和附件名不能出現(xiàn)中文的問題,如果你遇到了這樣的問題可以參考我之前寫的這篇博客Asp.Net Core MailKit 完美附件(中文名、長文件名)。
在我們簡單搜索網(wǎng)絡(luò),并成功解決了附件的問題之后,我們已經(jīng)能夠發(fā)送郵件啦!不過另一個問題顯現(xiàn)出來——發(fā)送郵件太慢了,沒錯,在我使用QQ郵箱發(fā)送時,單封郵件發(fā)送大概要用1.5秒左右,用戶可能難以忍受請求發(fā)生1.5秒的延遲。
所以,我們必須解決這個問題,我們的解決辦法就是使用郵件隊列來發(fā)送郵件
設(shè)計郵件隊列
Ok, 第一步就是規(guī)劃我們的郵件隊列有什么
EmailOptions
我們得有一個郵件Options類,來存儲郵件相關(guān)的選項
/// <summary>/// 郵件選項
/// </summary>
public class EmailOptions{ ?
?public bool DisableOAuth { get; set; } ?
? ?public string DisplayName { get; set; } ?
? ? ?public string Host { get; set; } // 郵件主機地址public string Password { get; set; } ?
? ? ??public int Port { get; set; } ?
? ? ???public string UserName { get; set; } ?
? ? ????public int SleepInterval { get; set; } = 3000;...
SleepInterval?是睡眠間隔,因為目前我們實現(xiàn)的隊列是進程內(nèi)的獨立線程,發(fā)送器會循環(huán)讀取隊列,當隊列是空的時候,我們應該讓線程休息一會,不然無限循環(huán)會消耗大量CPU資源
然后我們還需要的就是 一個用于存儲郵件的隊列,或者叫隊列提供器,總之我們要將郵件存儲起來。以及一個發(fā)送器,發(fā)送器不斷的從隊列中讀取郵件并發(fā)送。還需要一個郵件寫入工具,想要發(fā)送郵件的代碼使用寫入工具將郵件轉(zhuǎn)儲到隊列中。
那么我們設(shè)計的郵件隊列事實上就有了三個部分:
隊列存儲提供器(郵件的事實存儲)
郵件發(fā)送機 (不斷讀取隊列中的郵件,并發(fā)送)
郵件服務(wù) (想法送郵件時,調(diào)用郵件服務(wù),郵件服務(wù)會將郵件寫入隊列)
隊列存儲提供器設(shè)計
那么我們設(shè)計的郵件隊列提供器接口如下:
public interface IMailQueueProvider{ ??void Enqueue(MailBox mailBox); ?
? ?bool TryDequeue(out MailBox mailBox); ?
? ??int Count { get; } ?
? ?? ?bool IsEmpty { get; }...
四個方法,入隊、出隊、隊列剩余郵件數(shù)量、隊列是否是空,我們對隊列的基本需求就是這樣。
MailBox是對郵件的封裝,并不復雜,稍后會介紹到
郵件服務(wù)設(shè)計
public interface IMailQueueService{ ?? ? ?void Enqueue(MailBox box);
對于想要發(fā)送郵件的組件或者代碼部分來講,只需要將郵件入隊,這就足夠了
郵件發(fā)送機(兼郵件隊列管理器)設(shè)計
public interface IMailQueueManager{ ??void Run(); ?
? ?void Stop(); ?
? ?bool IsRunning { get; } ?
? ??int Count { get; } ? ?
啟動隊列,停止隊列,隊列運行中狀態(tài),郵件計數(shù)
現(xiàn)在,三個主要部分就設(shè)計好了,我們先看下MailBox,接下來就去實現(xiàn)這三個接口
MailBox
MailBox 如下:
public class MailBox{? ?public IEnumerable<IAttachment> Attachments { get; set; }
? ?? ?public string Body { get; set; } ?
? ?? ? ?public IEnumerable<string> Cc { get; set; }
? ?? ? ? ? ?public bool IsHtml { get; set; }
? ?? ? ? ? ?? ?public string Subject { get; set; }
? ?? ? ? ? ?? ?? ?public IEnumerable<string> To { get; set; }...
這里面沒什么特殊的,大家一看便能理解,除了IEnumerable<IAttachment> Attachments { get; set; }。
附件的處理
在發(fā)送郵件中最復雜的就是附件了,因為附件體積大,往往還涉及非托管資源(例如:文件),所以附件處理一定要小心,避免留下漏洞和bug。
在MailKit中附件實際上是流Stream,例如下面的代碼:
attachment = new MimePart(contentType) {Content = new MimeContent(fs),ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),ContentTransferEncoding = ContentEncoding.Base64, };其中new MimeContent(fs)是創(chuàng)建的Content,fs是Stream,MimeContent的構(gòu)造函數(shù)如下:
public MimeContent(Stream stream, ContentEncoding encoding = ContentEncoding.Default)所以我們的設(shè)計的附件是基于Stream的。
一般情況附件是磁盤上的文件,或者內(nèi)存流MemoryStream或者 byte[]數(shù)據(jù)。附件需要實際的文件的流Stream和一個附件名,所以附件接口設(shè)計如下:
public interface IAttachment : IDisposable{ ? ?Stream GetFileStream(); ? ?string GetName();那么我們默認實現(xiàn)了兩中附件類型?物理文件附件和內(nèi)存文件附件,byte[]數(shù)據(jù)可以輕松的轉(zhuǎn)換成 內(nèi)存流,所以沒有寫這種
MemoryStreamAttechment
public class MemoryStreamAttechment : IAttachment{ ??private readonly MemoryStream _stream; ?
??private readonly string _fileName; ?
???public MemoryStreamAttechment(MemoryStream stream, string fileName) ? ?{_stream = stream;_fileName = fileName;} ?
???
???public void Dispose() ? ? ? ?=> _stream.Dispose();
???public Stream GetFileStream() ? ? ? ?=> _stream; ?
???public string GetName() ? ? ? ?=> _fileName;
內(nèi)存流附件實現(xiàn)要求在創(chuàng)建時傳遞一個 MemoryStream和附件名稱,比較簡單
物理文件附件
public class PhysicalFileAttachment : IAttachment{ ??public PhysicalFileAttachment(string absolutePath) ? ?{ ?
?
?? ? ?if (!File.Exists(absolutePath)){ ? ? ? ? ?
?? ? ??throw new FileNotFoundException("文件未找到", absolutePath);}AbsolutePath = absolutePath;} ? ?
?? ? ??private FileStream _stream; ?
?? ? ?? ?public string AbsolutePath { get; } ?
?? ? ?? ? ?public void Dispose() ? ?{_stream.Dispose();} ?
?? ? ? ?public Stream GetFileStream() ? ?{ ? ?
?? ? ? ? ? ?if (_stream == null){_stream = new FileStream(AbsolutePath, FileMode.Open);} ? ? ? ?return _stream;} ? ?public string GetName() ? ?{ ? ? ?
?? ? ? ? ? ? ?return System.IO.Path.GetFileName(AbsolutePath);... ? ?
這里,我們要注意的是創(chuàng)建FileStream的時機,是在請求GetFileStream方法時,而不是構(gòu)造函數(shù)中,因為創(chuàng)建FileStreamFileStream會占用文件,如果我們發(fā)兩封郵件使用了同一個附件,那么會拋出異常。而寫在GetFileStream方法中相對比較安全(除非發(fā)送器是并行的)
實現(xiàn)郵件隊列
在我們這篇文章中,我們實現(xiàn)的隊列提供器是基于內(nèi)存的,日后呢我們還可以實現(xiàn)其它的基于其它存儲模式的,比如數(shù)據(jù)庫,外部持久性隊列等等,另外基于內(nèi)存的實現(xiàn)不是持久的,一旦程序崩潰。未發(fā)出的郵件就會boom然后消失 XD...
郵件隊列提供器IMailQueueProvider實現(xiàn)
代碼如下:
public class MailQueueProvider : IMailQueueProvider{ ??private static readonly ConcurrentQueue<MailBox> _mailQueue = new ConcurrentQueue<MailBox>(); ? ?public int Count => _mailQueue.Count; ? ?public bool IsEmpty => _mailQueue.IsEmpty; ? ?public void Enqueue(MailBox mailBox) ? ?{_mailQueue.Enqueue(mailBox);} ? ?public bool TryDequeue(out MailBox mailBox) ? ?{ ? ? ? ?return _mailQueue.TryDequeue(out mailBox);}
本文的實現(xiàn)是一個 ConcurrentQueue
郵件服務(wù)IMailQueueService實現(xiàn)
代碼如下:
public class MailQueueService : IMailQueueService{ ? ?private readonly IMailQueueProvider _provider; ? ?/// <summary>/// 初始化實例/// </summary>/// <param name="provider"></param>public MailQueueService(IMailQueueProvider provider) ? ?{_provider = provider;} ? ?/// <summary>/// 入隊/// </summary>/// <param name="box"></param>public void Enqueue(MailBox box) ? ?{_provider.Enqueue(box);} ? ?這里,我們的服務(wù)依賴于IMailQueueProvider,使用了其入隊功能
郵件發(fā)送機IMailQueueManager實現(xiàn)
這個相對比較復雜,我們先看下完整的類,再逐步解釋:
public class MailQueueManager : IMailQueueManager{ ? ?private readonly SmtpClient _client; ? ?private readonly IMailQueueProvider _provider; ? ?private readonly ILogger<MailQueueManager> _logger; ? ?private readonly EmailOptions _options; ? ?private bool _isRunning = false; ? ?private bool _tryStop = false; ? ?private Thread _thread; ? ?/// <summary>/// 初始化實例/// </summary>/// <param name="provider"></param>/// <param name="options"></param>/// <param name="logger"></param>public MailQueueManager(IMailQueueProvider provider, IOptions<EmailOptions> options, ILogger<MailQueueManager> logger) ? ?{_options = options.Value;_client = new SmtpClient{ ? ? ? ? ? ?// For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)ServerCertificateValidationCallback = (s, c, h, e) => true}; ? ? ? ?// Note: since we don't have an OAuth2 token, disable// the XOAUTH2 authentication mechanism.if (_options.DisableOAuth){_client.AuthenticationMechanisms.Remove("XOAUTH2");}_provider = provider;_logger = logger;} ? ?/// <summary>/// 正在運行/// </summary>public bool IsRunning => _isRunning; ? ?/// <summary>/// 計數(shù)/// </summary>public int Count => _provider.Count; ? ?/// <summary>/// 啟動隊列/// </summary>public void Run() ? ?{ ? ? ? ?if (_isRunning || (_thread != null && _thread.IsAlive)){_logger.LogWarning("已經(jīng)運行,又被啟動了,新線程啟動已經(jīng)取消"); ? ? ? ? ? ?return;}_isRunning = true;_thread = new Thread(StartSendMail){Name = "PmpEmailQueue",IsBackground = true,};_logger.LogInformation("線程即將啟動");_thread.Start();_logger.LogInformation("線程已經(jīng)啟動,線程Id是:{0}", _thread.ManagedThreadId);} ? ?/// <summary>/// 停止隊列/// </summary>public void Stop() ? ?{ ? ? ? ?if (_tryStop){ ? ? ? ? ? ?return;}_tryStop = true;} ? ?private void StartSendMail() ? ?{ ? ? ? ?var sw = new Stopwatch(); ? ? ? ?try{ ? ? ? ? ? ?while (true){ ? ? ? ? ? ? ? ?if (_tryStop){ ? ? ? ? ? ? ? ? ? ?break;} ? ? ? ? ? ? ? ?if (_provider.IsEmpty){_logger.LogTrace("隊列是空,開始睡眠");Thread.Sleep(_options.SleepInterval); ? ? ? ? ? ? ? ? ? ?continue;} ? ? ? ? ? ? ? ?if (_provider.TryDequeue(out MailBox box)){_logger.LogInformation("開始發(fā)送郵件 標題:{0},收件人 {1}", box.Subject, box.To.First());sw.Restart();SendMail(box);sw.Stop();_logger.LogInformation("發(fā)送郵件結(jié)束標題:{0},收件人 {1},耗時{2}", box.Subject, box.To.First(), sw.Elapsed.TotalSeconds);}}} ? ? ? ?catch (Exception ex){_logger.LogError(ex, "循環(huán)中出錯,線程即將結(jié)束");_isRunning = false;}_logger.LogInformation("郵件發(fā)送線程即將停止,人為跳出循環(huán),沒有異常發(fā)生");_tryStop = false;_isRunning = false;} ? ?private void SendMail(MailBox box) ? ?{ ? ? ? ?if (box == null){ ? ? ? ? ? ?throw new ArgumentNullException(nameof(box));} ? ? ? ?try{MimeMessage message = ConvertToMimeMessage(box);SendMail(message);} ? ? ? ?catch (Exception exception){_logger.LogError(exception, "發(fā)送郵件發(fā)生異常主題:{0},收件人:{1}", box.Subject, box.To.First());} ? ? ? ?finally{ ? ? ? ? ? ?if (box.Attachments != null && box.Attachments.Any()){ ? ? ? ? ? ? ? ?foreach (var item in box.Attachments){item.Dispose();}}}} ? ?private MimeMessage ConvertToMimeMessage(MailBox box) ? ?{ ? ? ? ?var message = new MimeMessage(); ? ? ? ?var from = InternetAddress.Parse(_options.UserName); ? ? ? ?from.Name = _options.DisplayName;message.From.Add(from); ? ? ? ?if (!box.To.Any()){ ? ? ? ? ? ?throw new ArgumentNullException("to必須含有值");}message.To.AddRange(box.To.Convert()); ? ? ? ?if (box.Cc != null && box.Cc.Any()){message.Cc.AddRange(box.Cc.Convert());}message.Subject = box.Subject; ? ? ? ?var builder = new BodyBuilder(); ? ? ? ?if (box.IsHtml){builder.HtmlBody = box.Body;} ? ? ? ?else{builder.TextBody = box.Body;} ? ? ? ?if (box.Attachments != null && box.Attachments.Any()){ ? ? ? ? ? ?foreach (var item in GetAttechments(box.Attachments)) ? ? ? ? ? ?{builder.Attachments.Add(item);}}message.Body = builder.ToMessageBody(); ? ? ? ?return message;} ? ?private void SendMail(MimeMessage message) ? ?{ ? ? ? ?if (message == null){ ? ? ? ? ? ?throw new ArgumentNullException(nameof(message));} ? ? ? ?try{_client.Connect(_options.Host, _options.Port, false); ? ? ? ? ? ?// Note: only needed if the SMTP server requires authenticationif (!_client.IsAuthenticated){_client.Authenticate(_options.UserName, _options.Password);}_client.Send(message);} ? ? ? ?finally{_client.Disconnect(false);}} ? ?private AttachmentCollection GetAttechments(IEnumerable<IAttachment> attachments) ? ?{ ? ? ? ?if (attachments == null){ ? ? ? ? ? ?throw new ArgumentNullException(nameof(attachments));}AttachmentCollection collection = new AttachmentCollection();List<Stream> list = new List<Stream>(attachments.Count()); ? ? ? ?foreach (var item in attachments){ ? ? ? ? ? ?var fileName = item.GetName(); ? ? ? ? ? ?var fileType = MimeTypes.GetMimeType(fileName); ? ? ? ? ? ?var contentTypeArr = fileType.Split('/'); ? ? ? ? ? ?var contentType = new ContentType(contentTypeArr[0], contentTypeArr[1]);MimePart attachment = null;Stream fs = null; ? ? ? ? ? ?try{fs = item.GetFileStream();list.Add(fs);} ? ? ? ? ? ?catch (Exception ex){_logger.LogError(ex, "讀取文件流發(fā)生異常");fs?.Dispose(); ? ? ? ? ? ? ? ?continue;}attachment = new MimePart(contentType){Content = new MimeContent(fs),ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),ContentTransferEncoding = ContentEncoding.Base64,}; ? ? ? ? ? ?var charset = "UTF-8";attachment.ContentType.Parameters.Add(charset, "name", fileName);attachment.ContentDisposition.Parameters.Add(charset, "filename", fileName); ? ? ? ? ? ?foreach (var param in attachment.ContentDisposition.Parameters){param.EncodingMethod = ParameterEncodingMethod.Rfc2047;} ? ? ? ? ? ?foreach (var param in attachment.ContentType.Parameters){param.EncodingMethod = ParameterEncodingMethod.Rfc2047;}collection.Add(attachment);} ? ? ? ?return collection;} }在構(gòu)造函數(shù)中請求了另外三個服務(wù),并且初始化了SmtpClient(這是MailKit中的)
? ?public MailQueueManager(IMailQueueProvider provider, IOptions<EmailOptions> options, ILogger<MailQueueManager> logger){_options = options.Value;_client = new SmtpClient{ ? ? ? ? ? ?// For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)ServerCertificateValidationCallback = (s, c, h, e) => true}; ? ? ? ?// Note: since we don't have an OAuth2 token, disable// the XOAUTH2 authentication mechanism.if (_options.DisableOAuth){_client.AuthenticationMechanisms.Remove("XOAUTH2");}_provider = provider;_logger = logger;}啟動隊列時創(chuàng)建了新的線程,并且將線程句柄保存起來:
? ?public void Run() ? ?{ ? ? ? ?if (_isRunning || (_thread != null && _thread.IsAlive)){_logger.LogWarning("已經(jīng)運行,又被啟動了,新線程啟動已經(jīng)取消"); ? ? ? ? ? ?return;}_isRunning = true;_thread = new Thread(StartSendMail){Name = "PmpEmailQueue",IsBackground = true,};_logger.LogInformation("線程即將啟動");_thread.Start();_logger.LogInformation("線程已經(jīng)啟動,線程Id是:{0}", _thread.ManagedThreadId);}線程啟動時運行了方法StartSendMail:
? ?private void StartSendMail() ? ?{ ? ? ? ?var sw = new Stopwatch(); ? ? ? ?try{ ? ? ? ? ? ?while (true){ ? ? ? ? ? ? ? ?if (_tryStop){ ? ? ? ? ? ? ? ? ? ?break;} ? ? ? ? ? ? ? ?if (_provider.IsEmpty){_logger.LogTrace("隊列是空,開始睡眠");Thread.Sleep(_options.SleepInterval); ? ? ? ? ? ? ? ? ? ?continue;} ? ? ? ? ? ? ? ?if (_provider.TryDequeue(out MailBox box)){_logger.LogInformation("開始發(fā)送郵件 標題:{0},收件人 {1}", box.Subject, box.To.First());sw.Restart();SendMail(box);sw.Stop();_logger.LogInformation("發(fā)送郵件結(jié)束標題:{0},收件人 {1},耗時{2}", box.Subject, box.To.First(), sw.Elapsed.TotalSeconds);}}} ? ? ? ?catch (Exception ex){_logger.LogError(ex, "循環(huán)中出錯,線程即將結(jié)束");_isRunning = false;}_logger.LogInformation("郵件發(fā)送線程即將停止,人為跳出循環(huán),沒有異常發(fā)生");_tryStop = false;_isRunning = false;} ? ?這個方法不斷的從隊列讀取郵件并發(fā)送,當 遇到異常,或者_tryStop為true時跳出循環(huán),此時線程結(jié)束,注意我們會讓線程睡眠,在適當?shù)臅r候。
接下來就是方法SendMail了:
? ?private void SendMail(MailBox box) ? ?{ ? ? ? ?if (box == null){ ? ? ? ? ? ?throw new ArgumentNullException(nameof(box));} ? ? ? ?try{MimeMessage message = ConvertToMimeMessage(box);SendMail(message);} ? ? ? ?catch (Exception exception){_logger.LogError(exception, "發(fā)送郵件發(fā)生異常主題:{0},收件人:{1}", box.Subject, box.To.First());} ? ? ? ?finally{ ? ? ? ? ? ?if (box.Attachments != null && box.Attachments.Any()){ ? ? ? ? ? ? ? ?foreach (var item in box.Attachments){item.Dispose();... ? ? ? ? ? ? ? ?這里有一個特別要注意的就是在發(fā)送之后釋放附件(非托管資源):
foreach (var item in box.Attachments) {item.Dispose();...發(fā)送郵件的核心代碼只有兩行:
MimeMessage message = ConvertToMimeMessage(box);SendMail(message);第一行將mailbox轉(zhuǎn)換成 MailKit使用的MimeMessage實體,第二步切實的發(fā)送郵件
為什么,我們的接口中沒有直接使用MimeMessage而是使用MailBox?
因為MimeMessage比較繁雜,而且附件的問題不易處理,所以我們設(shè)計接口時單獨封裝MailBox簡化了編程接口
轉(zhuǎn)換一共兩步,1是主體轉(zhuǎn)換,比較簡單。二是附件的處理這里涉及到附件名中文編碼的問題。
? ?private MimeMessage ConvertToMimeMessage(MailBox box) ? ?{ ? ? ? ?var message = new MimeMessage(); ? ? ? ?var from = InternetAddress.Parse(_options.UserName); ? ? ? ?from.Name = _options.DisplayName;message.From.Add(from); ? ? ? ?if (!box.To.Any()){ ? ? ? ? ? ?throw new ArgumentNullException("to必須含有值");}message.To.AddRange(box.To.Convert()); ? ? ? ?if (box.Cc != null && box.Cc.Any()){message.Cc.AddRange(box.Cc.Convert());}message.Subject = box.Subject; ? ? ? ?var builder = new BodyBuilder(); ? ? ? ?if (box.IsHtml){builder.HtmlBody = box.Body;} ? ? ? ?else{builder.TextBody = box.Body;} ? ? ? ?if (box.Attachments != null && box.Attachments.Any()){ ? ? ? ? ? ?foreach (var item in GetAttechments(box.Attachments)) ? ? ? ? ? ?{builder.Attachments.Add(item);}}message.Body = builder.ToMessageBody(); ? ? ? ?return message;} ? ?private AttachmentCollection GetAttechments(IEnumerable<IAttachment> attachments) ? ?{ ? ? ? ?if (attachments == null){ ? ? ? ? ? ?throw new ArgumentNullException(nameof(attachments));}AttachmentCollection collection = new AttachmentCollection();List<Stream> list = new List<Stream>(attachments.Count()); ? ? ? ?foreach (var item in attachments){ ? ? ? ? ? ?var fileName = item.GetName(); ? ? ? ? ? ?var fileType = MimeTypes.GetMimeType(fileName); ? ? ? ? ? ?var contentTypeArr = fileType.Split('/'); ? ? ? ? ? ?var contentType = new ContentType(contentTypeArr[0], contentTypeArr[1]);MimePart attachment = null;Stream fs = null; ? ? ? ? ? ?try{fs = item.GetFileStream();list.Add(fs);} ? ? ? ? ? ?catch (Exception ex){_logger.LogError(ex, "讀取文件流發(fā)生異常");fs?.Dispose(); ? ? ? ? ? ? ? ?continue;}attachment = new MimePart(contentType){Content = new MimeContent(fs),ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),ContentTransferEncoding = ContentEncoding.Base64,}; ? ? ? ? ? ?var charset = "UTF-8";attachment.ContentType.Parameters.Add(charset, "name", fileName);attachment.ContentDisposition.Parameters.Add(charset, "filename", fileName); ? ? ? ? ? ?foreach (var param in attachment.ContentDisposition.Parameters){param.EncodingMethod = ParameterEncodingMethod.Rfc2047;} ? ? ? ? ? ?foreach (var param in attachment.ContentType.Parameters){param.EncodingMethod = ParameterEncodingMethod.Rfc2047;}collection.Add(attachment);} ? ? ? ?return collection;}在轉(zhuǎn)化附件時下面的代碼用來處理附件名編碼問題:
var charset = "UTF-8"; attachment.ContentType.Parameters.Add(charset, "name", fileName); attachment.ContentDisposition.Parameters.Add(charset, "filename", fileName);foreach (var param in attachment.ContentDisposition.Parameters) {param.EncodingMethod = ParameterEncodingMethod.Rfc2047; }foreach (var param in attachment.ContentType.Parameters) {param.EncodingMethod = ParameterEncodingMethod.Rfc2047; }到這了我們的郵件隊列就基本完成了,接下來就是在程序啟動后,啟動隊列,找到 Program.cs文件,并稍作改寫如下:
var host = BuildWebHost(args);var provider = host.Services; provider.GetRequiredService<IMailQueueManager>().Run(); host.Run();這里在host.Run()主機啟動之前,我們獲取了IMailQueueManager并啟動隊列(別忘了注冊服務(wù))。
運行程序我們會看到控制臺每隔3秒就會打出日志:
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]User profile is available. Using 'C:\Users\Administrator\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.info: MailQueueManager[0]線程即將啟動info: MailQueueManager[0]線程已經(jīng)啟動,線程Id是:9trce: MailQueueManager[0]隊列是空,開始睡眠 Hosting environment: Development Content root path: D:\publish Now listening on: http://[::]:5000Application started. Press Ctrl+C to shut down.trce: MailQueueManager[0]隊列是空,開始睡眠trce: MailQueueManager[0]隊列是空,開始睡眠到此,我們的郵件隊列就完成了! :D
原文地址:http://www.cnblogs.com/rocketRobin/p/9294845.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的Asp.Net Core 快速邮件队列设计与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#:如何将坏的代码重新编译为好的代码
- 下一篇: .net core redis 驱动推荐