8

理解ASP.NET Core - 选项(Options)

 2 years ago
source link: https://www.cnblogs.com/xiaoxiaotank/p/15391905.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

Options绑定

上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊Options,中文译为“选项”,该功能用于实现以强类型的方式对程序配置信息进行访问。

既然是强类型的方式,那么就需要定义一个Options类,该类:

  • 推荐命名规则:{Object}Options
  • 特点:
    • 必须包含公共无参的构造函数
    • 类中的所有公共读写属性都会与配置项进行绑定
    • 字段不会被绑定

接下来,为了便于理解,先举个例子:

首先在 appsetting.json 中添加如下配置:

json
{
  "Book": {
    "Id": 1,
    "Name": "三国演义",
    "Author": "罗贯中"
  }
}

然后定义Options类:

csharp
public class BookOptions
{
    public const string Book = "Book";

    public int Id { get; set; }

    public string Name { get; set; }

    public string Author { get; set; }
}

最后进行绑定(有BindGet两种方式):

csharp
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 方式 1:
        var bookOptions1 = new BookOptions();
        Configuration.GetSection(BookOptions.Book).Bind(bookOptions1);

        // 方式 2:
        var bookOptions2 = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
    }
}

其中,属性IdTitleAuthor均会与配置进行绑定,但是字段Book并不会被绑定,该字段只是用来让我们避免在程序中使用“魔数”。另外,一定要确保配置项能够转换到其绑定的属性类型(你该不会想把string绑定到int类型上吧)。

如果中文读取出来是乱码,那么你可以按照.L.net core 读取appsettings.json 文件中文乱码的问题来配置一下。

当然,这样写代码还不够完美,还是要将Options添加到依赖注入服务容器中,例如通过IServiceCollection的扩展方法Configure

csharp
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
    }
}

Options读取

通过Options接口,我们可以读取依赖注入容器中的Options。常用的有三个接口:

  • IOptions<TOptions>
  • IOptionsSnapshot<TOptions>
  • IOptionsMonitor<TOptions>

接下来,我们看看它们的区别。

IOptions

  • 该接口对象实例生命周期为 Singleton,因此能够将该接口注入到任何生命周期的服务中
  • 当该接口被实例化后,其中的选项值将永远保持不变,即使后续修改了与选项进行绑定的配置,也永远读取不到修改后的配置值
  • 不支持命名选项(Named Options),这个下面会说
csharp
public class ValuesController : ControllerBase
{
    private readonly BookOptions _bookOptions;

    public ValuesController(IOptions<BookOptions> bookOptions)
    {
        // bookOptions.Value 始终是程序启动时加载的配置,永远不会改变
        _bookOptions = bookOptions.Value;
    }
}

IOptionsSnapshot

  • 该接口被注册为 Scoped,因此该接口无法注入到 Singleton 的服务中,只能注入到 Transient 和 Scoped 的服务中。
  • 在作用域中,创建IOptionsSnapshot<TOptions>对象实例时,会从配置中读取最新选项值作为快照,并在作用域中始终使用该快照。
  • 支持命名选项
csharp
public class ValuesController : ControllerBase
{
    private readonly BookOptions _bookOptions;

    public ValuesController(IOptionsSnapshot<BookOptions> bookOptionsSnapshot)
    {
        // bookOptions.Value 是 Options 对象实例创建时读取的配置快照
        _bookOptions = bookOptionsSnapshot.Value;
    }
}

IOptionsMonitor

  • 该接口除了可以查看TOptions的值,还可以监控TOptions配置的更改。
  • 该接口被注册为 Singleton,因此能够将该接口注入到任何生命周期的服务中
  • 每次读取选项值时,都是从配置中读取最新选项值(具体读取逻辑查看下方三种接口对比测试)。
  • 支持:
    • 重新加载配置(CurrentValue),并当配置发生更改时,进行通知(OnChange
    • 缓存与缓存失效 (IOptionsMonitorCache<TOptions>)
csharp
public class ValuesController : ControllerBase
{
    private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;

    public ValuesController(IOptionsMonitor<BookOptions> bookOptionsMonitor)
    {
        // _bookOptionsMonitor.CurrentValue 的值始终是最新配置的值
        _bookOptionsMonitor = bookOptionsMonitor;
    }
}

三种接口对比测试

IOptions<TOptions>就不说了,主要说一下IOptionsSnapshot<TOptions>IOptionsMonitor<TOptions>的不同:

  • IOptionsSnapshot<TOptions> 注册为 Scoped,在创建其实例时,会从配置中读取最新选项值作为快照,并在作用域中使用该快照
  • IOptionsMonitor<TOptions> 注册为 Singleton,每次调用实例的 CurrentValue 时,会先检查缓存(IOptionsMonitorCache<TOptions>)是否有值,如果有值,则直接用,如果没有,则从配置中读取最新选项值,并记入缓存。当配置发生更改时,会将缓存清空。

搞个测试小程序:

csharp
[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
    private readonly IOptions<BookOptions> _bookOptions;
    private readonly IOptionsSnapshot<BookOptions> _bookOptionsSnapshot;
    private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;

    public ValuesController(
        IOptions<BookOptions> bookOptions,
        IOptionsSnapshot<BookOptions> bookOptionsSnapshot,
        IOptionsMonitor<BookOptions> bookOptionsMonitor)
    {
        _bookOptions = bookOptions;
        _bookOptionsSnapshot = bookOptionsSnapshot;
        _bookOptionsMonitor = bookOptionsMonitor;

    }

    [HttpGet]
    public dynamic Get()
    {
        var bookOptionsValue1 = _bookOptions.Value;
        var bookOptionsSnapshotValue1 = _bookOptionsSnapshot.Value;
        var bookOptionsMonitorValue1 = _bookOptionsMonitor.CurrentValue;

        Console.WriteLine("请修改配置文件 appsettings.json");
        Task.Delay(TimeSpan.FromSeconds(10)).Wait();

        var bookOptionsValue2 = _bookOptions.Value;
        var bookOptionsSnapshotValue2 = _bookOptionsSnapshot.Value;
        var bookOptionsMonitorValue2 = _bookOptionsMonitor.CurrentValue;


        return new
        {
            bookOptionsValue1,
            bookOptionsSnapshotValue1,
            bookOptionsMonitorValue1,
            bookOptionsValue2,
            bookOptionsSnapshotValue2,
            bookOptionsMonitorValue2
        };
    }
}

运行2次,并按照指示修改两次配置文件(初始是“三国演义”,第一次修改为“水浒传”,第二次修改为“红楼梦”)

  • 第1次输出:
json
{
  "bookOptionsValue1": {
    "id": 1,
    "name": "三国演义",
    "author": "罗贯中"
  },
  "bookOptionsSnapshotValue1": {
    "id": 1,
    "name": "三国演义",
    "author": "罗贯中"
  },
  "bookOptionsMonitorValue1": {
    "id": 1,
    "name": "三国演义",
    "author": "罗贯中"
  },
  "bookOptionsValue2": {
    "id": 1,
    "name": "三国演义",
    "author": "罗贯中"
  },
  // 注意 OptionsSnapshot 的值在当前作用域内没有进行更新
  "bookOptionsSnapshotValue2": {
    "id": 1,
    "name": "三国演义",
    "author": "罗贯中"
  },
  
  // 注意 OptionsMonitor 的值变成最新的
  "bookOptionsMonitorValue2": {
    "id": 1,
    "name": "水浒传",
    "author": "施耐庵"
  }
}
  • 第2次输出:
json
{
  // Options 的值始终没有变化
  "bookOptionsValue1": {
    "id": 1,
    "name": "三国演义",
    "author": "罗贯中"
  },
  
  // 注意 OptionsSnapshot 的值变成当前最新值了
  "bookOptionsSnapshotValue1": {
    "id": 1,
    "name": "水浒传",
    "author": "施耐庵"
  },
  // 注意 OptionsMonitor 的值始终是最新的
  "bookOptionsMonitorValue1": {
    "id": 1,
    "name": "水浒传",
    "author": "施耐庵"
  },
  
  // Options 的值始终没有变化
  "bookOptionsValue2": {
    "id": 1,
    "name": "三国演义",
    "author": "罗贯中"
  },
  // 注意 OptionsSnapshot 的值在当前作用域内没有进行更新
  "bookOptionsSnapshotValue2": {
    "id": 1,
    "name": "水浒传",
    "author": "施耐庵"
  },
  
  // 注意 OptionsMonitor 的值始终是最新的
  "bookOptionsMonitorValue2": {
    "id": 1,
    "name": "红楼梦",
    "author": "曹雪芹"
  }
}

通过测试我相信你应该能深刻理解它们之间的区别了。

命名选项(Named Options)

上面我们提到了命名选项,命名选项常用于多个配置节点绑定同一属性的情况,举个例子你就明白了:

在 appsettings.json 中添加如下配置

json
{
  "DateTime": {
    "Beijing": {
      "Year": 2021,
      "Month": 1,
      "Day":1,
      "Hour":12,
      "Minute":0,
      "Second":0
    },
    "Tokyo": {
      "Year": 2021,
      "Month": 1,
      "Day":1,
      "Hour":13,
      "Minute":0,
      "Second":0
    },
  }
}

很显然,虽然“Beijing”和“Tokyo”是两个配置项,但是属性都是一样的,我们没必要创建两个Options类,只需要创建一个就好了:

csharp
public class DateTimeOptions
{
    public const string Beijing = "Beijing";
    public const string Tokyo = "Tokyo";

    public int Year { get; set; }
    public int Month { get; set; }
    public int Day { get; set; }
    public int Hour { get; set; }
    public int Minute { get; set; }
    public int Second { get; set; }
}

然后,通过对选项进行指定命名的方式,一个叫做“Beijing”,一个叫做“Tokyo”,将选项添加到DI容器中:

csharp
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
        services.Configure<DateTimeOptions>(DateTimeOptions.Beijing, Configuration.GetSection($"DateTime:{DateTimeOptions.Beijing}"));
        services.Configure<DateTimeOptions>(DateTimeOptions.Tokyo, Configuration.GetSection($"DateTime:{DateTimeOptions.Tokyo}"));
    }
}

最后,通过构造函数的方式将选项注入到Controller中。需要注意的是,因为DateTimeOptions类绑定了两个选项类,所以当我们获取时选项值时,需要指定选项的名字。

csharp
public class ValuesController : ControllerBase
{
    private readonly DateTimeOptions _beijingDateTimeOptions;
    private readonly DateTimeOptions _tockyoDateTimeOptions;

    public ValuesController(IOptionsSnapshot<DateTimeOptions> dateTimeOptions)
    {
        _beijingDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Beijing);
        _tockyoDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Tokyo);
    }
}

程序运行后,你会发现变量 _beijingDateTimeOptions 绑定的配置是“Beijing”配置节点,变量 _tockyoDateTimeOptions 绑定的配置是“Tokyo” 配置节点,但它们绑定的都是同一个类DateTimeOptions

事实上,.NET Core 中所有 Options 都是命名选项,当没有显式指定名字时,使用的名字默认是Options.DefaultName,即string.Empty

使用 DI 服务配置选项

在某些场景下,选项的配置需要依赖DI中的服务,这时可以借助OptionsBuilderConfigure方法(注意这个Configure不是上面提到的IServiceCollection的扩展方法Configure,这是两个不同的方法),该方法支持最多5个服务来配置选项:

csharp
services.AddOptions<BookOptions>()
    .Configure<Service1, Service2, Service3, Service4, Service5>((o, s, s2, s3, s4, s5) => 
    {
        o.Authors = DoSomethingWith(s, s2, s3, s4, s5);
    });

Options 验证

配置毕竟是我们手动进行文本输入的,难免会出现错误,这种情况下,就需要使用程序来帮助进行校验了。

DataAnnotations

Install-Package Microsoft.Extensions.Options.DataAnnotations

我们先升级一下BookOptions,增加一些数据校验:

csharp
public class BookOptions
{
    public const string Book = "Book";

    [Range(1,1000,
        ErrorMessage = "必须 {1} <= {0} <= {2}")]
    public int Id { get; set; }

    [StringLength(10, MinimumLength = 1,
        ErrorMessage = "必须 {2} <= {0} Length <= {1}")]
    public string Name { get; set; }

    public string Author { get; set; }
}

然后我们在添加到DI容器时,增加数据注解验证:

csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<BookOptions>()
        .Bind(Configuration.GetSection(BookOptions.Book))
        .ValidateDataAnnotations();
        .Validate(options =>
        {
            // 校验通过 return true
            // 校验失败 return false
    
            if (options.Author.Contains("A"))
            {
                return false;
            }
    
            return true;
        });
}

ValidateDataAnnotations会根据你添加的特性进行数据校验,当特性无法实现想要的校验逻辑时,则使用Validate进行较为复杂的校验,如果过于复杂,则就要用到IValidateOptions了(实质上,Validate方法内部也是通过注入一个IValidateOptions实例来实现选项验证的)。

IValidateOptions

通过实现IValidateOptions<TOptions>接口,增加数据校验规则,例如:

csharp
public class BookValidation : IValidateOptions<BookOptions>
{
    public ValidateOptionsResult Validate(string name, BookOptions options)
    {
        var failures = new List<string>();
        if(!(options.Id >= 1 && options.Id <= 1000))
        {
            failures.Add($"必须 1 <= {nameof(options.Id)} <= {1000}");
        }
        if(!(options.Name.Length >= 1 && options.Name.Length <= 10))
        {
            failures.Add($"必须 1 <= {nameof(options.Name)} <= 10");
        }

        if (failures.Any())
        {
            return ValidateOptionsResult.Fail(failures);
        }

        return ValidateOptionsResult.Success;
    }
}

然后我们将其注入到DI容器 Singleton,这里使用了TryAddEnumerable扩展方法添加该服务,是因为我们可以注入多个针对同一Options的IValidateOptions,这些IValidateOptions实例都会被执行:

csharp
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<BookOptions>, BookValidation>());
}

Options后期配置

介绍两个方法,分别是PostConfigurePostConfigureAll,他们用来对选项进行后期配置。

  • 在所有的OptionsServiceCollectionExtensions.Configure方法运行后执行
  • ConfigureConfigureAll类似,PostConfigure仅用于对指定名称的选项进行后期配置(默认名称为string.Empty),PostConfigureAll则用于对所有选项实例进行后期配置
  • 每当选项更改时,均会触发相应的方法
csharp
public void ConfigureServices(IServiceCollection services)
{
    services.PostConfigure<DateTimeOptions>(options =>
    {
        Console.WriteLine($"我只对名称为{Options.DefaultName}的{nameof(DateTimeOptions)}实例进行后期配置");
    });

    services.PostConfigure<DateTimeOptions>(DateTimeOptions.Beijing, options =>
    {
        Console.WriteLine($"我只对名称为{DateTimeOptions.Beijing}的{nameof(DateTimeOptions)}实例进行后期配置");
    });

    services.PostConfigureAll<DateTimeOptions>(options =>
    {
        Console.WriteLine($"我对{nameof(DateTimeOptions)}的所有实例进行后期配置");
    });
}

Options 体系

IConfigureOptions

该接口用于包装对选项的配置。默认实现为ConfigureOptions<TOptions>

csharp
public interface IConfigureOptions<in TOptions> where TOptions : class
{
    void Configure(TOptions options);
}

ConfigureOptions

csharp
public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
    public ConfigureOptions(Action<TOptions> action)
    {
        Action = action;
    }

    public Action<TOptions> Action { get; }

    // 配置 TOptions 实例
    public virtual void Configure(TOptions options)
    {
        Action?.Invoke(options);
    }
}

ConfigureFromConfigurationOptions

该类通过继承类ConfigureOptions<TOptions>,对选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到选项上:

csharp
public class ConfigureFromConfigurationOptions<TOptions> : ConfigureOptions<TOptions>
    where TOptions : class
{
    public ConfigureFromConfigurationOptions(IConfiguration config)
        : base(options => ConfigurationBinder.Bind(config, options))
    { }
}

IConfigureNamedOptions

该接口用于包装对命名选项的配置,该接口同时继承了接口IConfigureOptions<TOptions>的行为,默认实现为ConfigureNamedOptions<TOptions>,另外为了实现“使用 DI 服务配置选项”的功能,还提供了一些泛型类重载。

csharp
public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
    void Configure(string name, TOptions options);
}

ConfigureNamedOptions

csharp
public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{
    public ConfigureNamedOptions(string name, Action<TOptions> action)
    {
        Name = name;
        Action = action;
    }

    public string Name { get; }

    public Action<TOptions> Action { get; }

    public virtual void Configure(string name, TOptions options)
    {
        // Name == null 表示针对 TOptions 的所有实例进行配置
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }

    public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}

NamedConfigureFromConfigurationOptions

该类通过继承类ConfigureNamedOptions<TOptions>,对命名选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到命名选项上:

csharp
public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
    where TOptions : class
{
    public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
        : this(name, config, _ => { })
    { }

    public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
        : base(name, options => config.Bind(options, configureBinder))
    { }
}

IPostConfigureOptions

该接口用于包装对命名选项的后期配置,将在所有IConfigureOptions<TOptions>执行完毕后才会执行,默认实现为PostConfigureOptions<TOptions>,同样的,为了实现“使用 DI 服务对选项进行后期配置”的功能,也提供了一些泛型类重载:

csharp
public interface IPostConfigureOptions<in TOptions> where TOptions : class
{
    void PostConfigure(string name, TOptions options);
}

public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
{
    public PostConfigureOptions(string name, Action<TOptions> action)
    {
        Name = name;
        Action = action;
    }

    public string Name { get; }

    public Action<TOptions> Action { get; }

    public virtual void PostConfigure(string name, TOptions options)
    {
        // Name == null 表示针对 TOptions 的所有实例进行后期配置
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }
}

AddOptions & AddOptions & OptionsBuilder

csharp
public static class OptionsServiceCollectionExtensions
{
    // 该方法帮我们把一些常用的与 Options 相关的服务注入到 DI 容器
    public static IServiceCollection AddOptions(this IServiceCollection services)
    {
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
        services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
        return services;
    }
    
    // 没有指定 Options 名称时,默认使用 Options.DefaultName
    public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services) where TOptions : class
        => services.AddOptions<TOptions>(Options.Options.DefaultName);
    
    // 由于后续还要对 TOptions 进行配置,所以返回一个 OptionsBuilder 出去
    public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name)
        where TOptions : class
    {
        services.AddOptions();
        return new OptionsBuilder<TOptions>(services, name);
    }
}

那我们看看OptionsBuilder<TOptions>可以配置哪些东西,由于该类中有大量重载方法,我只挑选最基础的方法来看一看:

csharp
public class OptionsBuilder<TOptions> where TOptions : class
{
    private const string DefaultValidationFailureMessage = "A validation error has occurred.";
    
    // TOptions 实例的名字
    public string Name { get; }
    
    public IServiceCollection Services { get; }
    
    public OptionsBuilder(IServiceCollection services, string name)
    {
        Services = services;
        Name = name ?? Options.DefaultName;
    }
    
    // 选项配置
    public virtual OptionsBuilder<TOptions> Configure(Action<TOptions> configureOptions)
    {
        Services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(Name, configureOptions));
        return this;
    }
    
    // 选项后期配置
    public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions)
    {
        Services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(Name, configureOptions));
        return this;
    }
    
    // 选项验证
    public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation)
        => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage);
        
    public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation, string failureMessage)
    {
        Services.AddSingleton<IValidateOptions<TOptions>>(new ValidateOptions<TOptions>(Name, validation, failureMessage));
        return this;
    }
}

