[Abp vNext 源码分析] - 11. 用户的自定义参数与配置
一、简要说明
文章信息:
基于的 abp vnext 版本:1.0.0
创作日期:2019 年 10 月 23 日晚
更新日期:暂无
abp vnext 针对用户可编辑的配置,提供了单独的 volo.abp.settings 模块,本篇文章的后面都将这种用户可变更的配置,叫做 参数。所谓可编辑的配置,就是我们在系统页面上,用户可以动态更改的参数值。
例如你做的系统是一个门户网站,那么前端页面上展示的 title ,你可以在后台进行配置。这个时候你就可以将网站这种全局配置作为一个参数,在程序代码中进行定义。通过 globalsettingvalueprovider
(后面会讲) 作为这个参数的值提供者,用户就可以随时对 title 进行更改。又或者是某些通知的开关,你也可以定义一堆参数,让用户可以动态的进行变更。
二、源码分析
模块启动流程
abpsettingsmodule
模块干的事情只有两件,第一是扫描所有 isettingdefinitionprovider
(参数定义提供者),第二则是往配置参数添加一堆参数值提供者(isettingvalueprovider
)。
public class abpsettingsmodule : abpmodule { public override void preconfigureservices(serviceconfigurationcontext context) { // 自动扫描所有实现了 isettingdefinitionprovider 的类型。 autoadddefinitionproviders(context.services); } public override void configureservices(serviceconfigurationcontext context) { // 配置默认的一堆参数值提供者。 configure<abpsettingoptions>(options => { options.valueproviders.add<defaultvaluesettingvalueprovider>(); options.valueproviders.add<globalsettingvalueprovider>(); options.valueproviders.add<tenantsettingvalueprovider>(); options.valueproviders.add<usersettingvalueprovider>(); }); } private static void autoadddefinitionproviders(iservicecollection services) { var definitionproviders = new list<type>(); services.onregistred(context => { if (typeof(isettingdefinitionprovider).isassignablefrom(context.implementationtype)) { definitionproviders.add(context.implementationtype); } }); // 将扫描到的数据添加到 options 中。 services.configure<abpsettingoptions>(options => { options.definitionproviders.addifnotcontains(definitionproviders); }); } }
参数的定义
参数的基本定义
abp vnext 关于参数的定义在类型 settingdefinition
可以找到,内部的结构与 permissiondefine
类似。。开发人员需要先定义有哪些可配置的参数,然后 abp vnext 会自动进行管理,在网站运行期间,用户、租户可以根据自己的需要随时变更参数值。
public class settingdefinition { /// <summary> /// 参数的唯一标识。 /// </summary> [notnull] public string name { get; } // 参数的显示名称,是一个多语言字符串。 [notnull] public ilocalizablestring displayname { get => _displayname; set => _displayname = check.notnull(value, nameof(value)); } private ilocalizablestring _displayname; // 参数的描述信息,也是一个多语言字符串。 [canbenull] public ilocalizablestring description { get; set; } /// <summary> /// 参数的默认值。 /// </summary> [canbenull] public string defaultvalue { get; set; } /// <summary> /// 指定参数与其参数的值,是否能够在客户端进行显示。对于某些密钥设置来说是很危险的,默认值为 fasle。 /// </summary> public bool isvisibletoclients { get; set; } /// <summary> /// 允许更改本参数的值提供者,为空则允许所有提供者提供参数值。 /// </summary> public list<string> providers { get; } //todo: 考虑重命名为 allowedproviders。 /// <summary> /// 当前参数是否能够继承父类的 scope 信息,默认值为 true。 /// </summary> public bool isinherited { get; set; } /// <summary> /// 参数相关连的一些扩展属性,通过一个字典进行存储。 /// </summary> [notnull] public dictionary<string, object> properties { get; } /// <summary> /// 参数的值是否以加密的形式存储,默认值为 false。 /// </summary> public bool isencrypted { get; set; } public settingdefinition( string name, string defaultvalue = null, ilocalizablestring displayname = null, ilocalizablestring description = null, bool isvisibletoclients = false, bool isinherited = true, bool isencrypted = false) { name = name; defaultvalue = defaultvalue; isvisibletoclients = isvisibletoclients; displayname = displayname ?? new fixedlocalizablestring(name); description = description; isinherited = isinherited; isencrypted = isencrypted; properties = new dictionary<string, object>(); providers = new list<string>(); } // 设置附加数据值。 public virtual settingdefinition withproperty(string key, object value) { properties[key] = value; return this; } // 设置 provider 属性的值。 public virtual settingdefinition withproviders(params string[] providers) { if (!providers.isnullorempty()) { providers.addrange(providers); } return this; } }
上面的参数定义值得注意的就是 defaultvalue
、isvisibletoclients
、isencrypted
这三个属性。默认值一般适用于某些系统配置,例如当前系统的默认语言。后面两个属性则更加注重于 安全问题,因为某些参数存储的是一些重要信息,这个时候就需要进行特殊处理了。
如果参数值是加密的,那么在获取参数值的时候就会进行解密操作,例如下面的代码。
settingprovider
类中的相关代码:
// ... public class settingprovider : isettingprovider, itransientdependency { // ... public virtual async task<string> getornullasync(string name) { // ... var value = await getornullvaluefromprovidersasync(providers, setting); // 对值进行解密处理。 if (setting.isencrypted) { value = settingencryptionservice.decrypt(setting, value); } return value; } // ... }
参数不对客户端可见的话,在默认的 abpapplicationconfigurationappservice
服务类中,获取参数值的时候就会跳过。
private async task<applicationsettingconfigurationdto> getsettingconfigasync() { var result = new applicationsettingconfigurationdto { values = new dictionary<string, string>() }; foreach (var settingdefinition in _settingdefinitionmanager.getall()) { // 不会展示这些属性为 false 的参数。 if (!settingdefinition.isvisibletoclients) { continue; } result.values[settingdefinition.name] = await _settingprovider.getornullasync(settingdefinition.name); } return result; }
参数定义的扫描
跟权限定义类似,所有的参数定义都被放在了 settingdefinitionprovider
里面,如果你需要定义一堆参数,只需要继承并实现 define(isettingdefinitioncontext)
抽象方法就可以了。
public class testsettingdefinitionprovider : settingdefinitionprovider { public override void define(isettingdefinitioncontext context) { context.add( new settingdefinition(testsettingnames.testsettingwithoutdefaultvalue), new settingdefinition(testsettingnames.testsettingwithdefaultvalue, "default-value"), new settingdefinition(testsettingnames.testsettingencrypted, isencrypted: true) ); } }
因为我们的 settingdefinitionprovider
实现了 isettingdefinitionprovider
和 itransientdependency
接口,所以这些 provider 都会在组件注册的时候(模块里面有定义),添加到对应的 abpsettingoptions
内部,方便后续进行调用。
参数定义的管理
我们的 参数定义提供者 和 参数值提供者 都赋值给 abpsettingoptions
了,首先看有哪些地方使用到了 参数定义提供者。
第二个我们已经看过,是在模块启动时有用到。第一个则是有一个 settingdefinitionmanager
,顾名思义就是管理所有的 settingdefinition
的管理器。这个管理器提供了三个方法,都是针对 settingdefinition
的查询功能。
public interface isettingdefinitionmanager { // 根据参数定义的标识查询,不存在则抛出 abpexception 异常。 [notnull] settingdefinition get([notnull] string name); // 获得所有的参数定义。 ireadonlylist<settingdefinition> getall(); // 根据参数定义的标识查询,如果不存在则返回 null。 settingdefinition getornull(string name); }
接下来我们看一下它的默认实现 settingdefinitionmanager
,它的内部没什么说的,只是注意 settingdefinitions
的填充方式,这里使用了线程安全的 懒加载模式。只有当用到的时候,才会调用 createsettingdefinitions()
方法填充数据。
public class settingdefinitionmanager : isettingdefinitionmanager, isingletondependency { protected lazy<idictionary<string, settingdefinition>> settingdefinitions { get; } protected abpsettingoptions options { get; } protected iserviceprovider serviceprovider { get; } public settingdefinitionmanager( ioptions<abpsettingoptions> options, iserviceprovider serviceprovider) { serviceprovider = serviceprovider; options = options.value; // 填充的时候,调用 createsettingdefinitions 方法进行填充。 settingdefinitions = new lazy<idictionary<string, settingdefinition>>(createsettingdefinitions, true); } // ... protected virtual idictionary<string, settingdefinition> createsettingdefinitions() { var settings = new dictionary<string, settingdefinition>(); using (var scope = serviceprovider.createscope()) { // 从 options 中得到类型,然后通过 ioc 进行实例化。 var providers = options .definitionproviders .select(p => scope.serviceprovider.getrequiredservice(p) as isettingdefinitionprovider) .tolist(); // 执行每个 provider 的 define 方法填充数据。 foreach (var provider in providers) { provider.define(new settingdefinitioncontext(settings)); } } return settings; } }
参数值的管理
当我们构建好参数的定义之后,我们要设置某个参数的值,或者说获取某个参数的值应该怎么操作呢?查看相关的单元测试,看到了 abp vnext 自身是注入 isettingprovider
,调用它的 getornullasync()
获取参数值。
private readonly isettingprovider _settingprovider; var settingvalue = await _settingprovider.getornullasync("website.title")
跳转到接口,发现它有两个实现,这里我们只讲解一下 settingprovider
类的实现。
获取参数值
直奔主题,来看一下 isettingprovider.getornullasync(string)
方法是怎么来获取参数值的。
public class settingprovider : isettingprovider, itransientdependency { protected isettingdefinitionmanager settingdefinitionmanager { get; } protected isettingencryptionservice settingencryptionservice { get; } protected isettingvalueprovidermanager settingvalueprovidermanager { get; } public settingprovider( isettingdefinitionmanager settingdefinitionmanager, isettingencryptionservice settingencryptionservice, isettingvalueprovidermanager settingvalueprovidermanager) { settingdefinitionmanager = settingdefinitionmanager; settingencryptionservice = settingencryptionservice; settingvalueprovidermanager = settingvalueprovidermanager; } public virtual async task<string> getornullasync(string name) { // 根据名称获取参数定义。 var setting = settingdefinitionmanager.get(name); // 从参数值提供者管理器,获得一堆参数值提供者。 var providers = enumerable .reverse(settingvalueprovidermanager.providers); // 过滤符合参数定义的提供者,这里就是用到了之前参数定义的 list<string> providers 属性。 if (setting.providers.any()) { providers = providers.where(p => setting.providers.contains(p.name)); } //todo: how to implement setting.isinherited? //todo: 如何实现 setting.isinherited 功能? var value = await getornullvaluefromprovidersasync(providers, setting); // 如果参数是加密的,则需要进行解密操作。 if (setting.isencrypted) { value = settingencryptionservice.decrypt(setting, value); } return value; } protected virtual async task<string> getornullvaluefromprovidersasync(ienumerable<isettingvalueprovider> providers, settingdefinition setting) { // 只要从任意 provider 中,读取到了参数值,就直接进行返回。 foreach (var provider in providers) { var value = await provider.getornullasync(setting); if (value != null) { return value; } } return null; } // ... }
所以真正干活的还是 isettingvalueprovidermanager
里面存放的一堆 isettingvalueprovider
,这个 参数值管理器 的接口很简单,只提供了一个 list<isettingvalueprovider> providers { get; }
的定义。
它会从模块配置的 valueproviders
属性内部,通过 ioc 实例化对应的参数值提供者。
_lazyproviders = new lazy<list<isettingvalueprovider>>( () => options .valueproviders .select(type => serviceprovider.getrequiredservice(type) as isettingvalueprovider) .tolist(), true
参数值提供者
参数值提供者的接口定义是 isettingvalueprovider
,它定义了一个名称和 getornullasync(settingdefinition)
方法,后者可以通过参数定义获取存储的值。
public interface isettingvalueprovider { string name { get; } task<string> getornullasync([notnull] settingdefinition setting); }
注意这里的返回值是 task<string>
,也就是说我们的参数值类型必须是 string
类型的,如果需要存储其他的类型可能就需要从 string
进行类型转换了。
在这里的 settingvalueprovider
其实类似于我们之前讲过的 权限提供者。因为 abp vnext 考虑到了多种情况,我们的参数值有可能是根据用户获取的,同时也有可能是根据不同的租户进行获取的。所以 abp vnext 为我们预先定义了四种参数值提供器,他们分别是 defaultvaluesettingvalueprovider
、globalsettingvalueprovider
、tenantsettingvalueprovider
、usersettingvalueprovider
。
下面我们就来讲讲这几个不同的参数提供者有啥不一样。
defaultvaluesettingvalueprovider
:
顾名思义,默认值参数提供者就是使用的参数定义里面的 defaultvalue
属性,当你查询某个参数值的时候,就直接返回了。
public override task<string> getornullasync(settingdefinition setting) { return task.fromresult(setting.defaultvalue); }
globalsettingvalueprovider
:
这是一种全局的提供者,它没有对应的 key,也就是说如果数据库能查到 providername
是 g
的记录,就直接返回它的值了。
public class globalsettingvalueprovider : settingvalueprovider { public const string providername = "g"; public override string name => providername; public globalsettingvalueprovider(isettingstore settingstore) : base(settingstore) { } public override task<string> getornullasync(settingdefinition setting) { return settingstore.getornullasync(setting.name, name, null); } }
tenantsettingvalueprovider
:
租户提供者,则是会将当前登录租户的 id 结合 t
进行查询,也就是参数值是按照不同的租户进行隔离的。
public class tenantsettingvalueprovider : settingvalueprovider { public const string providername = "t"; public override string name => providername; protected icurrenttenant currenttenant { get; } public tenantsettingvalueprovider(isettingstore settingstore, icurrenttenant currenttenant) : base(settingstore) { currenttenant = currenttenant; } public override async task<string> getornullasync(settingdefinition setting) { return await settingstore.getornullasync(setting.name, name, currenttenant.id?.tostring()); } }
usersettingvalueprovider
:
用户提供者,则是会将当前用户的 id 作为查询条件,结合 u
在数据库进行查询匹配的参数值,参数值是根据不同的用户进行隔离的。
public class usersettingvalueprovider : settingvalueprovider { public const string providername = "u"; public override string name => providername; protected icurrentuser currentuser { get; } public usersettingvalueprovider(isettingstore settingstore, icurrentuser currentuser) : base(settingstore) { currentuser = currentuser; } public override async task<string> getornullasync(settingdefinition setting) { if (currentuser.id == null) { return null; } return await settingstore.getornullasync(setting.name, name, currentuser.id.tostring()); } }
参数值的存储
除了 defaultvaluesettingvalueprovider
是直接从参数定义获取值以外,其他的参数值提供者都是通过 isettingstore
读取参数值的。在该模块的默认实现当中,是直接返回 null
的,只有当你使用了 volo.abp.settingmanagement 模块,你的参数值才是存储到数据库当中的。
我这里不再详细解析 volo.abp.settingmanagement 模块的其他实现,只说一下 isettingstore
在它内部的实现 settingstore
。
public class settingstore : isettingstore, itransientdependency { protected isettingmanagementstore managementstore { get; } public settingstore(isettingmanagementstore managementstore) { managementstore = managementstore; } public task<string> getornullasync(string name, string providername, string providerkey) { return managementstore.getornullasync(name, providername, providerkey); } }
我们可以看到它也只是个包装,真正的操作类型是 isettingmanagementstore
。
参数值的设置
在 abp vnext 的核心模块当中,是没有提供对参数值的变更的。只有在 volo.abp.settingmanagement 模块内部,它提供了 isettingmanager
管理器,可以进行参数值的变更。原理很简单,就是对数据库对应的表进行修改而已。
public async task setasync(string name, string value, string providername, string providerkey) { // 操作仓储,查询记录。 var setting = await settingrepository.findasync(name, providername, providerkey); // 新增或者更新记录。 if (setting == null) { setting = new setting(guidgenerator.create(), name, value, providername, providerkey); await settingrepository.insertasync(setting); } else { setting.value = value; await settingrepository.updateasync(setting); } }
三、总结
abp vnext 提供了多种参数值提供者,我们可以根据自己的需要灵活选择。如果不能够满足你的需求,你也可以自己实现一个参数值提供者。我建议对于用户在界面可更改的参数,都可以使用 settingdefinition
定义成参数,可以根据不同的情况进行配置读取。
abp vnext 其他模块用到的许多参数,也都是使用的 settingdefinition
进行定义。例如 identity 模块用到的密码验证规则,就是通过 isettingprovider
进行读取的,还有当前程序的默认语言。
需要看其他的 abp vnext 相关文章? 即可跳转到总目录。
下面附上 e2home 的总结,很详细:
在各个模块中定义设置数据源的类来设定配置键值对, 该类只需要继承接口
isettingdefinitionprovider
或者settingdefinitionprovider
实现类
abp 会自动寻找被注册,最后会将配置键值对都汇总到settingprovider
类中。如果是存储在数据库中的,则需要重写isettingstore
当然建议依赖 volo.abp.settingmanagement.domain 这个模块,如果数据表是用自定义的,则建议重写isettingrepository
接口即可。在
configureservices()
方法中注册添加isettingvalueprovider
,比如:值是 json 格式的,就可以定义一个设置值 provider 来解析。isettingvalueprovider
可以有多个,并且按倒序进行执行,只要能获取到值就返回,不再继续往下执行。一般自定义的 isettingvalueprovider 放在后面。如果将敏感数据保存到设置管理,则建议采用加密的方式,只需要重写
isettingencryptionservice
即可。 参数定义:isencrypted = true
。volo.abp.settingmanagement.domain 是采用数据库加缓存的方式来读写设置的,
通过settingcacheiteminvalidator
来注册 setting 实体的entitychanged
事件,从而达到缓存能跟实体同步更新。- 为啥 abp 还需要设置管理,而不用 .net core 自带的配置(configuration)?
因为 abp 设置管理可以做到三个层级,用户,租户和全局(系统级),同时 abp 的设置管理只是做了一层封装,
具体的数据源可以是 .net core 自带的配置(configuration),也可以是分布式配置。只不过需要我们自己去写扩展。 另外建议大家对参数进行打包,比如邮件相关的参数可以封装在一个
emailconfig
类中,邮件 host,用户名和密码都是该类的属性,而具体取值同时通过isettingvalueprovider
来获取的。建议加入分布式缓存。