- A+
一、简介
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 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分