OptionsServiceCollectionExtensions.Configure

OptionsServiceCollectionExtensions.Configure<TOptions>实际上就是对选项的一般配置方式进行了封装,免去了OptionsBuilder<TOptions>

csharp
public static class OptionsServiceCollectionExtensions
{
    // 没有指定 Options 名称时,默认使用 Options.DefaultName
    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
        => services.Configure(Options.Options.DefaultName, configureOptions);
        
    // 等同于做了 AddOptions<TOptions> 和 OptionsBuilder<TOptions>.Configure 两件事
    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
    where TOptions : class
    {
        services.AddOptions();
        services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
        return services;
    }
    
    // 由于 ConfigureAll 是针对 TOptions 的所有实例进行配置,所以不需要指定名字
    public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
        => services.Configure(name: null, configureOptions: configureOptions);
}

OptionsConfigurationServiceCollectionExtensions.Configure

请注意,该Configure<TOptions>方法与上方提及的Configure<TOptions>不是同一个。该扩展方法针对配置(IConfiguration)绑定到选项(Options)上进行了扩展

Install-Package Microsoft.Extensions.Options.ConfigurationExtensions

csharp
public static class OptionsConfigurationServiceCollectionExtensions
{
    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
        => services.Configure<TOptions>(Options.Options.DefaultName, config);

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
        => services.Configure<TOptions>(name, config, _ => { });

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder)
        where TOptions : class
        => services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder);

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
        where TOptions : class
    {
        services.AddOptions();
        services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
        return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
    }
}

IOptionsFactory

IOptionsFactory<TOptions>负责创建命名选项实例,默认实现为OptionsFactory<TOptions>

csharp
public interface IOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> where TOptions : class
{
    TOptions Create(string name);
}

public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> 
    : IOptionsFactory<TOptions> where TOptions : class
{
    private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
    private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
    private readonly IEnumerable<IValidateOptions<TOptions>> _validations;

    // 这里通过依赖注入的的方式将与 TOptions 相关的配置、验证服务列表解析出来
    public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) 
    : this(setups, postConfigures, validations: null)
    { }

    public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
    {
        _setups = setups;
        _postConfigures = postConfigures;
        _validations = validations;
    }

    public TOptions Create(string name)
    {
        // 1. 创建并配置 Options
        TOptions options = CreateInstance(name);
        foreach (IConfigureOptions<TOptions> setup in _setups)
        {
            if (setup is IConfigureNamedOptions<TOptions> namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        
        // 2. 对 Options 进行后期配置
        foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }

        // 3. 执行 Options 校验
        if (_validations != null)
        {
            var failures = new List<string>();
            foreach (IValidateOptions<TOptions> validate in _validations)
            {
                ValidateOptionsResult result = validate.Validate(name, options);
                if (result.Failed)
                {
                    failures.AddRange(result.Failures);
                }
            }
            if (failures.Count > 0)
            {
                throw new OptionsValidationException(name, typeof(TOptions), failures);
            }
        }

        return options;
    }

    protected virtual TOptions CreateInstance(string name)
    {
        return Activator.CreateInstance<TOptions>();
    }
}

OptionsManager

通过AddOptions扩展方法的实现,可以看到,IOptions<TOptions>IOptionsSnapshot<TOptions>的实现都是OptionsManager<TOptions>,只不过一个是 Singleton,一个是 Scoped。我们通过前面的分析也知道了,当源中的配置改变时,IOptions<TOptions>始终维持初始值,IOptionsSnapshot<TOptions>在每次请求时会读取最新配置值,并在同一个请求中是不变的。接下来就来看看OptionsManager<TOptions>是如何实现的:

