【.NET源码解读】Configuration组件及自动更新

news2024/11/24 4:57:14

Configuration组件是.NET中一个核心的、非常重要的组件。它提供了一种方便的机制,用于从配置文件、环境变量、命令行参数等各种数据源中读取和配置应用程序,以满足不同环境下应用程序的需求。

在本篇文章中,将会介绍Configuration的基本用法,并通过源码探究.NET中Configuration的实现及热加载的原理。同时,还将提供标准组件扩展封装的示例,帮助深入理解如何自定义配置提供程序,以适应不同的业务需求。

阅读本篇文章,您将会获得以下收获:

  1. 熟练运用Configuration组件
  2. 掌握Configuration的实现原理,并了解热加载的实现方法
  3. 实现自定义配置提供程序

一、Configuration的基本用法

在本章节中,重点介绍了如何读取和运用配置文件。如果您已经熟练掌握了这些内容,可以直接跳过本章节。

1. 默认配置源的优先级(由高到低)

如果在配置源中有两个或更多具有相同键的配置项,除非您显式指定使用哪个配置源,否则后添加的配置项将覆盖先前添加的配置项。

  1. 命令行参数提供
  2. 非前缀环境变量提供(不以ASPNETCORE_ 或 DOTNET_ 为前缀的环境变量)
  3. 运行时的用户 机密
  4. 通过 appsettings.{Environment}.json 提供
  5. 通过 appsettings.json 提供

2. 添加数据文件

创建MyConfig.json文件

{
  "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配置文件

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("MyConfig.json",
        optional: true, // 文件是否可选
        reloadOnChange: true ); // 如果文件更改,是否重载配置

var app = builder.Build();

2.2 添加xml配置文件

builder.Configuration
    .AddXmlFile("MyXMLFile.xml", optional: true, reloadOnChange: true)
    .AddXmlFile($"MyXMLFile.{builder.Environment.EnvironmentName}.xml",
                optional: true, reloadOnChange: true);

2.3 命令行参数

以下命令使用 = 设置键和值:

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

键值:

  • 必须后跟 =,或者当值后跟一个空格时,键必须具有一个 -- 或 / 的前缀。
  • 如果使用 =,则不是必需的。 例如 MySetting=。

在同一命令中,请勿将使用 = 的命令行参数键值对与使用空格的键值对混合使用。

3. 多种读取方式

3.1 获取指定键的值

  • 索引器

使用 IConfiguration 接口的索引器来获取指定键的值(字符串类型),可以使用冒号分隔的键序列作为索引。例如,可以使用 config["Student:Name"] 来获取嵌套在 "Student" 属性下的 "Name" 属性的值。

请注意,在使用此方法读取配置数据时,确保配置提供程序中存在相应的键/值对。如果配置数据中没有指定的键,则此方法将返回 null。

// 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
}
  • GetValue

从配置中提取一个具有指定键的值,并将它转换为指定的类型

// 找不到,使用默认值 0
var number = _configuration.GetValue<int>("NumberKey");

// 配置中找不到 NumberKey,则使用默认值 99
var number = _configuration.GetValue<int>("NumberKey", 99);

3.2 获取多级嵌套配置

  • GetSection()

返回具有指定子节键的配置子节,GetSection 永远不会返回 null。 如果找不到匹配的节,则返回空 IConfigurationSection

IConfigurationSection? section = _configuration.GetSection("section1");
string? a = section["key0"];
  • GetChildren()
IEnumerable<IConfigurationSection>? children = _configuration.GetSection("section2").GetChildren();
foreach (IConfigurationSection item in children)
{
   // 处理数据
}

3.3 绑定配置值到对象

Get<T>() 方法适用于将一组相关的配置值聚合到一个自定义对象中,而 Bind() 方法适用于将配置值绑定到已实例化的自定义对象的属性上。选择使用哪种方法取决于您的需求和偏好

// 定义类
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>();
  • Bind()

方法将配置绑定到已实例化的自定义对象上,适用于将配置值直接绑定到现有对象的情况

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;
}

更详细的描述,可以直接阅读官方文档

二、Configuration源码解析

核心接口

  • IConfigurationBuilder 负责构建配置体系
  • IConfigurationRoot 提供了访问配置数据的方法
  • IConfigurationProvider 实现了具体的配置数据获取和解析逻辑
  • IConfigurationSource 定义了配置数据的来源和构建方式。

核心执行流程是使用IConfigurationBuilder的Build方法,遍历配置的IConfigurationSource集合,创建IConfigurationProvider实例,将不同的IConfigurationProvider集合存储到IConfigurationRoot中,用户读取时依次遍历IConfigurationProvider集合,获取Value。

具体功能及类图

根据我的理解,这个过程可以分为三个部分,以IConfigurationBuilder为核心。第一部分是对用户的IConfiguration,第二部分是确定配置数据来源的IConfigurationSource,第三部分则是IConfigurationProvider。

  • IConfiguration:接口表示应用程序的配置数据,提供了读取和访问配置值的功能
  • IConfigurationRoot:接口继承自 IConfiguration 接口,表示应用程序配置的根节点。它提供了额外的功能,如热加载配置和监听配置变更
  • IConfigurationSection:接口表示配置数据的一个特定部分或节点。它继承自 IConfiguration 接口,因此可以使用 IConfiguration 接口中的方法来读取和访问配置值,通过 GetSection() 方法,IConfiguration 接口可以获取一个 IConfigurationSection 对象,用于处理嵌套和分层的配置结构
  • IConfigurationBuilder:接口用于构建应用程序的配置体系,负责管理配置提供程序和配置源,并构建最终的 IConfigurationRoot 对象。通过调用 Build() 方法,IConfigurationBuilder 可以创建一个 IConfigurationRoot 对象,即应用程序的配置根节点
  • ConfigurationManager: 表示可变配置对象。 它继承了IConfigurationBuilder和IConfigurationRoot。添加源时,它会更新其配置的当前视图。 调用 后 IConfigurationBuilder.Build() ,配置将冻结。(注意这个在Microsoft.Extensions.Configuration下,不是之前的System.Configuration空间下)

这几个对象的关系:IConfigurationBuilder 用于构建和配置应用程序的配置体系,生成一个 IConfigurationRoot 对象作为配置的根节点,而 IConfiguration 和 IConfigurationSection 则用于读取和访问配置值,处理嵌套和分层的配置结构

在上文介绍的IConfigurationBuilder接口中,Add()方法会将IConfigurationSource到Sources配置源集合中。

  • IConfigurationSource:用于定义配置数据的来源(文件、内存、数据库等)和通过Build()方法构建配置提供程序(Configuration Provider)。ps:我认为这就是个工厂
  • FileConfigurationSource:是 IConfigurationSource 接口的一个具体实现类,通过指定文件路径和可选的配置源选项来从文件中加载配置数据(JSON、XML等文件)
  • StreamConfigurationSource:通过指定一个输入流和可选的配置源选项来确定配置数据的来源,可以是内存中的流或其他类型的流。
  • JsonConfigurationSource: 并专门用于处理 JSON 格式的配置文件

FileConfigurationSource 和 StreamConfigurationSource 都继承自 IConfigurationSource,而 JsonConfigurationSource 又继承自 FileConfigurationSource,意味着它们拥有各自的实现方式,但都遵循了 IConfigurationSource 的接口规范。这样,开发人员就可以基于实际需求选择不同的 IConfigurationSource 实现方式来读取与处理配置数据

  • IConfigurationProvider:定义了从 IConfigurationSource 加载数据并提供对其访问的接口
  • ConfigurationProvider:它为 IConfigurationProvider 接口提供了许多通用的方法,例如管理键值对集合、更改和保存配置数据等
  • FileConfigurationProvider:使用了 FileConfigurationSource 提供的数据源,并将数据读取到内存中的键值对集合中,供应用程序使用。
  • JsonConfigurationProvider: 使用System.Text.Json 库来解析 JSON 数据,并将数据转换成键值对的形式。

可以看出 ConfigurationProvider 是 IConfigurationProvider 的基础实现,而 FileConfigurationProvider 和 JsonConfigurationProvider 则是针对特定类型的数据源进行的实现,它们都继承自 ConfigurationProvider 并且遵循了 IConfigurationProvider 接口的规范

源码流程

以下是源代码的部分删减和修改,以便于更好地理解

在程序启动时,创建一个新的 HostBuilder 实例。调用 ConfigureDefaults() 方法向 HostBuilder 实例中添加一些默认的配置和服务

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)
{
    // 首先获取主机环境和一些配置参数
    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()方法的实现,分了图片左侧这么多。我们挑选命令行的深入看一下。

CommandLineConfigurationProvider的构造方法:

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角度解析

我们现在将从程序中添加 JSON 配置文件并解析配置文件的源码流程进行说明。

builder.Configuration.AddJsonFile("MyConfig.json",
        optional: true, // 文件是否可选
        reloadOnChange: true ); // 如果文件更改,是否重载配置

我们来看 AddJsonFile() 方法,它实际上是使用参数构建了一个 JsonConfigurationSource,然后将其传递给 IConfigurationBuilder。

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);
            });
    }
}

JsonConfigurationProvider 方法和上文命令行一样,到了具体的实现。

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);
        }
    }
}

三、自动更新

简要概述自动更新(热加载)的实现过程基于生产者和消费者的关系。它利用FileProvider(内部使用操作系统的文件变更API,并可选择性地开启轮询机制)来生成文件的变更信息,并通过Load/ReLoad方法来消费和重新加载文件。

IFileProvider的作用

IFileProvider 是一个用于抽象文件系统访问的接口。它允许您使用文件和文件夹的基本操作,例如读取、写入和删除文件。

IFileProvider 接口还支持监视文件系统上的更改。您可以注册一个事件,当文件或目录上发生更改时,将调用回调方法。这对于及时更新应用程序配置非常有用。

.NET Core 内置了许多实现了 IFileProvider 接口的文件访问器,例如 PhysicalFileProvider、EmbeddedFileProvider和CompositeFileProvider。在配置文件中默认的是PhysicalFileProvider。

下边是一个使用PhysicalFileProvider来监听文件更改的Demo:

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();
}

ChangeToken和IChangeToken的作用

在我们.NET中ChangeToken和IChangeToken是用于实现配置变更通知的机制。它们是用于监视配置更改并触发相应操作的重要组件。

ChangeToken是一个抽象类,用于表示一个令牌,用于检测配置更改。它定义了一种模式,允许订阅者注册在配置更改时接收通知。

public static class ChangeToken
{
  public static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
  {
      return new ChangeTokenRegistration<TState>(changeTokenProducer, changeTokenConsumer, state);
  }
}

注册ChangeToken的回调函数

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 的监听方法

PhysicalFileProvider 是 .NET中实现 IFileProvider 接口的一个类,用于访问物理文件系统上的文件和文件夹。它可以在应用程序启动时,自动注册到 DI 容器中。

PhysicalFileProvider 的 Watch 方法用于监视指定路径下的文件和文件夹,以便在更改时自动更新应用程序。它返回了一个 IChangeToken 对象,用于触发更改通知。

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 接口的对象作为文件更改的通知。

通过调用 GetOrAddChangeToken(filter) 方法,查找与 filter 参数对应的更改令牌 IChangeToken 对象。如果找到一个 IChangeToken 对象,则返回它,否则会创建一个新的更改令牌并添加到缓存中,然后返回它。

当一个文件或文件夹被更改时,FileSystemWatcher(这个阅读有点难度,暂时没有深入研究) 会通知更改令牌 IChangeToken 对象,PollForChanges 属性为 true时,在默认时间间隔内检查文件或文件夹的变化,进而引发更改通知。然后,应用程序可以处理这个更改通知,例如重新加载配置或更新数据源。

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;
}

现在回顾一下配置文件的自动更新,相信您应该能够理解它了。

四、封装自定义数据源

通过上文对源码的解析,我们已经了解到了 Configuration 组件的工作流程。在封装自定义数据源时,我们不需要关注热加载部分,只需要按照以下步骤进行:

  • 创建实现 IConfigurationSource 接口的类,该类用于定义从数据源读取配置信息的方法。
  • 创建实现 IConfigurationProvider 接口的类,该类用于将配置信息加载到 Data 字典中,并提供获取配置信息的方法。
  • 封装扩展方法,将配置源添加到 ConfigurationBuilder。

如果需要支持热加载功能,还需要相应地实现 IChangeToken 接口和 IOptionsChangeTokenSource 接口。这些接口的实现可以参考官方文档或其他资料。

具体示例请看官方Demo

五、常见问题和解决方案

  • 如何从 JSON 文件中读取数组?

在 JSON 文件中,可以使用 [ ] 符号表示一个数组。如果要将这个数组作为配置项读取,可以使用 GetSection() 方法获取该数组所在的子节点,然后通过 AsEnumerable() 方法将其转换为 IEnumerable<KeyValuePair<string, string>> 类型,并对其中的每个元素进行处理。

  • 如何从环境变量中读取特殊字符?

如果环境变量中包含特殊字符(如 $、:、/ 等),可能会导致解析错误。为了正确地从环境变量中读取特殊字符,可以使用双引号 " 对变量进行引用,例如:"my$envVar"。

  • 如何从命令行参数中读取配置信息?

可以使用 AddCommandLine() 方法将命令行参数添加到 IConfigurationBuilder 中。在使用该方法时,需要指定一个字典,用于将命令行参数映射到配置键。

  • 如何更改已注册的配置源的优先级?

可以使用 Add() 方法按顺序向 IConfigurationBuilder 中添加配置源。越先添加的配置源优先级越高。此外,还可以使用 AddJsonFile().AddEnvironmentVariables() 等方法来指定默认的配置源,并在需要时使用 AddFirst()、AddLast() 等方法将其他配置源添加到特定位置。

  • 如何自定义 IConfigurationProvider 实现?

可以继承 ConfigurationProvider 抽象类,并实现其抽象成员来创建一个新的 IConfigurationProvider。然后,可以通过 Add() 方法将其添加到 IConfigurationBuilder 中,并在需要时进行配置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1050135.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Visual Studio调试排查Windows系统程序audiodg.exe频繁弹出报错

VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到精通系列教程&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&a…

智慧燃气平台的总体架构到底应怎样设计?

关键词&#xff1a;智慧燃气、智慧燃气平台、智能燃气、智能监控 智慧燃气平台功能设计的一些方向和思考&#xff1a; 1、资源统一&#xff0c;管理调度 城市燃气智慧调度运营管理平台收集并且整理出每个业务系统信息&#xff0c;并且根据所整理出的信息结果制定出标准规范&…

Excel·VBA分列、字符串拆分

看到一篇博客《VBA&#xff0c;用VBA进行分列&#xff08;拆分列&#xff09;的2种方法》&#xff0c;使用VBA对字符串进行拆分 目录 Excel分列功能将字符串拆分为二维数组&#xff0c;Split函数举例 将字符串拆分为一维数组&#xff0c;正则表达式举例 Excel分列功能 Sub 测…

windows下使用VS2019 + CMake 进行Qt开发记录

windows下使用VS2019 CMake 进行Qt开发 前言一、准备工作二、VS2019 cmake3.20 Qt1.VS2019新建一个cmake工程2.修改CMakelist.txt3.运行测试 总结 前言 注意&#xff1a;本文讲的是vs2019 cmake的方式开发Qt程序。 常言道&#xff1a;工欲善其事必先利其器。工具利用的好…

数字货币的一些隐私保护问题

常见的数字货币基本模型 代表数字货币的不同架构和交易验证方式。 Account Based 基于账户的数字货币模型。主要特点 账户地址&#xff1a;每个用户都有一个唯一的账户地址&#xff0c;类似于银行账户号码。这个地址用来标识用户的身份&#xff0c;并用于接收、存储和发送…

多线程学习(C/C++)

1.进程 运行着的程序就是进程 进程的特性:1.独立性 2.动态性 3.并发性 (1)进程的状态 进程一共有五种状态分别为:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态)其中创建态和退出态维持的时间是非常短的,稍纵即逝。我们主要是需要将就绪态, 运行态, 挂起态,三者…

7.网络原理之TCP_IP(上)

文章目录 1.网络基础1.1认识IP地址1.2子网掩码1.3认识MAC地址1.4一跳一跳的网络数据传输1.5总结IP地址和MAC地址1.6网络设备及相关技术1.6.1集线器&#xff1a;转发所有端口1.6.2交换机&#xff1a;MAC地址转换表转发对应端口1.6.3主机&#xff1a;网络分层从上到下封装1.6.4主…

文件的编译与链接

目录 翻译环境与链接环境&#xff1a; 翻译环境&#xff1a; 编译器部分&#xff1a; 预处理&#xff1a; 编译&#xff1a; 词法分析&#xff1a; 语法分析&#xff1a; 语义分析&#xff1a; 汇编&#xff1a; 小总结&#xff1a; 链接器部分&#xff1a; 运行环境…

LLMS: 将模型与人类价值观对齐Aligning models with human values

欢迎回来。让我们回到 生成式 AI 项目的生命周期。 上周&#xff0c;你 仔细研究了一种叫做微调的技术。 使用 指令&#xff08;包括路径方法&#xff09;进行微调的目标是 进一步训练 模型&#xff0c;以便它们更好地理解 类似人类的提示并 生成更多类似人类的响应。 与基…

2023版 STM32实战5 基本定时器中断

基本定时器简介与特性 -1-时钟可分频 -2-计数模式只可以选择累加 -3-只可以用来定时&#xff08;含中断&#xff09; 查看时钟源 如图定时器7的时钟最大为72MHZ 定时时间的计算 通用定时器的时间计算公式为 Tout &#xff08;&#xff08;arr1&#xff09;&#xff08;psc1&…

[Framework] Android Binder 工作原理

Binder 是 Android 系统中主要的 IPC 通信方式&#xff0c;其性能非常优异。但是包括我在内的很多开发者都对它望而却步&#xff0c;确实比较难&#xff0c;每次都是看了忘&#xff0c;忘了看&#xff0c;但是随着工作的时间约来越长&#xff0c;每次看也都对 Binder 有新的认识…

【图像处理】SIFT角点特征提取原理

一、说明 提起在OpenCV中的特征点提取&#xff0c;可以列出Harris&#xff0c;可以使用SIFT算法或SURF算法来检测图像中的角特征点。本篇围绕sift的特征点提取&#xff0c;只是管中窥豹&#xff0c;而更多的特征点算法有&#xff1a; Harris & Stephens / Shi–Tomasi 角点…

一种节约存储空间的技术——数据压缩

数据压缩是指&#xff1a;通过特定的算法&#xff0c;将计算的中的文件大小得到降低的一种机制。 目前生活中最常见的应用例子&#xff0c;比如&#xff1a;你通过聊天软件将一张图片发送给好友&#xff0c;再选择发送图片的时候&#xff0c;有一个选项为是否发送原图&#xf…

FL Studio21.1电脑试用体验版音乐制作软件

我一直以来对音乐艺术都很感兴趣。最近我接触到了一款名为 FL Studio 的电脑版音乐制作软件&#xff0c;深感其强大功能和广泛适用性。通过使用这款软件&#xff0c;我不仅深入了解了音乐制作的过程与技巧&#xff0c;也加深了对音乐创作的理解。 FL Studio 最初是一款针对 MI…

Flutter笔记 - ListTile组件及其用法

Flutter笔记 ListTile组件及其用法 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/133411883 目 录 1. …

leetCode 213. 打家劫舍 II 动态规划 房间连成环怎么偷呢?

213. 打家劫舍 II - 力扣&#xff08;LeetCode&#xff09; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋&#xff0c;每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 &#xff0c;这意味着第一个房屋和最后一个房屋是紧挨着的。同时&#xff0c;相邻的房屋装…

【Java 进阶篇】深入理解 SQL 聚合函数

在 SQL 数据库中&#xff0c;聚合函数是一组强大的工具&#xff0c;用于处理和分析数据。它们可以帮助您对数据进行统计、计算总和、平均值、最大值、最小值等操作。无论您是数据库开发者、数据分析师还是希望更好地了解 SQL 数据库的用户&#xff0c;了解聚合函数都是非常重要…

三个要点,掌握Spring Boot单元测试

单元测试是软件开发中不可或缺的重要环节&#xff0c;它用于验证软件中最小可测试单元的准确性。结合运用Spring Boot、JUnit、Mockito和分层架构&#xff0c;开发人员可以更便捷地编写可靠、可测试且高质量的单元测试代码&#xff0c;确保软件的正确性和质量。 一、介绍 本文…

(SAR)Sentinel-1影像自动下载

基于ASF网站提供的python代码&#xff0c;实现Sentinel-1影像的自动下载&#xff1b; 1、登录ASF网站 登录Sentinel-1影像ASF网站&#xff1a;https://search.asf.alaska.edu/&#xff1b; 点击网站最右侧Sign in图标&#xff0c;进行用户注册&#xff1b; 注册完用户之后&…

基于Vue+ELement实现增删改查案例与表单验证(附源码)

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《ELement》。&#x1f3af;&#x1f3af; &#x1…