3.实现自定义配置提供程序
{ "Student": { "Name": "Broder", "Age": "26" } "section0": { "key0": "value00", "key1": "value01" }, "section1": { "key0": "value10", "key1": "value11" }, "array": { "entries": { "0": "value00", "1": "value10", "2": "value20", } } }2.1 添加json配置文件
// 堆代码 duidaima.com var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("MyConfig.json", optional: true, // 文件是否可选 reloadOnChange: true ); // 如果文件更改,是否重载配置 var app = builder.Build();
builder.Configuration .AddXmlFile("MyXMLFile.xml", optional: true, reloadOnChange: true) .AddXmlFile($"MyXMLFile.{builder.Environment.EnvironmentName}.xml", optional: true, reloadOnChange: true);
dotnet run MyKey="Using =" Position:Title=Cmd Position:Name=Cmd_Rick以下命令使用 / 设置键和值:
dotnet run /MyKey "Using /" /Position:Title=Cmd /Position:Name=Cmd_Rick以下命令使用 -- 设置键和值:
dotnet run --MyKey "Using --" --Position:Title=Cmd --Position:Name=Cmd_Rick键值:
// 堆代码 duiddaima.com // requires using Microsoft.Extensions.Configuration; private readonly IConfiguration _configuration; public WeatherForecastController(IConfiguration configuration) { _configuration = configuration; } public void Test() { string? city = _configuration["City"]; // Shanghai string? student = _configuration["Student"]; // null string? name = _configuration["Student:Name"]; // Broder string? age = _configuration["Student:Age"]; // 26 }
// 找不到,使用默认值 0 var number = _configuration.GetValue<int>("NumberKey"); // 配置中找不到 NumberKey,则使用默认值 99 var number = _configuration.GetValue<int>("NumberKey", 99);
IConfigurationSection? section = _configuration.GetSection("section1"); string? a = section["key0"]; GetChildren() IEnumerable<IConfigurationSection>? children = _configuration.GetSection("section2").GetChildren(); foreach (IConfigurationSection item in children) { // 处理数据 }3.3 绑定配置值到对象
// 定义类 public class MyOptions { public string Key = "Student"; public string Name { get; set; } public string Age { get; set; } }Get<T>()
MyOptions? myOptions= _configuration.GetSection("Student").Get<MyOptions>();
var myOptions = new MyOptions(); _configuration.GetSection(myOptions.Key).Bind(myOptions);3.4 添加到IOC容器
builder.Services.Configure<MyOptions>( builder.Configuration.GetSection("Student"));使用配置
private readonly MyOptions _options; public WeatherForecastController(IOptions<MyOptions> options) { _options = options.Value; } public void Test() { string? name = _options.Name; }更详细的描述,可以直接阅读官方文档
public static IHostBuilder CreateDefaultBuilder(string[]? args) { HostBuilder builder = new(); return builder.ConfigureDefaults(args); }在ConfigureDefaults()方法中,会调用静态方法ApplyDefaultAppConfiguration()。用于向应用程序的配置对象中添加默认的配置信息。
internal static void ApplyDefaultAppConfiguration(HostBuilderContext hostingContext, IConfigurationBuilder appConfigBuilder, string[]? args) { // 堆代码 duidaima.com // 首先获取主机环境和一些配置参数 IHostEnvironment env = hostingContext.HostingEnvironment; bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext); // 加载 appsettings.json 和 appsettings.{环境名称}.json 文件中的配置信息(如果存在)。同时,还要每当文件改变时重新加载配置信息。 appConfigBuilder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange); // AddUserSecrets方法加载用户机密 if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 }) { try { var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); appConfigBuilder.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange); } catch (FileNotFoundException) { // The assembly cannot be found, so just skip it. } } // 将操作系统环境变量中的配置信息添加到配置对象中 appConfigBuilder.AddEnvironmentVariables(); // AddCommandLineConfig方法将命令行参数中的配置信息添加到配置对象中 AddCommandLineConfig(appConfigBuilder, args); [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Calling IConfiguration.GetValue is safe when the T is bool.")] static bool GetReloadConfigOnChangeValue(HostBuilderContext hostingContext) => hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true); }我们继续跟进AddEnvironmentVariables方法,就会发现在主机初始时,就已经将默认的ConfigurationSource添加到configurationBuilder中了。
public static IConfigurationBuilder AddCommandLine( this IConfigurationBuilder configurationBuilder, string[] args, IDictionary<string, string>? switchMappings) { configurationBuilder.Add(new CommandLineConfigurationSource { Args = args, SwitchMappings = switchMappings }); return configurationBuilder; }接下来就开始构建和返回 IHost 实例
public IHost Build() { _hostBuilt = true; // 诊断监听器 using DiagnosticListener diagnosticListener = LogHostBuilding(this); // 初始化主机配置,包括默认配置和应用程序附加的配置 InitializeHostConfiguration(); // 初始化主机环境,设置应用程序名称、内容根路径和环境名称 InitializeHostingEnvironment(); // 初始化 HostBuilderContext 对象,将主机环境和主机配置设置为成员变量 InitializeHostBuilderContext(); // 初始化应用程序配置,包括从 appsettings.json 文件加载配置信息和应用程序自定义的配置 InitializeAppConfiguration(); // 初始化服务提供程序,包括向 DI 容器中添加所需的服务并编译容器以生成 IServiceProvider 实例 InitializeServiceProvider(); return ResolveHost(_appServices, diagnosticListener); }在InitializeHostConfiguration()方法中,我们创建了ConfigurationBuilder对象,并通过调用Build()方法生成了一个IConfiguration实例。然而,在下文的InitializeAppConfiguration()方法中,我们又重新创建了一次ConfigurationBuilder并进行了配置。因此,我们可以直接跳过InitializeHostConfiguration()方法,直接来看InitializeAppConfiguration()方法的实现。
private void InitializeAppConfiguration() { IConfigurationBuilder configBuilder = new ConfigurationBuilder() .SetBasePath(_hostingEnvironment!.ContentRootPath) .AddConfiguration(_hostConfiguration!, shouldDisposeConfiguration: true); foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions) { buildAction(_hostBuilderContext!, configBuilder); } // Build() 方法从 ConfigurationBuilder 实例中创建 IConfiguration 实例 _appConfiguration = configBuilder.Build(); _hostBuilderContext!.Configuration = _appConfiguration; }经过前面的铺垫,我们终于来到了IConfigurationBuilder对象中。在该对象中,Build()方法的实现非常简单,它遍历Sources集合中的每个IConfigurationSource对象,并调用其Build()方法生成对应的IConfigurationProvider实例。然后,将所有的IConfigurationProvider合并到一个单独的IConfigurationRoot实例中,最终将该对象返回。
public IConfigurationRoot Build() { var providers = new List<IConfigurationProvider>(); foreach (IConfigurationSource source in Sources) { IConfigurationProvider provider = source.Build(this); providers.Add(provider); } return new ConfigurationRoot(providers); }我们看下IConfigurationSource的Build()方法的实现,分了图片左侧这么多。我们挑选命令行的深入看一下。
public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string>? switchMappings = null) { Args = args; if (switchMappings != null) { // 确保命令行参数映射到配置键的字典是有效的、无重复的,并且所有键都是大小写不敏感的 _switchMappings = GetValidatedSwitchMappingsCopy(switchMappings); } } private static Dictionary<string, string> GetValidatedSwitchMappingsCopy(IDictionary<string, string> switchMappings) { // 使用不区分大小写的比较器来确保字典中的所有键都是大小写不敏感的 var switchMappingsCopy = new Dictionary<string, string>(switchMappings.Count, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, string> mapping in switchMappings) { // Only keys start with "--" or "-" are acceptable if (!mapping.Key.StartsWith("-") && !mapping.Key.StartsWith("--")) { throw new ArgumentException( SR.Format(SR.Error_InvalidSwitchMapping, mapping.Key), nameof(switchMappings)); } if (switchMappingsCopy.ContainsKey(mapping.Key)) { throw new ArgumentException( SR.Format(SR.Error_DuplicatedKeyInSwitchMappings, mapping.Key), nameof(switchMappings)); } switchMappingsCopy.Add(mapping.Key, mapping.Value); } return switchMappingsCopy; }在上文的IConfigurationBuilder的Build()方法中,我们将所有的IConfigurationProvider对象添加到ConfigurationRoot并返回配置根对象。其中,我们需要重点关注的是p.Load()方法用于加载配置信息。该方法涉及的热加载,在下文中介绍。
public ConfigurationRoot(IList<IConfigurationProvider> providers) { _providers = providers; // 用于存储所有的更改通知委托对象 _changeTokenRegistrations = new List<IDisposable>(providers.Count); foreach (IConfigurationProvider p in providers) { p.Load(); // ChangeToken.OnChange() 方法注册一个更改通知委托,监听该提供程序的更改通知,并在收到通知时调用 RaiseChanged() 方法 _changeTokenRegistrations.Add(ChangeToken.OnChange(p.GetReloadToken, RaiseChanged)); } }该方法是解析命令行参数的Load方法。如有兴趣,您可以继续查看该方法的实现代码,来深入了解其中的实现逻辑。(该方法代码全贴)
public override void Load() { var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); string key, value; using (IEnumerator<string> enumerator = Args.GetEnumerator()) { while (enumerator.MoveNext()) { string currentArg = enumerator.Current; int keyStartIndex = 0; if (currentArg.StartsWith("--")) { keyStartIndex = 2; } else if (currentArg.StartsWith("-")) { keyStartIndex = 1; } else if (currentArg.StartsWith("/")) { // "/SomeSwitch" is equivalent to "--SomeSwitch" when interpreting switch mappings // So we do a conversion to simplify later processing currentArg = $"--{currentArg.Substring(1)}"; keyStartIndex = 2; } int separator = currentArg.IndexOf('='); if (separator < 0) { // If there is neither equal sign nor prefix in current argument, it is an invalid format if (keyStartIndex == 0) { // Ignore invalid formats continue; } // If the switch is a key in given switch mappings, interpret it if (_switchMappings != null && _switchMappings.TryGetValue(currentArg, out string? mappedKey)) { key = mappedKey; } // If the switch starts with a single "-" and it isn't in given mappings , it is an invalid usage so ignore it else if (keyStartIndex == 1) { continue; } // Otherwise, use the switch name directly as a key else { key = currentArg.Substring(keyStartIndex); } if (!enumerator.MoveNext()) { // ignore missing values continue; } value = enumerator.Current; } else { string keySegment = currentArg.Substring(0, separator); // If the switch is a key in given switch mappings, interpret it if (_switchMappings != null && _switchMappings.TryGetValue(keySegment, out string? mappedKeySegment)) { key = mappedKeySegment; } // If the switch starts with a single "-" and it isn't in given mappings , it is an invalid usage else if (keyStartIndex == 1) { throw new FormatException(SR.Format(SR.Error_ShortSwitchNotDefined, currentArg)); } // Otherwise, use the switch name directly as a key else { key = currentArg.Substring(keyStartIndex, separator - keyStartIndex); } value = currentArg.Substring(separator + 1); } // Override value when key is duplicated. So we always have the last argument win. data[key] = value; } } Data = data; }通过上文我们已经了解了如何添加和解析配置文件。关于配置键的读取和设置也非常简单。在ConfigurationRoot类中,我们看下基于索引的方法进行操作
public string? this[string key] { get => GetConfiguration(_providers, key); set => SetConfiguration(_providers, key, value); }GetConfiguration() 方法会倒序依次遍历所有的配置提供程序,当获取到key,就会返回结果。(所以后添加的配置文件会覆盖之前的key)
internal static string? GetConfiguration(IList<IConfigurationProvider> providers, string key) { for (int i = providers.Count - 1; i >= 0; i--) { IConfigurationProvider provider = providers[i]; if (provider.TryGet(key, out string? value)) { return value; } } return null; }SetConfiguration()方法会将每个IConfigurationProvider 中的key,进行修改
internal static void SetConfiguration(IList<IConfigurationProvider> providers, string key, string? value) { foreach (IConfigurationProvider provider in providers) { provider.Set(key, value); } }AddJson角度解析
builder.Configuration.AddJsonFile("MyConfig.json", optional: true, // 文件是否可选 reloadOnChange: true ); // 如果文件更改,是否重载配置
public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, IFileProvider? provider, string path, bool optional, bool reloadOnChange) { return builder.AddJsonFile(s => { s.FileProvider = provider;// 这个为自动更新提供文件变动监听方法 s.Path = path; s.Optional = optional; s.ReloadOnChange = reloadOnChange; s.ResolveFileProvider(); }); } public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource>? configureSource) => builder.Add(configureSource);JsonConfigurationSource 类的构建方法,用于创建和返回一个新的IConfigurationProvider 对象。EnsureDefaults()确保提供了默认值,返回一个具体的解析实例JsonConfigurationProvider
public class JsonConfigurationSource : FileConfigurationSource { public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new JsonConfigurationProvider(this); } }FileConfigurationProvider是JsonConfigurationProvider的父类,在构造方法中ChangeToken.OnChange方法来持续监听文件更新。
public FileConfigurationProvider(FileConfigurationSource source) { ThrowHelper.ThrowIfNull(source); Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { _changeTokenRegistration = ChangeToken.OnChange( () => Source.FileProvider.Watch(Source.Path!), () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } }
public class JsonConfigurationProvider : FileConfigurationProvider { public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { } public override void Load(Stream stream) { try { Data = JsonConfigurationFileParser.Parse(stream); } catch (JsonException e) { throw new FormatException(SR.Error_JSONParseError, e); } } }三、自动更新
public static void Main(string[] args) { string path = @"C:\Users\"; // 生效一次 PhysicalFileProvider phyFileProvider = new PhysicalFileProvider(path); // 订阅文件更改事件 IChangeToken watcher = phyFileProvider.Watch("*.*"); watcher.RegisterChangeCallback((state) => { Console.WriteLine($"文件发生改变: {state}"); }, null); // 持续生效 ChangeToken.OnChange( changeTokenProducer: () => phyFileProvider.Watch("*.*"), changeTokenConsumer: () => Console.WriteLine($"文件发生改变") ); Console.ReadLine(); }
public static class ChangeToken { public static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state) { return new ChangeTokenRegistration<TState>(changeTokenProducer, changeTokenConsumer, state); } }
public ChangeTokenRegistration(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state) { _changeTokenProducer = changeTokenProducer; _changeTokenConsumer = changeTokenConsumer; _state = state; IChangeToken? token = changeTokenProducer(); RegisterChangeTokenCallback(token); } private void RegisterChangeTokenCallback(IChangeToken? token) { if (token is null) { return; } IDisposable registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>?)s)!.OnChangeTokenFired(), this); if (token.HasChanged && token.ActiveChangeCallbacks) { registraton?.Dispose(); return; } SetDisposable(registraton); }IChangeToken是ChangeToken的接口,定义了ChangeToken的行为和功能。它包含一个属性HasChanged,指示令牌是否已更改,以及一个事件RegisterChangeCallback,用于注册当令牌更改时触发的回调函数。
public interface IChangeToken { bool HasChanged { get; } bool ActiveChangeCallbacks { get; } IDisposable RegisterChangeCallback(Action<object?> callback, object? state); }PhysicalFileProvider 的监听方法
public IChangeToken Watch(string filter) { if (filter == null || PathUtils.HasInvalidFilterChars(filter)) { return NullChangeToken.Singleton; } // Relative paths starting with leading slashes are okay filter = filter.TrimStart(_pathSeparators); return FileWatcher.CreateFileChangeToken(filter); }CreateFileChangeToken方法接收一个字符串参数 filter,表示要监视的文件或文件夹的相对路径。它将返回一个实现了 IChangeToken 接口的对象作为文件更改的通知。
public IChangeToken CreateFileChangeToken(string filter) { IChangeToken changeToken = GetOrAddChangeToken(filter); return changeToken; }GetOrAddWildcardChangeToken 是 PhysicalFileProvider 用于创建通配符监视的 IChangeToken 对象的方法,可在文件或文件夹更改时通知应用程序,并进行必要的更新。
internal IChangeToken GetOrAddWildcardChangeToken(string pattern) { if (!_wildcardTokenLookup.TryGetValue(pattern, out ChangeTokenInfo tokenInfo)) { var cancellationTokenSource = new CancellationTokenSource(); var cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token); var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); matcher.AddInclude(pattern); tokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken, matcher); tokenInfo = _wildcardTokenLookup.GetOrAdd(pattern, tokenInfo); } IChangeToken changeToken = tokenInfo.ChangeToken; if (PollForChanges) { // The expiry of CancellationChangeToken is controlled by this type and consequently we can cache it. // PollingFileChangeToken on the other hand manages its own lifetime and consequently we cannot cache it. var pollingChangeToken = new PollingWildCardChangeToken(_root, pattern); if (UseActivePolling) { pollingChangeToken.ActiveChangeCallbacks = true; pollingChangeToken.CancellationTokenSource = new CancellationTokenSource(); PollingChangeTokens.TryAdd(pollingChangeToken, pollingChangeToken); } changeToken = new CompositeChangeToken( new[] { changeToken, pollingChangeToken, }); } return changeToken; }现在回顾一下配置文件的自动更新,相信您应该能够理解它了。