csharp
public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
    IOptions<TOptions>,
    IOptionsSnapshot<TOptions>
    where TOptions : class
{
    private readonly IOptionsFactory<TOptions> _factory;
    // 将已创建的 TOptions 实例缓存到该私有变量中
    private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); 

    public OptionsManager(IOptionsFactory<TOptions> factory)
    {
        _factory = factory;
    }

    public TOptions Value => Get(Options.DefaultName);

    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;

        // 若缓存不存在,则通过工厂新建 Options 实例,否则直接读取缓存
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }
}

OptionsMonitor

同样,通过前面的分析,我们知道OptionsMonitor<TOptions>读取的始终是配置的最新值,它的实现在OptionsManager<TOptions>的基础上,除了使用缓存将创建的 Options 实例缓存起来外,还增添了监听机制,当配置发生更改时,会将缓存移除。

csharp
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
    IOptionsMonitor<TOptions>,
    IDisposable
    where TOptions : class
{
    private readonly IOptionsMonitorCache<TOptions> _cache;
    private readonly IOptionsFactory<TOptions> _factory;
    private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
    private readonly List<IDisposable> _registrations = new List<IDisposable>();
    internal event Action<TOptions, string> _onChange;

    public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
    {
        _factory = factory;
        _sources = sources;
        _cache = cache;

        // 监听更改
        foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
        {
            IDisposable registration = ChangeToken.OnChange(
                  () => source.GetChangeToken(),
                  (name) => InvokeChanged(name),
                  source.Name);

            _registrations.Add(registration);
        }
    }

    // 当发生更改时,移除缓存
    private void InvokeChanged(string name)
    {
        name = name ?? Options.DefaultName;
        _cache.TryRemove(name);
        TOptions options = Get(name);
        if (_onChange != null)
        {
            _onChange.Invoke(options, name);
        }
    }

    public TOptions CurrentValue => Get(Options.DefaultName);

    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }

    // 通过该方法绑定 OnChange 事件
    public IDisposable OnChange(Action<TOptions, string> listener)
    {
        var disposable = new ChangeTrackerDisposable(this, listener);
        _onChange += disposable.OnChange;
        return disposable;
    }

    public void Dispose()
    {
        // 移除所有 change token 的订阅
        foreach (IDisposable registration in _registrations)
        {
            registration.Dispose();
        }

        _registrations.Clear();
    }
}
  • 所有选项均为命名选项,默认名称为Options.DefaultName,即string.Empty
  • 通过ConfigurationBinder.GetConfigurationBinder.Bind手动获取选项实例。
  • 通过Configure方法进行选项配置:
    • OptionsBuilder<TOptions>.Configure:通过包含DI服务的委托来进行选项配置
    • OptionsServiceCollectionExtensions.Configure<TOptions>:通过简单委托来进行选项配置
    • OptionsConfigurationServiceCollectionExtensions.Configure<TOptions>:直接将IConfiguration实例绑定到选项上
  • 通过OptionsServiceCollectionExtensions.ConfigureAll<TOptions>方法针对某个选项类型的所有实例(不同名称)统一进行配置。
  • 通过PostConfigure方法进行选项后期配置:
    • OptionsBuilder<TOptions>.PostConfigure:通过包含DI服务的委托来进行选项后期配置
    • OptionsServiceCollectionExtensions.PostConfigure<TOptions>:通过简单委托来进行选项后期配置
  • 通过PostConfigureAll<TOptions>方法针对某个选项类型的所有实例(不同名称)统一进行配置。
  • 通过Validate进行选项验证:
    • OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations:通过数据注解进行选项验证
    • OptionsBuilder<TOptions>.Validate:通过委托进行选项验证
    • IValidateOptions<TOptions>:通过实现该接口并注入实现来进行选项验证
  • 通过依赖注入读取选项:
    • IOptions<TOptions>:Singleton,值永远是该接口被实例化时的选项配置初始值
    • IOptionsSnapshot<TOptions>:Scoped,每一次Http请求开始时会读取选项配置的最新值,并在当前请求中保持不变
    • IOptionsMonitor<TOptions>:Singleton,每次读取都是选项配置的最新值

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK