文章目錄
前言
這邊文章主要是對netcore中的配置框架做一個實戰詳解,篇幅較長內容涉及比較多,請耐心閱讀並進行嘗試,均采用控制台程序進行展示。
環境:
netcore 3.1.4
win10
vs 2019 16.5.5
1、依賴項安裝
以下所有依賴項是包含了配置框架中主要用到的依賴項。
主要是以下兩個包:
- Microsoft.Extensions.Configuration.Abstractions 配置框架抽象包
- Microsoft.Extensions.Configuration 實現包
配置框架中幾個重要的對象:
- IConfigurationBuilder
- IConfigurationRoot
- IConfiguration
- IConfigurationProvider
其他的都主要是配置框架中的擴展項。下面介紹到相關的時候會給出是依賴於那個包,耐心閱讀。
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.4" />
</ItemGroup>
2、根路徑輸出
在使用添加文件的配置時需要設置程序根目錄,這里做了一個添加根目錄的匯總以及輸出展示。
可以看到AppContext.BaseDirectory
和AppDomain.CurrentDomain.BaseDirectory
輸出了根目錄絕對路徑(后面多了\
),而Environment.CurrentDirectory
和Directory.GetCurrentDirectory()
輸出后面沒有\
。
有\
的意義表示bin文件夾里的所有文件和文件夾;反之,bin文件夾里的所有文件和與bin同節的文件
一般都是使用Directory.GetCurrentDirectory()進行設置根目錄,這里嘗試了添加文件配置的時候四種方式都是可以的。
3、注冊各種配置方式
下面代碼塊展示了添加各種配置的方式。
添加文件的時候有三個參數,第一個參數路徑,第二個參數該文件是否可選,第三個參數是否熱更新(文件發生變化配置自動更新)。
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory());
//Microsoft.Extensions.Configuration.Json包
builder.AddJsonFile("appsetting.json", optional: false, reloadOnChange: true);
//Microsoft.Extensions.Configuration.Ini包
builder.AddIniFile("appsetting.ini", optional: false, reloadOnChange: true);
//Microsoft.Extensions.Configuration包
builder.AddInMemoryCollection(new Dictionary<string, string>
{
{"Name","Jonny" },
{"Age","25" },
{"Gender","Male" },
{"Address:Address1","重慶奉節" },
{"Address:Address2","重慶渝北" }
});
//Microsoft.Extensions.Configuration.CommandLine包
builder.AddCommandLine(args);
//Microsoft.Extensions.Configuration.EnvironmentVariables包
builder.AddEnvironmentVariables();
方法 | 依賴包 | 說明 |
---|---|---|
AddJsonFile | Microsoft.Extensions.Configuration.Json | 通過Json文件,aspnet core中最常用的配置方式appsetting.json |
AddIniFile | Microsoft.Extensions.Configuration.Ini | ini文件配置方式 |
AddInMemoryCollection | Microsoft.Extensions.Configuration | 內存中配置 |
AddCommandLine | Microsoft.Extensions.Configuration.CommandLine | 命令行 |
AddEnvironmentVariables | Microsoft.Extensions.Configuration.EnvironmentVariables | 環境變量,環境變量在windows中使用key1:key2:value的方式進行層級之分。而在Linux中:使用雙下划線__代替,在編寫代碼的時候照樣使用:取,只是在添加環境變量的時候使用__ |
3.1 內存配置的讀取
上面在內容中添加了配置,這里進行讀取。
#region 內存配置的讀取
{
Console.WriteLine("=======================內存配置的讀取======================");
var configRoot = builder.Build();
Console.WriteLine($"Name:{configRoot["Name"]}");
Console.WriteLine($"Age:{configRoot["Age"]}");
Console.WriteLine($"Gender:{configRoot["Gender"]}");
//這里讀取出Address節點下內容
var addressRoot = configRoot.GetSection("Address");
Console.WriteLine($"Address__Address1:{addressRoot["Address1"]}");
Console.WriteLine($"Address__Address2:{addressRoot["Address2"]}");
}
#endregion
通過IConfiguration
的GetSection()
方法獲取節點塊,再通過配置Key名稱獲取對應的值,在IConfiguration
中也可以通過:
來分層讀取,這里介紹了使用key的方式讀取后下面在介紹其他方式的時候就不做過多介紹。
例如:
這里的Address1可以直接通過
configRoot["Address:Address1"]
讀取。
最終的結果展示:
3.2 JSON配置的讀取
配置文件appsetting.json內容如下:
{
"AppConfig": {
"RemoteService": "http://localhost:44371",
"Hospital": {
"Name": "重慶市婦幼保健院 一分院",
"Tel": "023-56781234",
"Level": 3
}
}
}
這里層級比較深,專為上面的GetSection()
方法和:
再一次驗證。
測試代碼:
#region JSON配置的讀取
{
Console.WriteLine("=======================JSON配置的讀取======================");
var configRoot = builder.Build();
var appConfigRoot = configRoot.GetSection("AppConfig");
Console.WriteLine($"AppConfig__RemoteService:{appConfigRoot["RemoteService"]}");
var hospitalRoot = appConfigRoot.GetSection("Hospital");
Console.WriteLine($"AppConfig__Hospital__Name:{hospitalRoot["Name"]}");
Console.WriteLine($"AppConfig__Hospital__Tel:{hospitalRoot["Tel"]}");
//這里通過一步到位獲取一個值
Console.WriteLine($"AppConfig__Hospital__Level:{configRoot["AppConfig:Hospital:Level"]}");
//解釋一下這里為什么用__來分隔,因為在Linux中:就是用雙下划線__來替換的
}
#endregion
最終效果展示:
3.3 INI配置的讀取
文件內容:
[服務器]
RemoteService=http://localhost:44372
Account=admin
測試代碼:
#region INI配置的讀取
{
Console.WriteLine("=======================INI配置的讀取======================");
var configRoot = builder.Build();
Console.WriteLine($"服務器__RemoteService:{configRoot["服務器:RemoteService"]}");
Console.WriteLine($"服務器__Account:{configRoot["服務器:Account"]}");
}
#endregion
3.4 命令行配置
#region 命令行配置
{
Console.WriteLine("=======================命令行配置======================");
//Microsoft.Extensions.Configuration.CommandLine包
builder.AddCommandLine(args);
var configRoot = builder.Build();
Console.WriteLine($"CommandLine1:{configRoot["CommandLine1"]}");
Console.WriteLine($"CommandLine2:{configRoot["CommandLine2"]}");
}
#endregion
通過項目屬性–>調試–>添加命令行
也可以使用launchSettings.json
文件添加命令行
{
"profiles": {
"Jonny.AllDemo.OptionsConfig": {
"commandName": "Project",
"commandLineArgs": "CommandLine1=key1 --CommandLine2=key2 /CommandLine3=key3 --c=newKey1",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"launchSetting.json": {
"commandName": "Executable"
}
}
}
commandLineArgs
節點配置命令行。
在dotnet-cli中我們可以看到–help命也可以使用-h代替,那么我們也可以這樣操作。
在上面的添加命令行方法AddCommandLine()
第二個參數加入進去,使用-c代替CommandLine1命令。
//使用 - c替換掉CommandLine1
var replaceCommond = new Dictionary<string, string>
{
{"-c","CommandLine1" }
};
builder.AddCommandLine(args, replaceCommond);
修改命令行:
"commandLineArgs": "CommandLine1=key1 --CommandLine2=key2 /CommandLine3=key3 -c=newKey1"
將上面的–c改成-c。可以看到輸出值變成了newKey1,不再是==key1 ==
3.5 環境變量
- 項目中添加環境變量
一般在asp.net core直接在文件中通過environmentVariables
添加環境變量。
- 系統環境變量
- 讀取
4、實體綁定配置
通過上面的各種方式來測試了配置框架中實現,但是在開發中一般不會這么操作 ,而是通過實體的綁定來進行操作的,在實體中操作又要涉及到netcore 框架中的依賴注入,本能篇幅就不對依賴注入進行展開,后面會更新。
這里我們定義配置實體IdentityClientOption
,新增配置文件內容。
"IdentityClients": {
"Default": {
"GrantType": "password",
"ClientId": "Jonny.AllDemo.OptionsConfig",
"ClientSecret": "1q2w3E*",
"UserName": "admin",
"UserPassword": "1q2w3E*",
"Authority": "https://localhost:44371",
"Scope": "OptionsConfig"
}
}
public class IdentityClientOption
{
public string GrantType { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string UserName { get; set; }
public string UserPassword { get; set; }
public string Authority { get; set; }
public string Scope { get; set; }
public override string ToString()
{
return $"GrantType:{GrantType},ClientId:{ClientId},ClientSecret:{ClientSecret},UserName:{UserName},UserPassword:{UserPassword},Authority:{Authority},Scope:{Scope}";
}
}
為了方便測試我們這里增加一個IUserAppService
接口,其實也是可以直接通過ServiceProvider
獲取進行測試,但是在官方文檔中這樣的操作是返回的,應該使用注入的方式進行獲取。
測試服務代碼:
public class AppUser
{
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
public override string ToString()
{
return $"Name:{Name},Age:{Age},Gender:{Gender}";
}
}
public enum Gender
{
Unkonw,
Male,
Famale
}
public interface IUserAppService
{
List<AppUser> GetUsers();
}
public class UserAppService : IUserAppService
{
protected readonly IdentityClientOption _identityMonitor;
protected readonly IdentityClientOption _identitySnapshot;
protected readonly IdentityClientOption _identity;
public UserAppService(IOptionsMonitor<IdentityClientOption> optionsMonitor,
IOptionsSnapshot<IdentityClientOption> optionsSnapshot,
IOptions<IdentityClientOption> options)
{
_identityMonitor = optionsMonitor?.CurrentValue;
_identitySnapshot = optionsSnapshot?.Value;
_identity = options?.Value;
Console.WriteLine($"Monitor:\t{_identityMonitor?.ToString()}");
Console.WriteLine($"Snapshot:\t{_identitySnapshot?.ToString()}");
Console.WriteLine($"Options:\t{_identity?.ToString()}");
}
public List<AppUser> GetUsers()
{
return new List<AppUser>
{
new AppUser
{
Name="Jonny",
Age=25,
Gender=Gender.Male
}
};
}
}
在測試之前我這里引入Host
概念,這是asp.net core中的宿主靜類。
宿主主要有兩個依賴包:
- Microsoft.Extensions.Hosting.Abstractions 抽象包
- Microsoft.Extensions.Hosting 實現包
測試代碼:
#region 實體綁定+驗證+修改配置
{
var configRoot = builder.Build();
Host.CreateDefaultBuilder(args).ConfigureServices(services =>
{
services.AddSingleton<IUserAppService, UserAppService>();
var defaultOption = configRoot.GetSection("IdentityClients:Default");
//綁定實體
{
//這種方式任何生命周期注冊都可以使用IOptions IOptionsSnapshot IOptionsMonitor
services.AddOptions<IdentityClientOption>().Bind(defaultOption);
//使用Configure的方式綁定
services.Configure<IdentityClientOption>(defaultOption);
//測試獲取
var userApp = services.BuildServiceProvider().GetRequiredService<IUserAppService>();
}
}).Build().Run();
}
通過ServiceProvider
獲取容器中的服務時會自動調用構造,具體服務之間的構造先后順序以及涉及到的生命周期我這里就不再展開了。下面結果可以看到配置文件綁定到了IdentityClientOption
中。
上面代碼中可以看到使用了兩種方式綁定:
//這種方式任何生命周期注冊都可以使用IOptions IOptionsSnapshot IOptionsMonitor
services.AddOptions<IdentityClientOption>().Bind(defaultOption);
//使用Configure的方式綁定
services.Configure<IdentityClientOption>(defaultOption);
而使用IOptionsMonitor
時使用CurrentValue,其他兩個使用Value。
4.1 實體綁定驗證
4.1.1 Validate()方法驗證
這里測試之間將前面的appsetting.json內容GrantType值改成Client。
//驗證--通過Validate()方法驗證
services.AddOptions<IdentityClientOption>()
.Validate(option =>
{
if (option.GrantType == "password")
{
return true;
}
return false;
});
下面會拋出一個錯誤,這樣配置中加入驗證是為了程序部署的時候配置不成功不讓啟動,這樣保證了程序正確性。
4.1.2 實現IValidateOptions添加驗證
這種驗證方式也是常用的驗證方式,這樣可以對復雜的配置項進行驗證,驗證代碼統一 管理,單一職責性。
public class IdentityValitate : IValidateOptions<IdentityClientOption>
{
public ValidateOptionsResult Validate(string name, IdentityClientOption options)
{
if (options.GrantType=="password")
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail("驗證方式不是password模式");
}
}
添加驗證到服務中:
//驗證-- > 通過注冊Validator來驗證
services.AddSingleton<IValidateOptions<IdentityClientOption>, IdentityValitate>();
4.1.3 ValidateDataAnnotations()
安裝Microsoft.Extensions.Options.DataAnnotations包,對於這個包相比大家都不是很陌生,以前MVC開發中模型驗證都會用到DataAnnotations下的特性。
配置屬性上增加驗證。
5、配置熱更新
有時候項目上線后需要用到不停機的情況下修改配置,這樣就要用到熱更新。
5.1 IChangeToken注冊
#region 使用ChangeToken熱更新監控配置改變
{
var configRoot = builder.Build();
var token = configRoot.GetReloadToken();
token.RegisterChangeCallback(state =>
{
Console.WriteLine($"【{configRoot["AppConfig:Hospital:Name"]}】服務配置發生了變化一次");
var token1 = configRoot.GetReloadToken();
token1.RegisterChangeCallback(state1=>
{
Console.WriteLine($"【{configRoot["AppConfig:Hospital:Name"]}】服務配置發生了變化兩次");
}, configRoot);
}, configRoot);
Console.WriteLine($"【{configRoot["AppConfig:Hospital:Name"]}】服務啟動完成");
}
#endregion
上面我直接注冊了兩次RegisterChangeCallback()
方法,當我第一次修改文件Name值保存會輸出更改值,當后面再更改后就沒有發生變化。
所以,使用IChangeToken注冊的只能觸發第一次的更改變化,這樣顯然是達不到要求的。下面會接受另外的方式。
5.2 靜態類ChangeToken
使用靜態類ChangeToken
的OnChange()
方法進行監控。
ChangeToken.OnChange(() => configRoot.GetReloadToken(), () =>
{
Console.WriteLine($"【{configRoot["AppConfig:Hospital:Name"]}】服務配置發生了變化");
});
修改bin\Debug文件夾下的配置文件進行測試,測試的過程中發現只要觸發了文件的保存操作都會觸發OnChange()
方法,無論內容是否變化,不知這里是一個什么原因???
注意,由於我這里測試使用的是控制台應用程序,需要修改bin\Debug文件下面的配置文件才能生效,但是使用asp.net core就不用,直接修改項目中的配置文件就可以。
5.3 IOptionsSnapshot和IOptionsMonitor
由於我這里是控制台應用程序,我這里采用RegisterChangeCallback()
方法來借助測試,通過更改后重新獲取IUserAppService
,輸出可以看見
//測試獲取
var userApp = services.BuildServiceProvider().GetRequiredService<IUserAppService>();
var token = configRoot.GetReloadToken();
token.RegisterChangeCallback(state =>
{
var userNewApp = services.BuildServiceProvider().GetRequiredService<IUserAppService>();
}, configRoot);
這里包括IOptions
也更新了,不知道是不是控制台應用程序的原因????
出現上面這么一個問題后我立馬用asp.net core做了一個測試。測試結果表明`IOptions並不會更新,那么為什么上面就更新了呢??程序那點沒寫對??知道的大佬歡迎指正下,我下來也會去摸索。
6 總結
以上所有內容的測試和文章記錄多多少少花了兩個晚上的時間,希望能夠快速的給園友們帶來幫助,寫這篇文章讓我對配置框架有了一個更深的認知,寫了差不多3個小時左右,寫作不易希望得園友們的支持點贊和關注。
文章中提到了依賴注入也使用了依賴注入的測試,后面會對依賴注入框架分享一篇文章。