

[Abp vNext 源码分析] - 20. 电子邮件与短信支持
source link: https://www.cnblogs.com/myzony/p/abp-vnext-email-and-sms-source-analyzsis.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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分
Recommend
-
100
README.md ABP
-
16
Abp 源码分析(4):Unit of work 2020-01-092020-01-10Abp 工作单元是”对象-关系”行为的一种模式。维护受业务事务影响的对象列表,...
-
26
Abp 的 DI 容器是基于 Microsoft 的依赖注入扩展库(Microsoft.Extensions.DependencyInjection nuget包)开发的。因此,它的文档在 A...
-
17
我和ABP vNext 的故事 Abp VNext是Abp的.NET Core 版本,但它不仅仅只是代码重写了...
-
12
2021-04-079 16 min.在 上一篇 博客中,博主和大家分享了如何在 EF Core 中...
-
8
ABP vNext 的实体与服务扩展技巧分享2021-04-182 20 min.使用 ABP vNext 有一个月左右啦,这中间最大的一个收获是:ABP vNext 的开发效率真的是非常好,只要你愿意取遵循它模块化、DDD...
-
5
系列文章列表,点击展示/隐藏本文梯子正文 本章节对 ABP 框架进行一个简单的介绍,摘自ABP官方,后面会在使用过程中对各个知识点进行细致的讲解。 领域驱动设计 领域驱动设计(简称...
-
12
五、Abp vNext 基础篇丨博客聚合功能 系列文章列表,点击展示/隐藏...
-
16
Abp Vnext Vue3 的版本实现 Abp Vnext Pro 的 Vue3 实现版本 开箱即用的中后台前端/设计解决方案
-
9
ABP VNext框架基础知识介绍(2)--微服务的网关 ABP VNext框架如果不考虑在微服务上的应用,也就是开发单体应用解决方案,虽然也是模块化开发,但其集成使用的难度会降低一个层级,不过ABP VNext和ABP框架一样,基础内容都会设计很多内容,如数据库都支...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK