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