在 ASP.NET Core 使用 IOptions pattern

该文章已生成可运行项目,

        一般而言,在Asp.net core 程序中,我们将程序的配置信息写在Appsetting.json文件中,然后利用Asp.netCore中的默认加载,可以很方便的访问,而且部署程序后,可以在不关闭程序的情况下实现配置更改。


目录

1.介绍 

2.实现

3.扩展

4. 命名选项

5.总结


1.介绍 

        但是如果你的程序很大,或者说要配置的东西较多,那么你的配置文件appsettings也会变得交大,从而不方便访问,例如你的appsettings.json文件是这样的:

{
  ...

  "Oidc": {
    "Google": {
      "ClientId": "27454jshd64dufmgngterh",
      "ClientSecret": "39jfytittthsd83jgygtttktt7tyy8kthgh8"
    },
    "Facebook": {
      "ClientId": "o375j6593jdgdb254en62rd",
      "ClientSecret": "vhfyrfjhrrfdsow0273485djdgw722pr955ht"
    },
    "Okta": {
      "ClientId": "a1b2c3d4e5f6g7h8i9j03fg",
      "ClientSecret": "hfgfwornrtftfjnsfre693i34543u2gdjfbff"
    }
  },
  "Smtp": {
    "Server": "mail.abc.com",
    "Port": "56",
    "From": "me@abc.com",
    "Username": "me@abc.com",
    "Password": "Abcd@1234",
    "IsSsl": false
  },
  "Jwt": {
    "Audience": "thisisyouraudience",
    "Issuer": "thisisyourissuers",
    "SigningKey": "thiskeyisveryhardtobreak",
    "IsValidateLifetime": true,
    "IsValidateIssuer": true,
    "IsValidateAudience": true
  },

  ...
}

如果我们要读取 OidcProvider “Google” 的 ClientId,我们应该使用经典的 IConfiguration 实例访问它的值,如下所示:

var googleClientId = Configuration["Oidc:Google:ClientId"].ToString();

由于配置结构随着嵌套部分而变得多样化,其关键表示变得冗长且难以维护。当然可以通过创建一个强类型的ConfigurationManager类来解决这个问题,该类封装了这种密钥访问机制,并且还处理了非字符串属性的类型转换要求。

        一个更优雅的方案是根据强类型类匹配,让Asp.NetCore为我们做这些复杂的匹配工站。就像前面提到的,如果我要访问settings中来自Oidc的Google字段,直接为这个Google字段配置一个对应的类型,这样每次我要访问这个字段,容器就会提供一个该类型的实例,并且实例字段的值和Settings中的保持一致。

        上面的理念就是Options Pattern的工作理念:通过预订的的强类型创建一个对配置文件字段区域的访问途径。


2.实现

回到正题,创建一个实体类型:

public class SmtpOptions
{
    public const string SectionName = "Smtp";
    public int Port { get; set; }
    public string From { get; set; }
    public string Server { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public bool IsSsl { get; set; }
}

注意:

  1. 属性的名字和类型要完全匹配settings中的Key和值类型
  2. 构造的强类型类必须是非抽象类且只拥有无参构造函数
  3. 只有公共属性才会被绑定到配置字段
"Smtp": {
    "Server": "mail.abc.com",
    "Port": "56",
    "From": "me@abc.com",
    "Username": "me@abc.com",
    "Password": "Abcd@1234",
    "IsSsl": false
}

创建绑定有两种办法:

  1. 利用Configuration.Bind()方法去new一个强类型实例对象,然后将其注册进去
  2. 使用IOption接口让Asp.NetCore自动为我们注册

先看第一种办法:

# Startup.ConfigureServices() Method #

var smtp = new SmtpOptions();
Configuration.Bind(SmtpOptions.SectionName, smtp);
services.AddSingleton(smtp);

我们也可以用Get函数替代Bind函数,从而消灭难看的new()

# Startup.ConfigureServices() Method #

var smtp = Configuration.GetSection(SmtpOptions.SectionName)
                .Get();

services.AddSingleton(smtp);

具体在项目中的代码为:

using OptionPattern.Models;

var builder = WebApplication.CreateBuilder(args);
#region 办法1
var smtp = new SmtpOptions();
builder.Configuration.Bind("Smtp", smtp);
builder.Services.AddSingleton(smtp);
#endregion

#region
var smtp2 = builder.Configuration.GetSection("Smtp").Get<SmtpOptions>();
builder.Services.AddSingleton(smtp2);
#endregion


var app = builder.Build();

var smtp1= app.Services.GetRequiredService<SmtpOptions>();
Console.WriteLine(smtp1.From);

app.MapGet("/", () => "Hello World!");

app.Run();

虽然上面两个办法都很有用,但是还是有一些不足:

  1. 显示的使用了new关键字创建实例
  2. 不够简洁,我们实际上做了实例化,绑定,注册

我们再看看第二种办法:

# Startup.ConfigureServices() Method #

services.Configure(
        Configuration.GetSection(SmtpOptions.SectionName));

当你需要参数配置时,直接通过构造函数注入实例:

public class MailController : ControllerBase
{
    private readonly SmtpOptions smtp;

    public MailController(IOptions<SmtpOptions> smtpOptions)
    {
        this.smtp = smtpOptions.value;
    }

    ...

具体代码:

先在Program.cs中添加配置

#region 办法三
builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"));
#endregion

然后在Controller中使用Ioption 

namespace OptionPattern.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class OptionPatternController:ControllerBase
    {
        private readonly SmtpOptions _smtpOptions;
        public OptionPatternController(IOptions<SmtpOptions> smtpOptions)
        {
            _smtpOptions = smtpOptions.Value;
        }

        [HttpGet,Route("Test1")]
        public SmtpOptions Test()
        {
            return _smtpOptions;
        }
    }
}

当然也可以直接在Startup中直接获取服务:

var smtp1 = app.Services.GetRequiredService<IOptions<SmtpOptions>>().Value;
Console.WriteLine(smtp1.From);

运行如下:


3.扩展

        前面的办法很好的为我们解决了配置文件过大时获取配置字段的问题。我们还可以更进一步,当程序启动后,如果要改这个程序的settings文件,那么IOptions接口就不行了,因为用IOptions配置时,会被注册为一个单利模式,这也意味着一旦实例化,就无法更改,除非你关闭程序。

        所以Asp.Netcore还提供了另外一个接口:IOptionsSnapshot. 使用这个接口,即使应用程序启动后,我们也可以家在配置更改,它会将配置类型注册为ScopedService 并且不能注入到Singleton服务中,每次获取实例都会重新读取配置。

        但是这样就完美了吗?让我们做一个测试:

        我们首先再建一个类:

public class TitleConfiguration
{
    public string WelcomeMessage { get; set; }
    public bool ShowWelcomeMessage { get; set; }
    public string Color { get; set; }
    public bool UseRandomTitleColor { get; set; }
}

  然后在日志中添加如下字段:

"Pages": {
    "HomePage": {
        "WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page",
        "ShowWelcomeMessage": true,
        "Color": "blue",
        "UseRandomTitleColor": true
    }
},

      假设我们有一个要注入为单例模式服务的接口:

public interface ITitleColorService
{
    string GetTitleColor();
}

这个服务要访问我们刚刚定义的类,并且才用IOptionsSanpShot接口:

public class TitleColorService : ITitleColorService
{
    private readonly string[] _colors = { "red", "green", "blue", "black", "purple", "yellow", "brown", "pink" };
    private readonly TitleConfiguration _homePageTitleConfiguration;

    public TitleColorService(IOptionsSnapshot<TitleConfiguration> homePageTitleConfiguration)
    {
        _homePageTitleConfiguration = homePageTitleConfiguration.Value;
    }

    public string GetTitleColor()
    {
        var random = new Random();
        var colorIndex = random.Next(7);
        return _homePageTitleConfiguration.UseRandomTitleColor ?
            _colors[colorIndex] :
            _homePageTitleConfiguration.Color;
    }
}

注册为单例模式:

builder.Services.AddSingleton<ITitleColorService,TitleColorService>();

最后在Controller中使用:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OptionPattern.Models;

namespace OptionPattern.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class OptionPatternController:ControllerBase
    {
        private readonly SmtpOptions _smtpOptions;
        private readonly ILogger<OptionPatternController> _logger;
        private readonly ITitleColorService _titleColorService;
        private readonly TitleConfiguration _titleConfiguration;
        public OptionPatternController(IOptionsSnapshot<SmtpOptions> smtpOptions,
            ILogger<OptionPatternController> logger,
            IOptionsSnapshot<TitleConfiguration> titleConfiguration,
            ITitleColorService titleColorService)
        {
            _smtpOptions = smtpOptions.Value;
            _titleConfiguration = titleConfiguration.Value;
            _titleColorService = titleColorService;
            _logger = logger;
        }

        [HttpGet,Route("Test1")]
        public SmtpOptions Test()
        {
            return _smtpOptions;
        }

        [HttpGet]
        public string Index()
        {
            var color = _titleColorService.GetTitleColor();
            return color;
        }

    }
}

运行后会报错:

报错的原因是因为: 系统阻止应用程序从Singleton服务中获取Scope服务对象,这在Asp.NetCore中是一个很常见的错误,这个错误会导致不可预料的行为。也就是说如果你的父对象是单例的,那么你的子对象只能是单例或者临时(Transient)的,不能时局部(Scope)的.

        所以这里介绍第三个Option接口:IOpttionMonitor, 它就是专门解决上述问题的,修改参考如下:

public TitleColorService(IOptionsMonitor<TitleConfiguration> homePageTitleConfiguration)
{
    _homePageTitleConfiguration = homePageTitleConfiguration.CurrentValue;
}

在此运行就没有问题了。

4. 命名选项

        最后让我们考虑这样一种情况:假设你在配置中有两个字段的属性是一模一样,比如下面:

"Pages": {
    "HomePage": {
        "WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page",
        "ShowWelcomeMessage": true,
        "Color": "black",
        "UseRandomTitleColor": true
    },
    "ProductPage": {
        "WelcomeMessage": "Welcome to the ProjectConfigurationDemo Product Page",
        "ShowWelcomeMessage": true,
        "Color": "black",
        "UseRandomTitleColor": false
    }
},

显然我们可以用两个类来分别配置HomePage字段和ProductPage字段,但是因为属性是完全一样的,那么有没有什么办法避免定义多个类呢?这里就需要命名字段了,直接看代码:

//使用命名选项
builder.Services.Configure<TitleConfiguration>("HomePage", builder.Configuration.GetSection("Pages:HomePage"));
builder.Services.Configure<TitleConfiguration>("ProductPage", builder.Configuration.GetSection("Pages:ProductPage"));

在获取时,也需要一点改动:

using Microsoft.Extensions.Options;

namespace OptionPattern.Models
{
    public class TitleColorService : ITitleColorService
    {
        private readonly string[] _colors = { "red", "green", "blue", "black", "purple", "yellow", "brown", "pink" };
      //  private readonly TitleConfiguration _homePageTitleConfiguration;
        private readonly IOptionsMonitor<TitleConfiguration> _options;

        public TitleColorService(IOptionsMonitor<TitleConfiguration> PageTitleConfiguration)
        {
           // _homePageTitleConfiguration = homePageTitleConfiguration.CurrentValue;
           _options = PageTitleConfiguration;
        }

        public string GetTitleColor(string pageTitleConfiguration)
        {
            var random = new Random();
            var colorIndex = random.Next(7);
            //return _homePageTitleConfiguration.UseRandomTitleColor ?
            //    _colors[colorIndex] :
            //    _homePageTitleConfiguration.Color;

            var titleConfig = _options.Get(pageTitleConfiguration);
            return titleConfig.UseRandomTitleColor?
                _colors[colorIndex] :
                titleConfig.Color;
        }
    }
}

接口也改一下:

    public interface ITitleColorService
    {
        string GetTitleColor(string option);
    }

最后修改一下controller:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OptionPattern.Models;

namespace OptionPattern.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class OptionPatternController:ControllerBase
    {
        private readonly SmtpOptions _smtpOptions;
        private readonly ILogger<OptionPatternController> _logger;
        private readonly ITitleColorService _titleColorService;
        private readonly TitleConfiguration _homePagetitleConfiguration;
        public OptionPatternController(IOptionsSnapshot<SmtpOptions> smtpOptions,
            ILogger<OptionPatternController> logger,
            IOptionsSnapshot<TitleConfiguration> titleConfiguration,
            ITitleColorService titleColorService)
        {
            _smtpOptions = smtpOptions.Value;
            _homePagetitleConfiguration = titleConfiguration.Get("HomePage");
            _titleColorService = titleColorService;
            _logger = logger;
        }

        [HttpGet,Route("Test1")]
        public SmtpOptions Test()
        {
            return _smtpOptions;
        }

        [HttpGet]
        public string Index()
        {
            var color = _titleColorService.GetTitleColor("HomePage");
            return color;
        }

    }
}

简而言之就是,用命名选项模式配置时,获取的时候也要提供命名,这样可以获得对应的Section实例。

5.总结

三种接口的小结

IOptions<T>

  • 是原来的Options接口,比绑定整个Configuration好
  • 不支持配置重载
  • 注册为单例服务,可以在任何地方注入
  • 注册时只绑定一次配置值,每次都返回相同的值
  • 不支持命名选项

IOptionsSnapshot<T>

  • 注册为范围服务
  • 支持配置重载
  • 无法注入单例服务
  • 每个请求重新加载值
  • 支持命名选项

IOptionsMonitor<T>:

  • 注册为单例服务
  • 支持配置重载
  • 可以注入任何服务生命周期
  • 值被缓存并立即重新加载
  • 支持命名选项

如果我们不想启用实时重新加载或者我们不需要命名选项,我们可以简单地使用IOptions<T>. 如果这样做,我们可以使用IOptionsSnapshot<T>or IOptionsMonitor<T>,但IOptionsMonitor<T>可以注入其他单例服务,而IOptionsSnapshot<T>不能。

有时项目的性质决定了我们不应该“即时”更改我们的配置。有时它是需要的。选择“正确的选项”时要小心!

代码链接在此:EFCoreStudy目录下Option Patternhttps://gitee.com/qin_yuanlong/AspDotnet_Staudy.git

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值