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


一、簡要說明

文章信息:

基於的 ABP vNext 版本:1.0.0

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

更新日期:2019 年 10 月 24 日

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM