.Net 5 Options 选项

DotNet Configuration 源码:https://github.com/dotnet/runtime/tree/master/src/libraries/,其中以 Microsoft.Extensions.Options 开头的项目

Options 官方文档:

Nuget包:Microsoft.Extensions.Options

建议先掌握:

  • DependencyInjection ( 依赖注入 ):
  • Configuration ( 配置 ) :

介绍

Options提供了对配置数据的强类型访问

Options提供了验证选项的机制

Configure

首先声明一个选项类

public class MyOption
{
    public string A { get; set; }

    public string B { get; set; }
}

使用Configure扩展方法对选项类进行配置

var serviceCollection = new ServiceCollection();

// 配置 MyOption
serviceCollection.Configure<MyOption>(n =>
{
    n.A = "A Value";
    n.B = "B Value";
});

var serviceProvider = serviceCollection.BuildServiceProvider();

获取IOptions<MyOption>选项服务

var myOption = serviceProvider.GetRequiredService<IOptions<MyOption>>();
MyOption myOptionValue = myOption.Value;
Console.WriteLine(myOptionValue.A);
Console.WriteLine(myOptionValue.B);

 中使用

配置

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.Configure<MyOption>(option =>
    {
        option.A = "A Value";
        option.B = "A Value";
    });
}

注入Ioption<MyOption>

[Route("Home")]
public class HomeController : ControllerBase
{
    private readonly MyOption _myOption;

    public HomeController(IOptions<MyOption> myOptions)
    {
        _myOption = myOptions.Value;
    }

    [HttpGet]
    public string GetAsync()
    {
        return $"A:{_myOption.A},B:{_myOption.B}";
    }
}

绑定配置,使用IConfiguration来配置选项

Nuget包:Microsoft.Extensions.Options.ConfigurationExtensions

Configure<TOptions>(IConfigureation configuration)不仅仅会绑定配置,还会添加对配置文件的更改通知,通知IOptionsMonitor重新计算选项

实例:

appsettings.json

{
  "MySetting": {
    "A": "A Value",
    "B": "B Value"
  }
}

C#

IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
var serviceCollection = new ServiceCollection();
serviceCollection.Configure<MyOption>(configuration.GetSection("MySetting"));
var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptions<MyOption>>();
var myOptionValue = myOption.Value;
Console.WriteLine(myOptionValue.A);
Console.WriteLine(myOptionValue.B);

PostConfigure

PostConfigure 会在 Configure 之后进行配置

实例:

var serviceCollection = new ServiceCollection();
serviceCollection.PostConfigure<MyOption>(option =>
{
    option.B = "PostConfigure B Value";
});

serviceCollection.Configure<MyOption>(n =>
{
    n.A = "A Value";
    n.B = "B Value";
});

var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptions<MyOption>>();
MyOption myOptionValue = myOption.Value;
Console.WriteLine(myOptionValue.A); // A Value
Console.WriteLine(myOptionValue.B); // PostConfigure B Value

命名选项

可以给选项命名

默认选项名称为空字符串

IOptions 不支持获取命名选项,只能获取默认命名选项

可以使用IOptionsSnapshotIOptionsMonitor等获取命名选项

IOptionsSnapshot继承子IOptions并添加了Get(string optionName)方法来获取命名选项

IOptionsMonitorCurrentValue属性用于获取默认选项,Get(string optionName)方法来获取命名选项

实例:

var serviceCollection = new ServiceCollection();
// 配置 MyOption
serviceCollection.Configure<MyOption>(n =>
{
    n.A = "A Value";
    n.B = "B Value";
});
serviceCollection.Configure<MyOption>("My", n =>
{
    n.A = "My:A Value";
    n.B = "My:B Value";
});
var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptions<MyOption>>();
MyOption myOptionValue = myOption.Value;
Console.WriteLine(myOptionValue.A); // A Value
Console.WriteLine(myOptionValue.B); // B Value

// IOptionsSnapshot 获取命名选项
var myOption2 = serviceProvider.GetRequiredService<IOptionsSnapshot<MyOption>>();
MyOption myOptionValue2 = myOption2.Get("My");
Console.WriteLine(myOptionValue2.A); // My:A Value
Console.WriteLine(myOptionValue2.B); // My:B Value

// IOptionsMonitor 获取命名选项
var myOptionsMonitor = serviceProvider.GetRequiredService<IOptionsMonitor<MyOption>>();
MyOption myOptionValue3 = myOptionsMonitor.Get("My");
Console.WriteLine(myOptionValue3.A);
Console.WriteLine(myOptionValue3.B);

ConfigureAll、PostConfigureAll

ConfigureAllPostConfigureAll可以配置全部的命名配置

实例:

var serviceCollection = new ServiceCollection();
serviceCollection.ConfigureAll<MyOption>(n =>
{
    n.A = "A Value,Config By ConfigureAll";
});
// 配置 MyOption
serviceCollection.Configure<MyOption>(n =>
{
    n.B = "B Value";
});
serviceCollection.Configure<MyOption>("My", n =>
{
    n.B = "My:B Value";
});

var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptions<MyOption>>();
MyOption myOptionValue = myOption.Value;
Console.WriteLine(myOptionValue.A); // A Value,Config By ConfigureAll
Console.WriteLine(myOptionValue.B); // B Value

var myOption2 = serviceProvider.GetRequiredService<IOptionsSnapshot<MyOption>>();
MyOption myOptionValue2 = myOption2.Get("My");
Console.WriteLine(myOptionValue2.A); // A Value,Config By ConfigureAll
Console.WriteLine(myOptionValue2.B); // My:B Value

IOptions、IOptionsSnapshot、IOptionsMonitor

IOptions<TOptions>

  • 只会计算一次选项
  • 不支持命名选项
  • 单例服务,可以注入到任何服务生存期

IOptionsSnapshot<TOptions>

  • 会计算多次选项,每次请求时应重新计算选项
  • 注册为范围内,因此无法注入到单一实例服务
  • 支持命名选项

IOptionsMonitor<TOptions>:

  • 单例服务,可以注入到任何服务生存期
  • 支持更改通知 ( 比如配置文件更改通知 )
  • 支持命名选项
  • 支持重载配置
  • 选择性选项失效 (IOptionsMonitorCache)

IOptionsFactory<TOptions>负责创建、计算选项实例,IOptions<TOptions>IOptionsSnapshot<TOptions>IOptionsMonitor<TOptions>都使用它来创建选项实例

对比实例:

appsettings.json

{
  "MySetting": {
    "B": "B 1"
  }
}

C# Startup

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup( IConfiguration configuration)
    {
        Configuration = configuration;
    }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.Configure<MyOption>(option => { option.A = DateTime.Now.ToString(CultureInfo.CurrentCulture); });
        services.Configure<MyOption>(Configuration.GetSection("MySetting"));
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseEndpoints(options => { options.MapControllers(); });
    }
}

C# HomeController

[Route("Home")]
public class HomeController : ControllerBase
{
    public IOptions<MyOption> MyOptions { get; }
    public IOptionsSnapshot<MyOption> MyOptionsSnapshot { get; }
    public IOptionsMonitor<MyOption> MyOptionsMonitor { get; }
    public IServiceProvider ServiceProvider { get; }
    public IWebHostEnvironment WebHostEnvironment { get; }

    public HomeController(
        IOptions<MyOption> myOptions,
        IOptionsSnapshot<MyOption> myOptionsSnapshot,
        IOptionsMonitor<MyOption> myOptionsMonitor,
        IServiceProvider serviceProvider,
        IWebHostEnvironment webHostEnvironment
    )
    {
        MyOptions = myOptions;
        MyOptionsSnapshot = myOptionsSnapshot;
        MyOptionsMonitor = myOptionsMonitor;
        ServiceProvider = serviceProvider;
        WebHostEnvironment = webHostEnvironment;
    }

    [HttpGet]
    public string GetAsync()
    {
        Console.WriteLine($"MyOption A:{MyOptions.Value.A}");
        Console.WriteLine($"MyOption B:{MyOptions.Value.B}");
        Console.WriteLine($"MyOptionsSnapshot A:{MyOptionsSnapshot.Value.A}");
        Console.WriteLine($"MyOptionsSnapshot B:{MyOptionsSnapshot.Value.B}");
        Console.WriteLine($"MyOptionsMonitor A:{MyOptionsMonitor.CurrentValue.A}");
        Console.WriteLine($"MyOptionsMonitor B:{MyOptionsMonitor.CurrentValue.B}");

        // 更改appsetting配置文件,处罚文件更改通知 ( json文件配置需要开启更改重载 )
        System.IO.File.WriteAllText(WebHostEnvironment.ContentRootPath + "/appsettings.json",
            JsonSerializer.Serialize(new{
                MySetting = new
                {
                    B = "B 2"
                }
            })
        );
        var timer = new Timer(2000);
        timer.Start();
        timer.Elapsed += (sender, e) =>
        {
            Console.WriteLine($"MyOption2 A:{MyOptions.Value.A}");
            Console.WriteLine($"MyOption2 B:{MyOptions.Value.B}");
            Console.WriteLine($"MyOptionsSnapshot2 A:{MyOptionsSnapshot.Value.A}");
            Console.WriteLine($"MyOptionsSnapshot2 B:{MyOptionsSnapshot.Value.B}");
            Console.WriteLine($"MyOptionsMonitor2 A:{MyOptionsMonitor.CurrentValue.A}");
            Console.WriteLine($"MyOptionsMonitor2 B:{MyOptionsMonitor.CurrentValue.B}");
            timer.Stop();
        };

        return "Hello";
    }
}

