一、簡要說明
文章資訊:
基于的 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 = https://www.cnblogs.com/myzony/p/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();
Providers = new List();
}
// 設定附加資料值,
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 = https://www.cnblogs.com/myzony/p/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 = https://www.cnblogs.com/myzony/p/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 = https://www.cnblogs.com/myzony/p/await GetOrNullValueFromProvidersAsync(providers, setting);
// 如果引數是加密的,則需要進行解密操作,
if (setting.IsEncrypted)
{
value = SettingEncryptionService.Decrypt(setting, value);
}
return value;
}
protected virtual async Task GetOrNullValueFromProvidersAsync(IEnumerable 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 = https://www.cnblogs.com/myzony/p/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來獲取的,建議加入分布式快取,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/101938.html
標籤:.NET Core
