一、簡介
ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包將簡訊和電子郵件作為基礎設施進行了抽象,開發人員僅需要在使用的時候注入 ISmsSender
或 IEmailSender
即可實現簡訊傳送和郵件傳送。
二、原始碼分析
2.1 啟動模組
簡訊傳送的抽象層比較簡單,AbpSmsModule
模組內部並無任何操作,僅作為空模組進行定義。
電子郵件的 AbpEmailingModule
模組內,主要添加了一些本地化資源支援。另一個動作就是添加了一個 BackgroundEmailSendingJob
後臺作業,這個後臺作業主要是用於後續傳送電子郵件使用。因為郵件傳送這個動作實時性要求並不高,在實際的業務實踐當中,我們基本會將其加入到一個後臺佇列慢慢傳送,所以這裡 ABP 為我們實現了 BackgroundEmailSendingJob
。
BackgroundEmailSendingJob.cs:
public class BackgroundEmailSendingJob : AsyncBackgroundJob<BackgroundEmailSendingJobArgs>, ITransientDependency
{
protected IEmailSender EmailSender { get; }
public BackgroundEmailSendingJob(IEmailSender emailSender)
{
EmailSender = emailSender;
}
public override async Task ExecuteAsync(BackgroundEmailSendingJobArgs args)
{
if (args.From.IsNullOrWhiteSpace())
{
await EmailSender.SendAsync(args.To, args.Subject, args.Body, args.IsBodyHtml);
}
else
{
await EmailSender.SendAsync(args.From, args.To, args.Subject, args.Body, args.IsBodyHtml);
}
}
}
這個後臺任務的邏輯也不復雜,就使用 IEmailSender
傳送郵件,我們在任何地方需要後臺傳送郵件的時,只需要注入 IBackgroundJobManager
,使用 BackgroundEmailSendingJobArgs
作為引數新增入隊一個後臺作業即可。
使用 IBackgroundJobManager
新增一個新的郵件傳送歡迎郵件:
public class DemoClass
{
private readonly IBackgroundJobManager _backgroundJobManager;
private readonly IUserInfoRepository _userRep;
public DemoClass(IBackgroundJobManager backgroundJobManager,
IUserInfoRepository userRep)
{
_backgroundJobManager = backgroundJobManager;
_userRep = userRep;
}
public async Task SendWelcomeEmailAsync(Guid userId)
{
var userInfo = await _userRep.GetByIdAsync(userId);
await _backgroundJobManager.EnqueueAsync(new BackgroundEmailSendingJobArgs
{
To = userInfo.EmailAddress,
Subject = "Welcome",
Body = "Welcome, Hello World!",
IsBodyHtml = false;
});
}
}
注意
目前
BackgroundEmailSendingJobArgs
引數不支援傳送附件,ABP 可能在以後的版本會進行實現。
2.2 Email 的核心元件
ABP 定義了一個 IEmailSender
介面,定義了多個 SendAsync()
方法過載,用於直接傳送電子郵件。同時也提供了 QueueAsync()
方法,通過後臺任務佇列來發送郵件。
public interface IEmailSender
{
Task SendAsync(
string to,
string subject,
string body,
bool isBodyHtml = true
);
Task SendAsync(
string from,
string to,
string subject,
string body,
bool isBodyHtml = true
);
Task SendAsync(
MailMessage mail,
bool normalize = true
);
Task QueueAsync(
string to,
string subject,
string body,
bool isBodyHtml = true
);
Task QueueAsync(
string from,
string to,
string subject,
string body,
bool isBodyHtml = true
);
//TODO: 準備新增的 QueueAsync 方法。目前存在的問題: MailMessage 不能夠被序列化,所以不能加入到後臺任務隊列當中。
}
ABP 實際擁有兩種 Email Sender 實現,分別是 SmtpEmailSender
和 MailkitEmailSender
,各個型別的關係如下。
UML 類圖:
class IEmailSender{
<<Interface>>
+SendAsync(string,string,string,bool=true) Task
+SendAsync(string,string,string,string,bool=true) Task
+SendAsync(MailMessage,bool=true) Task
+QueueAsync(string,string,string,bool=true) Task
+QueueAsync(string,string,string,string,bool=true) Task
}
class ISmtpEmailSender{
<<Interface>>
......
+BuildClientAsync() Task~SmtpClient~
}
class IMailKitSmtpEmailSemder{
<<Interface>>
......
+BuildClientAsync() Task~SmtpClient~
}
class EmailSenderBase{
<<Abstract>>
......
}
class SmtpEmailSender{
......
}
class MailKitSmtpEmailSender{
......
}
class NullEmailSender{
......
}
ISmtpEmailSender --|> IEmailSender: 繼承
IMailKitSmtpEmailSemder --|> IEmailSender: 繼承
EmailSenderBase ..|> IEmailSender: 實現
SmtpEmailSender ..|> ISmtpEmailSender: 實現
SmtpEmailSender --|> EmailSenderBase: 繼承
NullEmailSender --|> EmailSenderBase: 繼承
MailKitSmtpEmailSender ..|> IMailKitSmtpEmailSemder: 實現
MailKitSmtpEmailSender --|> EmailSenderBase: 繼承
可以從 UML 類圖看出,每個 EmailSender 實現都與一個 IXXXConfiguration
對應,這個配置類儲存了基於 Smtp 發件的必須配置。因為 MailKit 本身也是基於 Smtp 傳送郵件的,所以沒有重新定義新的配置類,而是直接複用的 ISmtpEmailSenderConfiguration
介面與實現。
在 EmailSenderBase
基類當中,基本實現了 IEmailSender
介面的所有方法的邏輯,只留下了 SendEmailAsync(MailMessage mail)
作為一個抽象方法等待子類實現。也就是說其他的方法最終都是使用該方法來最終傳送郵件。
public abstract class EmailSenderBase : IEmailSender
{
protected IEmailSenderConfiguration Configuration { get; }
protected IBackgroundJobManager BackgroundJobManager { get; }
protected EmailSenderBase(IEmailSenderConfiguration configuration, IBackgroundJobManager backgroundJobManager)
{
Configuration = configuration;
BackgroundJobManager = backgroundJobManager;
}
// ... 實現的介面方法
protected abstract Task SendEmailAsync(MailMessage mail);
// 使用 Configuration 裡面的引數,統一處理郵件資料。
protected virtual async Task NormalizeMailAsync(MailMessage mail)
{
if (mail.From == null || mail.From.Address.IsNullOrEmpty())
{
mail.From = new MailAddress(
await Configuration.GetDefaultFromAddressAsync(),
await Configuration.GetDefaultFromDisplayNameAsync(),
Encoding.UTF8
);
}
if (mail.HeadersEncoding == null)
{
mail.HeadersEncoding = Encoding.UTF8;
}
if (mail.SubjectEncoding == null)
{
mail.SubjectEncoding = Encoding.UTF8;
}
if (mail.BodyEncoding == null)
{
mail.BodyEncoding = Encoding.UTF8;
}
}
}
ABP 預設可用的郵件傳送元件是 SmtpEmailSender
,它使用的是 .NET 自帶的郵件傳送元件,本質上就是構建了一個 SmtpClient
客戶端,然後呼叫它的發件方法進行郵件傳送。
public class SmtpEmailSender : EmailSenderBase, ISmtpEmailSender, ITransientDependency
{
// ... 省略的程式碼。
public async Task<SmtpClient> BuildClientAsync()
{
var host = await SmtpConfiguration.GetHostAsync();
var port = await SmtpConfiguration.GetPortAsync();
var smtpClient = new SmtpClient(host, port);
// 從 SettingProvider 中獲取各個配置引數,構建 Client 進行傳送。
try
{
if (await SmtpConfiguration.GetEnableSslAsync())
{
smtpClient.EnableSsl = true;
}
if (await SmtpConfiguration.GetUseDefaultCredentialsAsync())
{
smtpClient.UseDefaultCredentials = true;
}
else
{
smtpClient.UseDefaultCredentials = false;
var userName = await SmtpConfiguration.GetUserNameAsync();
if (!userName.IsNullOrEmpty())
{
var password = await SmtpConfiguration.GetPasswordAsync();
var domain = await SmtpConfiguration.GetDomainAsync();
smtpClient.Credentials = !domain.IsNullOrEmpty()
? new NetworkCredential(userName, password, domain)
: new NetworkCredential(userName, password);
}
}
return smtpClient;
}
catch
{
smtpClient.Dispose();
throw;
}
}
protected override async Task SendEmailAsync(MailMessage mail)
{
// 呼叫構建方法,構建 Client,用於傳送 mail 資料。
using (var smtpClient = await BuildClientAsync())
{
await smtpClient.SendMailAsync(mail);
}
}
}
針對屬性注入失敗的情況,ABP 提供了 NullEmailSender
作為預設實現,在傳送郵件的時候會使用 Logger 列印具體的資訊。
public class NullEmailSender : EmailSenderBase
{
public ILogger<NullEmailSender> Logger { get; set; }
public NullEmailSender(IEmailSenderConfiguration configuration, IBackgroundJobManager backgroundJobManager)
: base(configuration, backgroundJobManager)
{
Logger = NullLogger<NullEmailSender>.Instance;
}
protected override Task SendEmailAsync(MailMessage mail)
{
Logger.LogWarning("USING NullEmailSender!");
Logger.LogDebug("SendEmailAsync:");
LogEmail(mail);
return Task.FromResult(0);
}
// ... 其他方法。
}
2.3 Email 的配置儲存
從 EmailSenderBase
裡面可以看到,它從 IEmailSenderConfiguration
當中獲取發件人的郵箱地址和展示名稱,它的 UML 類圖關係如下。
class IEmailSenderConfiguration{
<<Interface>>
+GetDefaultFromAddressAsync() Task~string~
+GetDefaultFromDisplayNameAsync() Task~string~
}
class ISmtpEmailSenderConfiguration{
<<Interface>>
+GetHostAsync() Task~string~
+GetPortAsync() Task~int~
+GetUserNameAsync() Task~string~
+GetPasswordAsync() Task~string~
+GetDomainAsync() Task~string~
+GetEnableSslAsync() Task~bool~
+GetUseDefaultCredentialsAsync() Task~bool~
}
class EmailSenderConfiguration{
#GetNotEmptySettingValueAsync(string name) Task~string~
}
class SmtpEmailSenderConfiguration{
}
class ISettingProvider{
<<Interface>>
+GetOrNullAsync(string name) Task~string~
}
ISmtpEmailSenderConfiguration --|> IEmailSenderConfiguration: 繼承
EmailSenderConfiguration ..|> IEmailSenderConfiguration: 實現
EmailSenderConfiguration ..> ISettingProvider: 依賴
SmtpEmailSenderConfiguration --|> EmailSenderConfiguration: 繼承
SmtpEmailSenderConfiguration ..|> ISmtpEmailSenderConfiguration: 實現
可以看到配置檔案時通過 ISettingProvider
獲取的,這樣就可以保證從不同租戶甚至是使用者來獲取發件人的配置資訊。這裡值得注意的是在 EmailSenderConfiguration
中,實現了一個 GetNotEmptySettingValueAsync(string name)
方法,該方法主要是封裝了獲取邏輯,當值不存在的時候丟擲 AbpException
異常。
protected async Task<string> GetNotEmptySettingValueAsync(string name)
{
var value = await SettingProvider.GetOrNullAsync(name);
if (value.IsNullOrEmpty())
{
throw new AbpException($"Setting value for '{name}' is null or empty!");
}
return value;
}
至於 SmtpEmailSenderConfiguration
,只是提供了其他的屬性獲取(密碼、埠等)而已,本質上還是呼叫的 GetNotEmptySettingValueAsync()
方法從 SettingProvider
中獲取具體的配置資訊。
傳送郵件 ->> Smtp 配置類: 1.GetHostAsync()
Smtp 配置類 ->> Email 配置類: 2.GetNotEmptySettingValueAsync("HotsItem")
Email 配置類 ->> Setting Provider: 3.GetOrNullAsync("HotsItem")
Setting Provider -->> 傳送郵件: 4.獲得主機資料。
關於配置名稱的常量,都在 EmailSettingNames
裡面進行定義,並使用 EmailSettingProvider
將其註冊到 ABP 的配置模組當中:
EmailSettingNames.cs
namespace Volo.Abp.Emailing
{
public static class EmailSettingNames
{
public const string DefaultFromAddress = "Abp.Mailing.DefaultFromAddress";
public const string DefaultFromDisplayName = "Abp.Mailing.DefaultFromDisplayName";
public static class Smtp
{
public const string Host = "Abp.Mailing.Smtp.Host";
public const string Port = "Abp.Mailing.Smtp.Port";
// ... 其他常量定義。
}
}
}
EmailSettingProvider.cs
internal class EmailSettingProvider : SettingDefinitionProvider
{
public override void Define(ISettingDefinitionContext context)
{
context.Add(
new SettingDefinition(
EmailSettingNames.Smtp.Host,
"127.0.0.1",
L("DisplayName:Abp.Mailing.Smtp.Host"),
L("Description:Abp.Mailing.Smtp.Host")),
new SettingDefinition(EmailSettingNames.Smtp.Port,
"25",
L("DisplayName:Abp.Mailing.Smtp.Port"),
L("Description:Abp.Mailing.Smtp.Port")),
// ... 其他配置引數。
);
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<EmailingResource>(name);
}
}
2.4 郵件模板
文字模板是 ABP 後續提供的一個新的模組,它可以讓開發人員預先定義文字模板,然後使用時根據物件資料替換模板中的內容,並且 ABP 提供的文字模板還支援本地化。關於文字模板的功能,我們後續單獨會寫一篇文章進行說明,在這裡只是大概 Mail 是如何使用的。
在專案當中,ABP 僅定義了兩個 *.tpl 的模板檔案,分別是控制佈局的 Layout.tpl,還有渲染具體訊息的 Message.tpl。同許可權、Setting 一樣,模板也會使用一個 StandardEmailTemplates
型別定義模板的編碼常量,並且實現一個 XXXDefinitionProvider
型別將其注入到 ABP 框架當中。
StandardEmailTemplates.cs
public static class StandardEmailTemplates
{
public const string Layout = "Abp.StandardEmailTemplates.Layout";
public const string Message = "Abp.StandardEmailTemplates.Message";
}
StandardEmailTemplateDefinitionProvider.cs
public class StandardEmailTemplateDefinitionProvider : TemplateDefinitionProvider
{
public override void Define(ITemplateDefinitionContext context)
{
context.Add(
new TemplateDefinition(
StandardEmailTemplates.Layout,
displayName: LocalizableString.Create<EmailingResource>("TextTemplate:StandardEmailTemplates.Layout"),
isLayout: true
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Layout.tpl", true)
);
context.Add(
new TemplateDefinition(
StandardEmailTemplates.Message,
displayName: LocalizableString.Create<EmailingResource>("TextTemplate:StandardEmailTemplates.Message"),
layout: StandardEmailTemplates.Layout
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Message.tpl", true)
);
}
}
2.5 MailKit 整合
MailKit 是一個優秀跨平臺的 .NET 郵件操作庫,它的官方 GitHub 地址為 https://github.com/jstedfast/MailKit ,支援很多高階特性,這裡我就不再詳細介紹 MailKit 的其他特性,只是講解一下 MailKit 同 ABP 自帶的郵件模組是如何整合的。
官方的 Volo.Abp.MailKit 包僅包含 4 個檔案,它們分別是 AbpMailKitModule.cs (空模組,佔位)、AbpMailKitOptions.cs (MailKit 的特殊配置)、IMailKitSmtpEmailSender.cs (實現了 IEmailSender
基類的一個介面)、MailKitSmtpEmailSender.cs (具體的傳送邏輯實現)。
需要注意一下,這裡針對 MailKit 的特殊配置是使用的 IConfiguration
裡面的資料(通常是 appsetting.json),而不是從 Abp.Settings 裡面獲取的。
MailKitSmtpEmailSender.cs
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
public class MailKitSmtpEmailSender : EmailSenderBase, IMailKitSmtpEmailSender
{
protected AbpMailKitOptions AbpMailKitOptions { get; }
protected ISmtpEmailSenderConfiguration SmtpConfiguration { get; }
// ... 建構函式。
protected override async Task SendEmailAsync(MailMessage mail)
{
using (var client = await BuildClientAsync())
{
// 使用了 mail 引數來構造 MailKit 的物件。
var message = MimeMessage.CreateFromMailMessage(mail);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
// 構造 MailKit 所需要的 Client 物件。
public async Task<SmtpClient> BuildClientAsync()
{
var client = new SmtpClient();
try
{
await ConfigureClient(client);
return client;
}
catch
{
client.Dispose();
throw;
}
}
// 進行一些基本配置,比如伺服器資訊和密碼資訊等。
protected virtual async Task ConfigureClient(SmtpClient client)
{
await client.ConnectAsync(
await SmtpConfiguration.GetHostAsync(),
await SmtpConfiguration.GetPortAsync(),
await GetSecureSocketOption()
);
if (await SmtpConfiguration.GetUseDefaultCredentialsAsync())
{
return;
}
await client.AuthenticateAsync(
await SmtpConfiguration.GetUserNameAsync(),
await SmtpConfiguration.GetPasswordAsync()
);
}
// 根據 Option 的值獲取一些安全配置。
protected virtual async Task<SecureSocketOptions> GetSecureSocketOption()
{
if (AbpMailKitOptions.SecureSocketOption.HasValue)
{
return AbpMailKitOptions.SecureSocketOption.Value;
}
return await SmtpConfiguration.GetEnableSslAsync()
? SecureSocketOptions.SslOnConnect
: SecureSocketOptions.StartTlsWhenAvailable;
}
}
三、總結
ABP 將 Email 這塊功能封裝成了單獨的模組,便於開發人員進行郵件傳送。並且官方也提供了 MailKit 的支援,我們可以根據自己的需求來替換不同的實現。只不過針對於一些非同步郵件傳送的場景,目前還不能很好的支援(主要是使用了 MailMessage
無法序列化)。
我覺得 ABP 應該自己定義一個 Context 型別,反轉依賴,在具體的實現當中確定郵件傳送的物件型別。或者是將預設的 Smtp 傳送者獨立出來一個模組,就跟 MailKit 一樣,使用 ABP 的 Context 型別來構造 MailMessage
物件。
四、總目錄
歡迎翻閱作者的其他文章,請 點選我 進行跳轉,如果你覺得本篇文章對你有幫助,請點選文章末尾的 推薦按鈕。
最後更新時間: 2021年6月27日 23點31分