控制台打印信息如下:

第一次请求:

MyOption A:10/23/2020 11:48:37 PM

MyOption B:B 1

MyOptionsSnapshot A:10/23/2020 11:48:37 PM

MyOptionsSnapshot B:B 1

MyOptionsMonitor A:10/23/2020 11:48:37 PM

MyOptionsMonitor B:B 1

2秒后

MyOption2 A:10/23/2020 11:48:37 PM // 没变,因为IOptions只会计算一次

MyOption2 B:B 1 // 没变,因为IOptions只会计算一次

MyOptionsSnapshot2 A:10/23/2020 11:48:37 PM // 没变,因为IOptionsSnapshot每次请求才会计算一次

MyOptionsSnapshot2 B:B 1 // 没变,因为IOptionsSnapshot每次请求才会计算一次

MyOptionsMonitor2 A:10/23/2020 11:48:37 PM // 没变,因为IOptionsMonitor没有接受到 A键 的相关更改通知

MyOptionsMonitor2 B:B 2 // 变了,因为IOptionsMonitor接受到了文件配置更改通知

第二次请求:

MyOption A:10/23/2020 11:48:37 PM // 没变,因为IOptions只会计算一次

MyOption B:B 1 // 没变,因为IOptions只会计算一次

MyOptionsSnapshot A:10/23/2020 11:49:15 PM // 变了,因为IOptionsSnapshot每次请求都会计算一次

MyOptionsSnapshot B:B 2 // 变了,因为IOptionsSnapshot每次请求都会计算一次

OptionBuilder

特点:

  • 简化了创建命名选项的过程,因为它只是初始 AddOptions<TOptions>(string optionsName) 调用的单个参数,而不会出现在所有后续调用中
  • 能在配置选项过程中使用依赖
  • 能够验证选项

不使用OptionsBuilder

var serviceCollection = new ServiceCollection();
serviceCollection.PostConfigure<MyOption>("My",n =>
{
    n.A = n.A + "_Stuff";
    n.B = "B";
});
serviceCollection.Configure<MyOption>("My", n => { n.A = "A"; });

使用OptionsBuilder

通过AddOptions<TOptions>(string optionsName)获取OptionsBuilder

var serviceCollection = new ServiceCollection();
serviceCollection.AddOptions<MyOption>("My")
    .PostConfigure(n =>
    {
        n.A = n.A + "_Stuff";
        n.B = "B";
    })
    .Configure(n => { n.A = "A"; });

选项依赖

OptionBuilderConfigure最多支持5个泛型参数,他们表示选项的依赖

var serviceCollection = new ServiceCollection();
serviceCollection.AddOptions<MyOption>("My")
    .Configure<IConfiguration>((option, configuration) =>
    {
        option.A = "A";
        option.B = configuration["B"];
    });

选项验证

OptionBuilderValidate可以对选项进行验证,将在此选项获取时进行验证,如果验证失败将抛出异常 Validate的第二个参数可以自定义验证的错误信息

IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(_ => configuration);
serviceCollection.AddOptions<MyOption>()
    .Configure<IConfiguration>((option, configuration) =>
    {
        option.A = "A2";
        option.B = configuration["B"];
    })
    .Validate(options => options.B != null,"B不能为空"); // 第二个参数用于自定义验证错误信息
    .Validate(options => options.A != null,"A不能为空"); // 第二个参数用于自定义验证错误信息
var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptions<MyOption>>();  // 如果验证失败,将抛出异常

使用特性进行选项验证

Nuget包:Microsoft.Extensions.Options.DataAnnotations

实例:

appsettings.json

{
  "MySetting": {
    "A": "A Value",
    "B": "B Value"
  }
}

C#

IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
var serviceCollection = new ServiceCollection();
serviceCollection.Configure<MyOption2>(configuration.GetSection("MySetting"));
serviceCollection.AddOptions<MyOption2>()
    .ValidateDataAnnotations(); // 使用特性进行选项验证

