5

【ASP.NET Core】选项模式的相关接口 - 东邪独孤

 2 years ago
source link: https://www.cnblogs.com/tcjiaan/p/16511729.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.
neoserver,ios ssh client

【ASP.NET Core】选项模式的相关接口

在 .NET 中,配置与选项模式其实有联系的(这些功能现在不仅限于 ASP.NET Core,而是作为平台扩展来提供,在其他.NET 项目中都能用)。配置一般从多个来源(上一篇水文中的例子,记得否?)来读取数据,最后以 Key - Value 的方式加载到应用程序中,然后应用程序可以读取配置。这些来源有 JSON文件、XML文件等。上次老周还演示了 CSV 文件。

而选项模式呢,说直白些就是一些简单的类,多数情况下只定义些公共属性,可以称为选项类(从泛型约束而言,选项类的要求一般就是 class,也就是类型是类的就行)。这些类其根本作用也是用来配置应用程序的,只是它们以面向对象的方式把配置信息封装起来。也就是说,选项类可以与配置信息做绑定。

咱们在定义选项类的时候,习惯让类名以“Options”结尾。这也不是什么硬规则,只不过易于理解罢了。你看到某个类以“Options”结尾,你就可以猜到它的用处——设定选项参数用的,其实也就是配置。

假设有这么个类。

    public class TestOptions
    {
        public int Key1 { get; set; }
        public string? Key2 { get; set; }
    }

看着很是简单,就两个属性。

在 appsettings.json 文件中,我们可以加上这样一个节点:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "myConfig": {
    "key1": 5055,
    "key2": "Speaking Chinglish"
  }
}

如你所见,“myConfig” 节点对应的那个 JObject 的属性结构和 TestOptions 类相同。

appsettings.json 在应用程序初始化时是自动添加的,我们不需要再配置,直接改文件就行,不用再添代码。IConfiguration 接口有个扩展方法 Bind,可以将配置信息与某个类型对象直接绑定。

app.MapGet("/", () =>
{
    IConfigurationRoot config = (IConfigurationRoot)app.Configuration;
    // 找出我们要的节
    IConfigurationSection myconfig = config.GetSection("myConfig");

    TestOptions options = new();
    // 直接绑定
    myconfig.Bind(options);
    // 看看结果
    return $"Key1 = {options.Key1}\nKey2 = {options.Key2}";
});

从 JSON 文件读出的配置信息可以直接与 TestOptions 实例绑定。这样,在代码运行后,能看到咱们刚刚在 JSON 文件中设置的内容。

367389-20220723121041174-1768081676.png

上面这种方法虽然能做到了绑定,但用起来还费劲。为啥不直接弄进服务容器中,来个依赖注入,岂不美哉?

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.Configure<TestOptions>(builder.Configuration.GetSection("myConfig"));
var app = builder.Build();

Configure<TOptions> 是个扩展方法,它用来对选项类进行配置——就是为选项类的属性赋值。选项模式不仅限于把配置信息强类型化,它通用于应用程序内各种功能配置。如日志怎么记录、验证策略、Cookie 策略等。

这里注意在依赖注入时,我们不是直接用 TestOptions 类型,而是 IOptions<TOptions> 接口。比如,我们可以在 MVC 控制器中这样玩:

    public class HomeController : Controller
    {
        readonly TestOptions _options;

        // 构造函数,获取注入的对象
        public HomeController(IOptions<TestOptions> wrapper)
        {
            _options = wrapper.Value;
        }

        public IActionResult Index()
        {
            // 这里访问选项
            string s = $"Key1: {_options.Key1}\n";
            s += $"Key2: {_options.Key2}";
            return Content(s);
        }
    }

其实,咱们可以用的接口类型并不只有 IOptions<>,不妨看看 AddOptions 扩展方法的源代码(OptionsServiceCollectionExtensions.cs)

   public static IServiceCollection AddOptions(this IServiceCollection services)
   {
       ThrowHelper.ThrowIfNull(services);

       services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
       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;
   }

以上源代码解释了为什么依赖注入时用的是 IOptions<> 接口,在容器中注册时用的就是这个接口嘛。其实,你可以用 IOptionsMonitor<TestOptions>。至于这几个接口有啥区别,我们先不管,下一个世纪再告诉你。

