一、簡要說明
文章信息:
基於的 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;
}
}
上面的參數定義值得注意的就是 DefaultValue
、IsVisibleToClients
、IsEncrypted
這三個屬性。默認值一般適用於某些系統配置,例如當前系統的默認語言。后面兩個屬性則更加注重於 安全問題,因為某些參數存儲的是一些重要信息,這個時候就需要進行特殊處理了。
如果參數值是加密的,那么在獲取參數值的時候就會進行解密操作,例如下面的代碼。
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
事件,從而達到緩存能跟實體同步更新。 -
為啥 ABP 還需要設置管理,而不用 .NET Core 自帶的配置(Configuration)?
因為 ABP 設置管理可以做到三個層級,用戶,租戶和全局(系統級),同時 ABP 的設置管理只是做了一層封裝,
具體的數據源可以是 .NET Core 自帶的配置(Configuration),也可以是分布式配置。只不過需要我們自己去寫擴展。 -
另外建議大家對參數進行打包,比如郵件相關的參數可以封裝在一個
EmailConfig
類中,郵件 Host,用戶名和密碼都是該類的屬性,而具體取值同時通過ISettingValueProvider
來獲取的。建議加入分布式緩存。