• [Abp vNext 源码分析]


    一、简介

    ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包将短信和电子邮件作为基础设施进行了抽象,开发人员仅需要在使用的时候注入 ISmsSenderIEmailSender 即可实现短信发送和邮件发送。

    二、源码分析

    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 实现,分别是 SmtpEmailSenderMailkitEmailSender,各个类型的关系如下。

    UML 类图:

    classDiagram 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 类图关系如下。

    classDiagram 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 中获取具体的配置信息。

    sequenceDiagram 发送邮件 ->> 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;
        }
    }
    

    2.6 短信发送的核心组件

    短信发送仅提供了一个 ISmsSender 接口,该接口有提供一个发送方法,ABP 官方提供了 Aliyun 的短信发送功能(Volo.Abp.Sms.Aliyun)。

    UML 图:

    classDiagram class ISmsSender{ <<Interface>> SendAsync(SmsMessage smsMessage) Task } class NullSmsSender{ } class SmsMessage{ +string PhoneNumber +string Text +IDictionary~string, object~ Properties } NullSmsSender ..|> ISmsSender: 实现 ISmsSender ..> SmsMessage: 依赖

    功能比较简单,重点是 SmsMessage 里面的参数,第一个是发送的号码,第二个是发送的内容。仅凭上述参数肯定不够,所以 ABP 提供了一个属性字典,便于我们传入一些特定的参数。

    三、总结

    ABP 将 Email 这块功能封装成了单独的模块,便于开发人员进行邮件发送。并且官方也提供了 MailKit 的支持,我们可以根据自己的需求来替换不同的实现。只不过针对于一些异步邮件发送的场景,目前还不能很好的支持(主要是使用了 MailMessage 无法序列化)。

    我觉得 ABP 应该自己定义一个 Context 类型,反转依赖,在具体的实现当中确定邮件发送的对象类型。或者是将默认的 Smtp 发送者独立出来一个模块,就跟 MailKit 一样,使用 ABP 的 Context 类型来构造 MailMessage 对象。

    四、总目录

    欢迎翻阅作者的其他文章,请 点击我 进行跳转,如果你觉得本篇文章对你有帮助,请点击文章末尾的 推荐按钮

    最后更新时间: 2021年6月27日 23点31分

  • 相关阅读:
    Python中列表
    Python中For循环
    While循环
    python中if else流程判断
    python中get pass用法
    python学习
    Forbidden Attack:7万台web服务器陷入被攻击的险境
    爱恨交织!我们经常抱怨却离不开的7种语言
    玩转大数据,你需要了解这8种项目类型!
    如何用 Python 实现 Web 抓取?
  • 原文地址:https://www.cnblogs.com/myzony/p/abp-vnext-email-and-sms-source-analyzsis.html
Copyright © 2020-2023  润新知