专栏首页大内老A[ASP.NET Core 3框架揭秘] Options[6]: 扩展与定制

[ASP.NET Core 3框架揭秘] Options[6]: 扩展与定制

由于Options模型涉及的核心对象最终都注册为相应的服务,所以从原则上讲这些对象都是可以定制的,下面提供几个这样的实例。由于Options模型提供了针对配置系统的集成,所以可以采用配置文件的形式来提供原始的Options数据,可以直接采用反序列化的方式将配置文件的内容转换成Options对象。

一、使用JSON文件提供Options数据

在介绍IConfigureOptions扩展的实现之前,下面先演示如何在应用中使用它。首先在演示实例中定义一个Options类型。简单起见,我们沿用前面使用的包含两个成员的FoobarOptions类型,从而实现IEquatable<FoobarOptions>接口。最终绑定生成的是一个FakeOptions对象,为了演示针对复合类型、数组、集合和字典类型的绑定,可以为其定义相应的属性成员。

public class FakeOptions
{
    public FoobarOptions Foobar { get; set; }
    public FoobarOptions[] Array { get; set; }
    public IList<FoobarOptions> List { get; set; }
    public IDictionary<string, FoobarOptions> Dictionary { get; set; }
}

public class FoobarOptions : IEquatable<FoobarOptions>
{
    public int Foo { get; set; }
    public int Bar { get; set; }

    public FoobarOptions() { }
    public FoobarOptions(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public override string ToString() => $"Foo:{Foo}, Bar:{Bar}";
    public bool Equals(FoobarOptions other) => this.Foo == other?.Foo && this.Bar == other?.Bar;
}

可以在项目根目录添加一个JSON文件(命名为fakeoptions.json),如下所示的代码片段表示该文件的内容,可以看出文件的格式与FakeOptions类型的数据成员是兼容的,也就是说,这个文件的内容能够被反序列化成一个FakeOptions对象。

{
    "Foobar": {
        "Foo": 1,
        "Bar": 1
    },
    "Array": [{
            "Foo": 1,
            "Bar": 1
        },
        {
            "Foo": 2,
            "Bar": 2
        },
        {
            "Foo": 3,
            "Bar": 3
        }],
    "List": [{
            "Foo": 1,
            "Bar": 1
        },
        {
            "Foo": 2,
            "Bar": 2
        },
        {
            "Foo": 3,
            "Bar": 3
        }],
    "Dictionary": {
        "1": {
            "Foo": 1,
            "Bar": 1
        },
        "2": {
            "Foo": 2,
            "Bar": 2
        },
        "3": {
            "Foo": 3,
            "Bar": 3
        }
    }
}

下面按照Options模式直接读取该配置文件,并将文件内容绑定为一个FakeOptions对象。如下面的代码片段所示,在调用IServiceCollection接口的AddOptions扩展方法之后,我们调用了另一个自定义的Configure<FakeOptions>扩展方法,该方法的参数表示承载原始Options数据的JSON文件的路径。这个演示程序提供的一系列调试断言表明:最终获取的FakeOptions对象与原始的JSON文件具有一致的内容。(S710)

class Program
{
    static void Main()
    {
        var foobar1 = new FoobarOptions(1, 1);
        var foobar2 = new FoobarOptions(2, 2);
        var foobar3 = new FoobarOptions(3, 3);

        var options = new ServiceCollection()
            .AddOptions()
            .Configure<FakeOptions>("fakeoptions.json")
            .BuildServiceProvider()
            .GetRequiredService<IOptions<FakeOptions>>()
            .Value;

        Debug.Assert(options.Foobar.Equals(foobar1));

        Debug.Assert(options.Array[0].Equals(foobar1));
        Debug.Assert(options.Array[1].Equals(foobar2));
        Debug.Assert(options.Array[2].Equals(foobar3));

        Debug.Assert(options.List[0].Equals(foobar1));
        Debug.Assert(options.List[1].Equals(foobar2));
        Debug.Assert(options.List[2].Equals(foobar3));

        Debug.Assert(options.Dictionary["1"].Equals(foobar1));
        Debug.Assert(options.Dictionary["2"].Equals(foobar2));
        Debug.Assert(options.Dictionary["3"].Equals(foobar3));
    }
}

二、JsonFileConfigureOptions<TOptions>

Options模型中针对Options对象的初始化是通过IConfigureOptions<TOptions>对象实现的,演示程序中调用的Configure<TOptions>方法实际上就是注册了这样一个服务。我们采用Newtonsoft.Json来完成针对JSON的序列化,并且使用基于物理文件系统的IFileProvider来读取文件。Configure<TOptions>方法注册的实际上就是如下这个JsonFileConfigureOptions<TOptions>类型。JsonFileConfigureOptions<TOptions>实现了IConfigureNamedOptions<TOptions>接口,在调用构造函数创建一个JsonFileConfigureOptions<TOptions>对象的时候,我们指定了Options名称、JSON文件的路径以及用于读取该文件的IFileProvider对象。

public class JsonFileConfigureOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class, new()
{
    private readonly IFileProvider _fileProvider;
    private readonly string _path;
    private readonly string _name;

    public JsonFileConfigureOptions(string name, string path, IFileProvider fileProvider)
    {
        _fileProvider = fileProvider;
        _path = path;
        _name = name;
    }

    public void Configure(string name, TOptions options)
    {
        if (name != null && _name != name)
        {
            return;
        }

        byte[] bytes;
        using (var stream = _fileProvider.GetFileInfo(_path).CreateReadStream())
        {
            bytes = new byte[stream.Length];
            stream.Read(bytes, 0, bytes.Length);
        }

        var contents = Encoding.Default.GetString(bytes);
        contents = contents.Substring(contents.IndexOf('{'));
        var newOptions = JsonConvert.DeserializeObject<TOptions>(contents);
        Bind(newOptions, options);
    }

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

    private void Bind(object from, object to)
    {
        var type = from.GetType();
        if (type.IsDictionary())
        {
            var dest = (IDictionary)to;
            var src = (IDictionary)from;
            foreach (var key in src.Keys)
            {
                dest.Add(key, src[key]);
            }
            return;
        }

        if (type.IsCollection())
        {
            var dest = (IList)to;
            var src = (IList)from;
            foreach (var item in src)
            {
                dest.Add(item);
            }
        }

        foreach (var property in type.GetProperties())
        {
            if (property.IsSpecialName || property.GetMethod == null ||
                property.Name == "Item" || property.DeclaringType != type)
            {
                continue;
            }

            var src = property.GetValue(from);
            var propertyType = src?.GetType() ?? property.PropertyType;

            if ((propertyType.IsValueType || src is string || src == null) && property.SetMethod != null)
            {
                property.SetValue(to, src);
                continue;
            }

            var dest = property.GetValue(to);
            if (null != dest && !propertyType.IsArray())
            {
                Bind(src, dest);
                continue;
            }

            if (property.SetMethod != null)
            {
                var destType = propertyType.IsDictionary()
                    ? typeof(Dictionary<,>).MakeGenericType(propertyType.GetGenericArguments())
                    : propertyType.IsArray()
                    ? typeof(List<>).MakeGenericType(propertyType.GetElementType())
                    : propertyType.IsCollection()
                    ? typeof(List<>).MakeGenericType(propertyType.GetGenericArguments())
                    : propertyType;

                dest = Activator.CreateInstance(destType);
                Bind(src, dest);

                if (propertyType.IsArray())
                {
                    IList list = (IList)dest;
                    dest = Array.CreateInstance(propertyType.GetElementType(), list.Count);
                    list.CopyTo((Array)dest, 0);
                }
                property.SetValue(to, src);
            }
        }
    }
}

internal static class Extensions
{
    public static bool IsDictionary(this Type type) => type.IsGenericType && typeof(IDictionary).IsAssignableFrom(type) && type.GetGenericArguments().Length == 2;
    public static bool IsCollection(this Type type) => typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string);
    public static bool IsArray(this Type type) => typeof(Array).IsAssignableFrom(type);
}

在实现的Configure方法中,JsonFileConfigureOptions<TOptions>利用提供的IFileProvider对象读取了指定JSON文件的内容,并将其反序列化成一个新的Options对象。由于Options模型最终提供的总是IOptionsFactory<TOptions>对象最初创建的那个Options对象,所以针对Options的初始化只能针对这个Options对象。因此,不能使用新的Options对象替换现有的Options对象,只能将新Options对象承载的数据绑定到现有的这个Options对象上,针对Options对象的绑定实现在上面提供的Bind方法中。如下所示的代码片段是注册JsonFileConfigureOptions<TOptions>对象的Configure<TOptions>扩展方法的定义。

public static class ServiceCollectionExtensions
{
    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string filePath, string basePath = null)  where TOptions : class, new()
        => services.Configure<TOptions>(Options.DefaultName, filePath, basePath);

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, string filePath,  string basePath = null) where TOptions : class, new()
    {
        var fileProvider = string.IsNullOrEmpty(basePath)
            ? new PhysicalFileProvider(Directory.GetCurrentDirectory())
            : new PhysicalFileProvider(basePath);

        return services.AddSingleton<IConfigureOptions<TOptions>>( new JsonFileConfigureOptions<TOptions>(name, filePath, fileProvider));
    }
}

三、定时刷新Options数据

通过对IOptionsMonitor<Options>的介绍,可知它通过IOptionsChangeTokenSource<TOptions>对象来感知Options数据的变化。到目前为止,我们尚未涉及针对这个服务的注册,下面演示如何通过注册该服务来实现定时刷新Options数据。对于如何同步Options数据,最理想的场景是在数据源发生变化的时候及时将通知“推送”给应用程序。如果采用本地文件,采用这种方案是很容易实现的。但是在很多情况下,实时监控数据变化的成本很高,消息推送在技术上也不一定可行,此时需要退而求其次,使应用定时获取并更新Options数据。这样的应用场景可以通过注册一个自定义的IOptionsChangeTokenSource<TOptions>实现类型来完成。

在讲述自定义IOptionsChangeTokenSource<TOptions>类型的具体实现之前,先演示针对Options数据的定时刷新。我们依然沿用前面定义的FoobarOptions作为绑定的目标Options类型,而具体的演示程序则体现在如下所示的代码片段中。

class Program
{
    static void Main()
    {
        var random = new Random();
        var optionsMonitor = new ServiceCollection()
            .AddOptions()
            .Configure<FoobarOptions>(TimeSpan.FromSeconds(1))
            .Configure<FoobarOptions>(foobar =>
            {
                foobar.Foo = random.Next(10, 100);
                foobar.Bar = random.Next(10, 100);
            })
            .BuildServiceProvider()
            .GetRequiredService<IOptionsMonitor<FoobarOptions>>();

        optionsMonitor.OnChange(foobar  => Console.WriteLine($"[{DateTime.Now}]{foobar}"));
        Console.Read();
    }
}

如上面的代码片段所示,针对自定义IOptionsChangeTokenSource<TOptions>对象的注册实现在我们为IServiceCollection接口定义的Configure<FoobarOptions>扩展方法中,该方法具有一个TimeSpan类型的参数表示定时刷新Options数据的时间间隔。在演示程序中,我们将这个时间间隔设置为1秒。为了模拟数据的实时变化,可以调用Configure<FoobarOptions>扩展方法注册一个Action<FoobarOptions>对象来更新Options对象的两个属性值。

利用IServiceProvider对象得到IOptionsMonitor<FoobarOptions>对象,并调用其OnChange方法注册了一个Action<FoobarOptions>对象,从而将FoobarOptions承载的数据和当前时间打印出来。由于我们设置的自动刷新时间为1秒,所以程序会以这个频率定时将新的Options数据以下图所示的形式打印在控制台上。

四、TimedRefreshTokenSource<TOptions>

前面演示程序中的Configure<TOptions>扩展方法注册了一个TimedRefreshTokenSource<TOptions>对象,下面的代码片段给出了该类型的完整定义。从给出的代码片段可以看出,实现的OptionsChangeToken方法返回的IChangeToken对象是通过字段_changeToken表示的OptionsChangeToken对象,它与第6章介绍的ConfigurationReloadToken类型具有完全一致的实现。

public class TimedRefreshTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>
{
    private OptionsChangeToken _changeToken;
    public string Name { get; }
    public TimedRefreshTokenSource(TimeSpan interval, string name)
    {
        this.Name = name ?? Options.DefaultName;
        _changeToken = new OptionsChangeToken();
        ChangeToken.OnChange(() => new CancellationChangeToken(new CancellationTokenSource(interval).Token),
            () =>
            {
                var previous = Interlocked.Exchange(ref _changeToken, new OptionsChangeToken());
                previous.OnChange();
            });
    }

