一、簡介
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 類圖:
可以從 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 類圖關系如下。
可以看到配置文件時通過 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
中獲取具體的配置信息。
關於配置名稱的常量,都在 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;
}
}
2.6 短信發送的核心組件
短信發送僅提供了一個 ISmsSender
接口,該接口有提供一個發送方法,ABP 官方提供了 Aliyun 的短信發送功能(Volo.Abp.Sms.Aliyun)。
UML 圖:
功能比較簡單,重點是 SmsMessage
里面的參數,第一個是發送的號碼,第二個是發送的內容。僅憑上述參數肯定不夠,所以 ABP 提供了一個屬性字典,便於我們傳入一些特定的參數。
三、總結
ABP 將 Email 這塊功能封裝成了單獨的模塊,便於開發人員進行郵件發送。並且官方也提供了 MailKit 的支持,我們可以根據自己的需求來替換不同的實現。只不過針對於一些異步郵件發送的場景,目前還不能很好的支持(主要是使用了 MailMessage
無法序列化)。
我覺得 ABP 應該自己定義一個 Context 類型,反轉依賴,在具體的實現當中確定郵件發送的對象類型。或者是將默認的 Smtp 發送者獨立出來一個模塊,就跟 MailKit 一樣,使用 ABP 的 Context 類型來構造 MailMessage
對象。
四、總目錄
歡迎翻閱作者的其他文章,請 點擊我 進行跳轉,如果你覺得本篇文章對你有幫助,請點擊文章末尾的 推薦按鈕。
最后更新時間: 2021年6月27日 23點31分