一、简要说明

文章信息:

基于的 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;
    }
}

上面的参数定义值得注意的就是 defaultvalueisvisibletoclientsisencrypted 这三个属性。默认值一般适用于某些系统配置,例如当前系统的默认语言。后面两个属性则更加注重于 安全问题,因为某些参数存储的是一些重要信息,这个时候就需要进行特殊处理了。

如果参数值是加密的,那么在获取参数值的时候就会进行解密操作,例如下面的代码。

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 实现了 isettingdefinitionprovideritransientdependency 接口,所以这些 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 为我们预先定义了四种参数值提供器,他们分别是 defaultvaluesettingvalueproviderglobalsettingvalueprovidertenantsettingvalueproviderusersettingvalueprovider

下面我们就来讲讲这几个不同的参数提供者有啥不一样。

defaultvaluesettingvalueprovider

顾名思义,默认值参数提供者就是使用的参数定义里面的 defaultvalue 属性,当你查询某个参数值的时候,就直接返回了。

public override task<string> getornullasync(settingdefinition setting)
{
    return task.fromresult(setting.defaultvalue);
}

globalsettingvalueprovider

这是一种全局的提供者,它没有对应的 key,也就是说如果数据库能查到 providernameg 的记录,就直接返回它的值了。

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 的总结,很详细:

  1. 在各个模块中定义设置数据源的类来设定配置键值对, 该类只需要继承接口 isettingdefinitionprovider 或者 settingdefinitionprovider 实现类
    abp 会自动寻找被注册,最后会将配置键值对都汇总到 settingprovider 类中。如果是存储在数据库中的,则需要重写 isettingstore
    当然建议依赖 volo.abp.settingmanagement.domain 这个模块,如果数据表是用自定义的,则建议重写 isettingrepository 接口即可。

  2. configureservices() 方法中注册添加 isettingvalueprovider,比如:值是 json 格式的,就可以定义一个设置值 provider 来解析。

  3. isettingvalueprovider 可以有多个,并且按倒序进行执行,只要能获取到值就返回,不再继续往下执行。一般自定义的 isettingvalueprovider 放在后面。

  4. 如果将敏感数据保存到设置管理,则建议采用加密的方式,只需要重写 isettingencryptionservice 即可。 参数定义:isencrypted = true

  5. volo.abp.settingmanagement.domain 是采用数据库加缓存的方式来读写设置的,
    通过 settingcacheiteminvalidator 来注册 setting 实体的 entitychanged 事件,从而达到缓存能跟实体同步更新。

  6. 为啥 abp 还需要设置管理,而不用 .net core 自带的配置(configuration)?
    因为 abp 设置管理可以做到三个层级,用户,租户和全局(系统级),同时 abp 的设置管理只是做了一层封装,
    具体的数据源可以是 .net core 自带的配置(configuration),也可以是分布式配置。只不过需要我们自己去写扩展。
  7. 另外建议大家对参数进行打包,比如邮件相关的参数可以封装在一个 emailconfig类中,邮件 host,用户名和密码都是该类的属性,而具体取值同时通过 isettingvalueprovider 来获取的。建议加入分布式缓存。