1. 程式人生 > >[Abp vNext 原始碼分析] - 11. 使用者的自定義引數與配置

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

上面的引數定義值得注意的就是 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 相關文章?點選我 即可跳轉到總目錄