var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptions<MyOption2>>();
var myOptionValue = myOption.Value;
Console.WriteLine(myOptionValue.A);
Console.WriteLine(myOptionValue.B);

ConfiguOptions

ConfiguOptions用于在依赖容器中注册IConfigureOptions<in TOptions>IPostConfigureOptions<in TOptions>IValidateOptions<TOptions>的实现(如果传入的类型实现了他们)

ConfigurePostConfigure等对选项进行配置,本质就是在依赖容器中注册IConfigureOptions<in TOptions>的实现 OptionBuilderValidate函数本质也是注册IValidateOptions<TOptions>的实现

所以我们也可以手动注册IConfigureOptions<in TOptions>,这样的一个好处是可以注入依赖

注意: serviceCollection.AddOptions()函数用于注册IOptionsIOptionsSnapshotIOptionsMonitorIOptionsFactoryIOptionsMonitorCache的实现 使用serviceCollection.Configure等函数会在内部调用serviceCollection.AddOptions(),所以不需要在调用它 这里我们手动注入IConfigureOptions<in TOptions>的实现,所以手动调用它

实例1:

C# 1

public class MyConfigureOption : IConfigureOptions<MyOption>
{
    public IConfiguration Configuration { get; }

    public MyConfigureOption(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void Configure(MyOption options)
    {
        options.A = "A Value";
        options.B = Configuration["B"];
    }
}

C# 2

/// appsettings.json:{"B": "B Value"}
IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(_ => configuration);
serviceCollection.AddOptions();
serviceCollection.AddTransient<IConfigureOptions<MyOption>, MyConfigureOption>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptions<MyOption>>();
Console.WriteLine(myOption.Value.A); // A Value
Console.WriteLine(myOption.Value.B); // B Value

实例2:

可以直接继承自ConfigureNamedOptions<MyOption>来实现命名配置

C# 1

public class MyConfigureOption : ConfigureNamedOptions<MyOption>
{
    public MyConfigureOption(IConfiguration configuration)
        : base("My", options =>
        {
            options.A = "A Value";
            options.B = configuration["B"];
        })
    {
    }
}

C# 2

/// appsettings.json:{"B": "B Value"}
IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(_ => configuration);
serviceCollection.AddOptions();
serviceCollection.AddTransient<IConfigureOptions<MyOption>, MyConfigureOption>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptionsSnapshot<MyOption>>();
var myOptionValue = myOption.Get("My");
Console.WriteLine(myOptionValue.A); // A Value
Console.WriteLine(myOptionValue.B); // B Value

实例3:

可以实现IValidateOptions<TOptions>来对选项进行验证

public class MyConfigureOption : ConfigureNamedOptions<MyOption>, IValidateOptions<MyOption>
{
    public MyConfigureOption(IConfiguration configuration)
        : base("My", options =>
        {
            options.A = "A Value";
            options.B = configuration["B"];
        })
    {
    }

    public ValidateOptionsResult Validate(string name, MyOption options)
    {
        if (name != "My")
        {
            return ValidateOptionsResult.Skip;
        }

        if (options.B == null)
        {
            return ValidateOptionsResult.Fail("B 不能为空");
        }
        else
        {
            return ValidateOptionsResult.Success;
        }
    }
}

C# 2

/// appsettings.json:{"B": "B Value"}
IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(_ => configuration);
serviceCollection.AddOptions();
serviceCollection.AddTransient<IConfigureOptions<MyOption>, MyConfigureOption>();
serviceCollection.AddTransient<IValidateOptions<MyOption>, MyConfigureOption>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var myOption = serviceProvider.GetRequiredService<IOptionsSnapshot<MyOption>>();
var myOptionValue = myOption.Get("My");
Console.WriteLine(myOptionValue.A);
Console.WriteLine(myOptionValue.B);

使用ConfiguOptions代替手动注册IConfigureOptions<in TOptions>IPostConfigureOptions<in TOptions>IValidateOptions<TOptions>

在上面实例中需要手动指定要注册的服务类,在实例3中需要注册2次,使用ConfiguOptions能帮你自动注册他们

实例3中的以下代码

serviceCollection.AddTransient<IConfigureOptions<MyOption>, MyConfigureOption>();
serviceCollection.AddTransient<IValidateOptions<MyOption>, MyConfigureOption>();

可以替换一下代码

serviceCollection.ConfigureOptions<MyConfigureOption>();
posted @ 2021-02-23 10:20  dreamw  阅读(377)  评论(0编辑  收藏  举报