文章信息:html
基於的 ABP vNext 版本:1.0.0前端
創做日期:2019 年 10 月 23 日晚數據庫
更新日期:2019 年 10 月 24 日json
ABP vNext 針對用戶可編輯的配置,提供了單獨的 Volo.Abp.Settings 模塊,本篇文章的後面都將這種用戶可變動的配置,叫作 參數。所謂可編輯的配置,就是咱們在系統頁面上,用戶能夠動態更改的參數值。緩存
例如你作的系統是一個門戶網站,那麼前端頁面上展現的 Title ,你能夠在後臺進行配置。這個時候你就能夠將網站這種全局配置做爲一個參數,在程序代碼中進行定義。經過 GlobalSettingValueProvider
(後面會講) 做爲這個參數的值提供者,用戶就能夠隨時對 Title 進行更改。又或者是某些通知的開關,你也能夠定義一堆參數,讓用戶能夠動態的進行變動。安全
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; } }
上面的參數定義值得注意的就是 DefaultValue
、IsVisibleToClients
、IsEncrypted
這三個屬性。默認值通常適用於某些系統配置,例如當前系統的默認語言。後面兩個屬性則更加註重於 安全問題,由於某些參數存儲的是一些重要信息,這個時候就須要進行特殊處理了。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
實現了 ISettingDefinitionProvider
和 ITransientDependency
接口,因此這些 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 爲咱們預先定義了四種參數值提供器,他們分別是 DefaultValueSettingValueProvider
、GlobalSettingValueProvider
、TenantSettingValueProvider
、UserSettingValueProvider
。
下面咱們就來說講這幾個不一樣的參數提供者有啥不同。
DefaultValueSettingValueProvider
:
顧名思義,默認值參數提供者就是使用的參數定義裏面的 DefaultValue
屬性,當你查詢某個參數值的時候,就直接返回了。
public override Task<string> GetOrNullAsync(SettingDefinition setting) { return Task.FromResult(setting.DefaultValue); }
GlobalSettingValueProvider
:
這是一種全局的提供者,它沒有對應的 Key,也就是說若是數據庫能查到 ProviderName
是 G
的記錄,就直接返回它的值了。
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 的總結,很詳細:
在各個模塊中定義設置數據源的類來設定配置鍵值對, 該類只須要繼承接口 ISettingDefinitionProvider
或者 SettingDefinitionProvider
實現類
ABP 會自動尋找被註冊,最後會將配置鍵值對都彙總到 SettingProvider
類中。若是是存儲在數據庫中的,則須要重寫 ISettingStore
固然建議依賴 Volo.Abp.SettingManagement.Domain 這個模塊,若是數據表是用自定義的,則建議重寫 ISettingRepository
接口便可。
在 ConfigureServices()
方法中註冊添加 ISettingValueProvider
,好比:值是 json 格式的,就能夠定義一個設置值 Provider 來解析。
ISettingValueProvider
能夠有多個,而且按倒序進行執行,只要能獲取到值就返回,再也不繼續往下執行。通常自定義的 ISettingValueProvider 放在後面。
若是將敏感數據保存到設置管理,則建議採用加密的方式,只須要重寫 ISettingEncryptionService
便可。 參數定義:IsEncrypted = true
。
Volo.Abp.SettingManagement.Domain 是採用數據庫加緩存的方式來讀寫設置的,
經過 SettingCacheItemInvalidator
來註冊 Setting 實體的 EntityChanged
事件,從而達到緩存能跟實體同步更新。
另外建議你們對參數進行打包,好比郵件相關的參數能夠封裝在一個 EmailConfig
類中,郵件 Host,用戶名和密碼都是該類的屬性,而具體取值同時經過 ISettingValueProvider
來獲取的。建議加入分佈式緩存。