[Abp vNext 源碼分析] - 11. 用戶的自定義參數與配置

1、簡要說明

文章信息:html

基於的 ABP vNext 版本:1.0.0前端

創做日期:2019 年 10 月 23 日晚數據庫

更新日期:2019 年 10 月 24 日json

ABP vNext 針對用戶可編輯的配置,提供了單獨的 Volo.Abp.Settings 模塊,本篇文章的後面都將這種用戶可變動的配置,叫作 參數。所謂可編輯的配置,就是咱們在系統頁面上,用戶能夠動態更改的參數值。緩存

例如你作的系統是一個門戶網站,那麼前端頁面上展現的 Title ,你能夠在後臺進行配置。這個時候你就能夠將網站這種全局配置做爲一個參數,在程序代碼中進行定義。經過 GlobalSettingValueProvider(後面會講) 做爲這個參數的值提供者,用戶就能夠隨時對 Title 進行更改。又或者是某些通知的開關,你也能夠定義一堆參數,讓用戶能夠動態的進行變動。安全

2、源碼分析

模塊啓動流程

AbpSettingsModule 模塊乾的事情只有兩件,第一是掃描全部 ISettingDefinitionProvider (參數定義提供者),第二則是往配置參數添加一堆參數值提供者(ISettingValueProvider)。async

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 這三個屬性。默認值通常適用於某些系統配置,例如當前系統的默認語言。後面兩個屬性則更加註重於 安全問題,由於某些參數存儲的是一些重要信息,這個時候就須要進行特殊處理了。ide

若是參數值是加密的,那麼在獲取參數值的時候就會進行解密操做,例以下面的代碼。源碼分析

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

3、總結

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 來獲取的。建議加入分佈式緩存。

相關文章
相關標籤/搜索