AddOptions 方法我们一般不需要调用,ASP.NET Core 应用程序初始化时自动调用了。

为了使选项模式有更好的适用性,我们也可以通过委托来配置选项类。

builder.Services.Configure<TestOptions>(opt =>
{
    opt.Key1 = 999;
    opt.Key2 = "Hi, baby";
});

至此,Configure 方法的两种用法就出来了:

1、用 IConfigure 对象,从配置信息源中加载,并填充选项类的属性值(这个挺像反序列化操作);

2、直接用委托。

不管是配置信息还是委托,Configure 方法只是向服务容器添加了一组 IConfigureOptions<TOptions> 对象。它的实现类型有 ConfigureOptions<TOptions>、ConfigureNamedOptions<TOptions> 等。

我们以 ConfigureOptions<TOptions> 类为例分析一下,因为这个类比较简单

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

        public Action<TOptions>? Action { get; }

        public virtual void Configure(TOptions options)
        {
             ……
            Action?.Invoke(options);
        }
    }

看到构造函数时,你有没有发现点什么?看,它是不是有个委托 Action<TOptions>,我们再回头看看刚刚我们在服务容器调用的 Configure 扩展方法,它是不是有一个重载版本也有个 Action<TOptions> 的参数。

builder.Services.Configure<TestOptions>(opt =>
{
    opt.Key1 = 999;
    opt.Key2 = "Hi, baby";
});

对的,我们传给它的委托就是传到了 ConfigureOptions 的构造函数里,ConfigureOptions 实现的就是 IConfigureOptions<TOptions> 接口。哦,原来是籍样子滴。我们每调用一次 Configure 方法,它就向容器注册一个 IConfigureOptions。

这只是放进了容器中罢了,并没有马上执行我们写的委托。

那,它们在哪里被调用呢?在工厂里面——IOptionsFactory<TOptions>。

    public interface IOptionsFactory<TOptions>
        where TOptions : class
    {
        TOptions Create(string name);
    }

别小看这货,它可是核心角色。选项类的实例就是由它来创建的(实现 Create 方法)。你会注意到,这里有个 name 参数,干吗的?这个是为选项的命名分组准备。一般可以不理会它,除非你要实现不同分组产生不太一样的选项类实例,这样就用得上了。

其实,如果需要,我们不妨为 TestOptions 类写个小工厂(家庭小作坊)。

    public class TestOptionsFactory : IOptionsFactory<TestOptions>
    {
        public TestOptions Create(string name)
        {
            return new TestOptions();
        }
    }

简单吧,然后咱用呢,不用管它咋用,你只要注册到服务容器中就行了。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddTransient<IOptionsFactory<TestOptions>, TestOptionsFactory>();

这里我注册为暂时性服务,你也可以考虑注册为其他的,不冲突就行。

你要是有疑问,我自定义的工厂它会执行吗?会的,不信打个断点看看。

367389-20220723172057468-1668665228.png

 然后运行程序,瞧,这不,停下来了。

367389-20220723172223362-2114280367.png

可,可,可是,这TNND,为什么是默认值,我刚刚不是 Configure 用委托设置了吗?

367389-20220723172343294-422934186.png

builder.Services.Configure<TestOptions>(opt =>
{
    opt.Key1 = 999;
    opt.Key2 = "Hi, baby";
});

无效?出啥故障了?等等,刚刚是不是说了,Configure 方法每调用一次就会注册一个 IConfigureOptions<TOptions> 吗?

想起来了,所以我们的工厂要改造一下,通过依赖注入把这一堆 IConfigureOptions 弄进来,然后逐个调用,这样就可以为选项类的属性赋值了。

    public class TestOptionsFactory : IOptionsFactory<TestOptions>
    {
        readonly IEnumerable<IConfigureOptions<TestOptions>> _configs;
        public TestOptionsFactory(IEnumerable<IConfigureOptions<TestOptions>> configs)
        {
            _configs = configs;
        }

        public TestOptions Create(string name)
        {
            TestOptions opt = new();
            foreach(var cfg in _configs)
            {
                cfg.Configure(opt);
            }
            return opt;
        }
    }

看,这就成了。

