Abp vNext 源码分析 - 11. 用户的自定义参数与配置

一、简要说明

文章信息:

基于的 ABP vNext 版本:1.0.0

创作日期:2019 年 10 月 23 日晚

更新日期:暂无

ABP vNext 针对用户可编辑的配置,提供了单独的 Volo.Abp.Settings 模块,本篇文章的后面都将这种用户可变更的配置,叫做 参数。所谓可编辑的配置,就是我们在系统页面上,用户可以动态更改的参数值。

例如你做的系统是一个门户网站,那么前端页面上展示的 Title ,你可以在后台进行配置。这个时候你就可以将网站这种全局配置作为一个参数,在程序代码中进行定义。通过 GlobalSettingValueProvider(后面会讲) 作为这个参数的值提供者,用户就可以随时对 Title 进行更改。又或者是某些通知的开关,你也可以定义一堆参数,让用户可以动态的进行变更。

二、源码分析

2.1 模块启动流程

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);
        });
    }
}

2.2 参数的定义

2.2.1 参数的基本定义

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;
}

2.2.2 参数定义的扫描

跟权限定义类似,所有的参数定义都被放在了 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 内部,方便后续进行调用。

2.2.3 参数定义的管理

我们的 参数定义提供者参数值提供者 都赋值给 AbpSettingOptions 了,首先看有哪些地方使用到了 参数定义提供者

1571827576154

第二个我们已经看过,是在模块启动时有用到。第一个则是有一个 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;
    }
}

2.3 参数值的管理

当我们构建好参数的定义之后,我们要设置某个参数的值,或者说获取某个参数的值应该怎么操作呢?查看相关的单元测试,看到了 ABP vNext 自身是注入 ISettingProvider ,调用它的 GetOrNullAsync() 获取参数值。

private readonly ISettingProvider _settingProvider;

var settingValue = await _settingProvider.GetOrNullAsync("WebSite.Title")

跳转到接口,发现它有两个实现,这里我们只讲解一下 SettingProvider 类的实现。

1571829369817

2.3.1 获取参数值

直奔主题,来看一下 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

2.3.2 参数值提供者

参数值提供者的接口定义是 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

1571832002740

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

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());
    }
}

2.3.3 参数值的存储

除了 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

2.3.4 参数值的设置

在 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 相关文章?点击我 即可跳转到总目录。