    public IChangeToken GetChangeToken() => _changeToken;

    private class OptionsChangeToken : IChangeToken
    {
        private readonly CancellationTokenSource _tokenSource;

        public OptionsChangeToken() => _tokenSource = new CancellationTokenSource();
        public bool HasChanged => _tokenSource.Token.IsCancellationRequested;
        public bool ActiveChangeCallbacks => true;
        public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _tokenSource.Token.Register(callback, state);
        public void OnChange() => _tokenSource.Cancel();
    }
}

通过调用构造函数创建一个TimedRefreshTokenSource<TOptions>对象时,除了需要指定Options的名称,还需要提供一个TimeSpan对象来控制Options自动刷新的时间间隔。在构造函数中,可以通过调用ChangeToken的OnChange方法以这个间隔定期地创建新的OptionsChangeToken对象并赋值给_changeToken。与此同时,我们通过调用前一个OptionsChange Token对象的OnChange方法对外通知Options已经发生变化。

public static class ServiceCollectionExtensions
{
    public static IServiceCollection Configure<TOptions>( this IServiceCollection services, string name, TimeSpan refreshInterval)
        => services.AddSingleton<IOptionsChangeTokenSource<TOptions>>( new TimedRefreshTokenSource<TOptions>(refreshInterval, name));
    public static IServiceCollection Configure<TOptions>( this IServiceCollection services, TimeSpan refreshInterval)
        => services.Configure<TOptions>(Options.DefaultName, refreshInterval);
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [WCF权限控制]通过扩展自行实现服务授权[提供源码下载]

    其实针对安全主体的授权实现的原理很简单,原则上讲,只要你能在服务操作执行之前能够根据本认证的用户正确设置当前的安全主体就可以了。如果你了解WCF的整个运行时框架...

    蒋金楠
  • [ASP.NET Core 3框架揭秘]服务承载系统[5]: 承载服务启动流程[上篇]

    Host类型是对IHost接口的默认实现,它仅仅是定义在NuGet包“Microsoft.Extensions.Hosting”中的一个内部类型,由于我们在本节...

    蒋金楠
  • WCF技术剖析之二十: 服务在WCF体系中是如何被描述的?

    任何一个程序都需要运行于一个确定的进程中,进程是一个容器,其中包含程序实例运行所需的资源。同理,一个WCF服务的监听与执行同样需要通过一个进程来承载。我们将为W...

    蒋金楠
  • 单细胞转录组中的pseudotime究竟是什么

    对于单细胞转录组数据,通过聚类分析,我们可以得到细胞亚型,再通过差异分析,可以得到不同细胞亚型的marker基因,结合下游的功能分析,可以让我们对细胞类型和功能...

    生信修炼手册
  • scRNA-seq—质量控制

    单细胞RNA-seq分析介绍 单细胞RNA-seq的设计和方法 从原始数据到计数矩阵 差异分析前的准备工作 scRNA-seq—读入数据详解

    生信技能树jimmy
  • 浅析PropertySource 基本使用

    一、PropertySource 简介二、@PropertySource与Environment读取配置文件三、@PropertySource与@Value读取...

    cxuan
  • HGE系列之三 渐入佳境

    前两次“乱七八糟”的讲述了一些HGE的基础知识,不知看过的朋友有何感想,反正我自己都觉着有些不知所谓(!),但本着坚持到底的原则,今天继续献上拙文一篇,如果有朋...

    用户2615200
  • [ASP.NET Core 3框架揭秘] 配置[1]:读取配置数据[上篇]

    提到“配置”二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我...

    蒋金楠
  • 面向接口编程

    我们经常说一个库或者模块对外提供了某某API。通过主动暴露的接口来通信,可以隐藏软件系统内部的工作细节。这也是我们最熟悉的第一种接口含义。

    一粒小麦
  • SpringBoot系列-配置解析

    在日常的开发和运维过程中,可以说配置都是及其重要的,因为它可能影响到应用的正常启动或者正常运行。相信在之前 Spring xml 时代,很多人都会被一堆 xml...

    用户4044670

扫码关注云+社区

领取腾讯云代金券