367389-20220723173050681-1921244722.png

 其实,工厂所需要的依赖对象并不只有 IConfigureOptions,还有两个接口,需要了解一下:

 1、IPostConfigureOptions:这货史称“后期配置”。其用法和 IConfigureOptions 一样的。对应的服务容器扩展方法是 PostConfigure<TOptions>,用法和 Configure<TOptions> 扩展方法完全一样的。它的用途是进行选项配置后做一些“修修补补”。比如,看看你还有哪些属性没赋值的,给它安排个默认值。有人会说,那我在创建选项类实例时就为每个属性分配默认值不就行了吗?还要啥后期配置?举个例子:

    public class ProgressOptions
    {
        public int Max { get; set; }
        public int Min { get; set; }
        public int Current { get; set; }
    }

就算我在工厂类中实例化时分配 Max = 100, Min = 0, Current = 50。那,如果我配置的时候是这样的呢:

builder.Services.Configure<ProgressOptions>(o =>
{
    o.Max = 60;
    o.Min = 0;
});

假如我没设置 Current 属性,默认规则是它取中值,那这时候取中值 50 显然就不行了。而是得根据 Max 和 Min 的值来决定。这时候用后期配置就很必要了。

builder.Services.PostConfigure<ProgressOptions>(o =>
{
    o.Current = (o.Max - o.Min) / 2;
});

2、IValidateOptions:这货用来做验证用的。这个和上面的“修修补补”不同。验证是检查选项类的属性值是否符合要求。如果不符合,直接给你来个异常——回炉重造。

我们不妨看看默认的工作是怎么实现的。

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

        public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: Array.Empty<IValidateOptions<TOptions>>())
        { }
      
        public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
        {
            _setups = setups as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(setups).ToArray();
            _postConfigures = postConfigures as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigures).ToArray();
            _validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray();
        }

        public TOptions Create(string name)
        {
            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);
                }
            }
            foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
            {
                post.PostConfigure(name, options);
            }

            if (_validations.Length > 0)
            {
                var failures = new List<string>();
                foreach (IValidateOptions<TOptions> validate in _validations)
                {
                    ValidateOptionsResult result = validate.Validate(name, options);
                    if (result is not null && result.Failed)
                    {
                        failures.AddRange(result.Failures);
                    }
                }
                if (failures.Count > 0)
                {
                    throw new OptionsValidationException(name, typeof(TOptions), failures);
                }
            }

            return options;
        }

        /// <summary>
        /// 这里是创建选项类实例的地方,为了通用化,它用了 Activator
        /// </summary>
        protected virtual TOptions CreateInstance(string name)
        {
            return Activator.CreateInstance<TOptions>();
        }
    }

从其源码我们知道它们的执行顺序:

N多个IConfigureOptions ---> N多个IPostConfigureOptions ---> N多个IValidateOptions。

好了,下面咱们解决最后一个疑问:选项类的实例怎么跑到 IOptions<TOptions> 里面的?通过前面的例子,咱们都知道,通过依赖注入访问选项类时,是通过 IOptions<> 等接口的。咋关联起来的?还是跟 Factory 有关,不然就不会说它是核心角色。

这里老周只说一对服务类型,其他的实现类似。在 AddOptions 扩展方法中,通过注册的服务类型得知,IOptions<> 对应的是 UnnamedOptionsManager。这个类没有公开,它的源码如下:

    internal sealed class UnnamedOptionsManager<TOptions> :
        IOptions<TOptions>
        where TOptions : class
    {
        private readonly IOptionsFactory<TOptions> _factory;
        private volatile object? _syncObj;
        private volatile TOptions? _value;

        public UnnamedOptionsManager(IOptionsFactory<TOptions> factory) => _factory = factory;

        public TOptions Value
        {
            get
            {
                if (_value is TOptions value)
                {
                    return value;
                }

                lock (_syncObj ?? Interlocked.CompareExchange(ref _syncObj, new object(), null) ?? _syncObj)
                {
                    return _value ??= _factory.Create(Options.DefaultName);
                }
            }
        }
    }

现在知道它们怎么关联起来了吧——还是一样的套路,用依赖注入。

经过老周上文一系列胡说八道,这些与选项模式有关的接口之间的关系就清晰了。

367389-20220723181901475-1357742880.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK