[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數據以下圖所示的形式打印在控制台上。

7-11

四、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);
}

[ASP.NET Core 3框架揭秘] Options[1]: 配置選項的正確使用方式[上篇]
[ASP.NET Core 3框架揭秘] Options[2]: 配置選項的正確使用方式[下篇]
[ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]
[ASP.NET Core 3框架揭秘] Options[4]: Options模型[下篇]
[ASP.NET Core 3框架揭秘] Options[5]: 依賴注入
[ASP.NET Core 3框架揭秘] Options[6]: 擴展與定制
[ASP.NET Core 3框架揭秘] Options[7]: 與配置系統的整合


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM