[Asp.net 5] Options-配置文件之后的配置


今天要講的是OptionsModel解決方案,整個解決方案中也只有Microsoft.Framework.OptionsModel一個工程。按照表面文字OptionsModel應該翻譯成選項模型,但是這個詞沒表現它實際的含義,我覺得稱呼它為配置選項好些,不過為了原滋原味,我們還是用英文的:Configuration和OptionsModel表示它們。

什么是OptionsModel

在之前的配置文件一節([Asp.net 5] Configuration-新一代的配置文件)我們介紹過配置文件最后生成的是IConfiguration對象,但是IConfiguration可能包含很多信息混雜在一起。比如設置日志的等級Level、連接數據庫的字符串connectionstring等。我們有時候不希望直接使用IConfiguration對象(比如對象中的配置項會變動),而是抽取一部分有用的信息以及一些其他值構成的具體的實體對象,那么這個從配置文件之后抽取的配置,就可以叫做OptionsModel。

簡而言之:IConfiguration是配置文件的抽象;OptionsModel配置文件之后的配置,是直接對於系統的配置項。

OptionsModel的特點與實現

這類對象的特點很明顯一般都是只包含一些屬性,並無具體的內部邏輯;但是這種配置可能不是僅有一個:比如有數據庫的connectionModel,而日志可能有LogOption。而且讓這些OptionsModel實現統一的接口也不是現實的,也沒有實際意義。那么如何實現Configuration到OptionsModel的轉換呢?

答案很簡單:Binder(神奇的Binder)。

        [Fact]
        public void CanReadComplexProperties()
        {
            var dic = new Dictionary<string, string>
            {
                {"Integer", "-2"},
                {"Boolean", "TRUe"},
                {"Nested:Integer", "11"}
            };
            var builder = new ConfigurationBuilder(new MemoryConfigurationSource(dic));
            var config = builder.Build();
            var options = ConfigurationBinder.Bind<ComplexOptions>(config);
            Assert.True(options.Boolean);
            Assert.Equal(-2, options.Integer);
            Assert.Equal(11, options.Nested.Integer);
        }

如何DI?

很多工程使用了DependencyInjection(依賴注入),而OptionsModel是比較基礎的配置,幾乎可以肯定內部用到OptionsModel的類會被注入,並且使用類型注冊的方式注入。那么DependencyInjection內部肯定會遞歸到OptionsModel類,所以OptionsModel類也必須要進行注入,那么如何實現?

答曰:使用實例(Instance)直接注入到該類類型。[services.AddInstance(serviceType, type.Assembly.CreateInstance(type));]

如果我對於多個OptionsModel注冊,但是我可以通過命名方式注入,還可以進行排序,那又該如何實現?

答曰:Microsoft.Framework.OptionsModel。

Microsoft.Framework.OptionsModel

類文件分類

在Microsoft.Framework.OptionsModel中,使用泛型的方式進行注入,之后以泛型的方式獲取注入;但是注入的類和獲取的泛型類確是不完全一致的。

  • 注入的類和接口:IConfigureOptions<in TOptions>、ConfigureOptions<TOptions>、ConfigureFromConfigurationOptions<TOptions>
  • 獲取注入的類和接口:IOptions<out TOptions>、OptionsManager<TOptions>
  • 注冊的擴展類: OptionsServiceCollectionExtensions
  • 其他:OptionsConstants

注入類和接口

 IConfigureOptions<in TOptions>、ConfigureOptions<TOptions>、ConfigureFromConfigurationOptions<TOptions>這幾個類和接口的關系為:

 

    public interface IConfigureOptions<in TOptions>
    {
        int Order { get; }
        void Configure(TOptions options, string name = "");
    }
IConfigureOptions
    public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions>
    {
        public ConfigureOptions([NotNull]Action<TOptions> action)
        {
            Action = action;
        }

        public Action<TOptions> Action { get; private set; }

        public string Name { get; set; } = "";
        public virtual int Order { get; set; } = OptionsConstants.DefaultOrder;

        public virtual void Configure([NotNull]TOptions options, string name = "")
        {
            // Always invoke the action if no Name was specified, otherwise only if it was the requested name
            if (string.IsNullOrEmpty(Name) || string.Equals(name, Name, StringComparison.OrdinalIgnoreCase))
            {
                Action.Invoke(options);
            }
        }
    }
ConfigureOptions
    public class ConfigureFromConfigurationOptions<TOptions> : ConfigureOptions<TOptions>
    {
        public ConfigureFromConfigurationOptions([NotNull] IConfiguration config)
            : base(options => ConfigurationBinder.Bind(options, config))
        {
        }
    }
ConfigureFromConfigurationOptions

注入類和接口的注入

在OptionsServiceCollectionExtensions中提供了三種注冊方式(實際是七個方法和重載):

  • 直接注冊實現 IConfigureOptions<in TOptions>類型的數據。(假設實現類的類型為OptionsDev,則實際注冊[services.AddTransient(IConfigureOptions<T>,OptionsDev)])
  • 提供Action<TOptions>進行注冊。(創建實例instanceA=new ConfigureOptions<TOptions>(setupAction),之后注冊[services.AddInstance(IConfigureOptions<T>,configureInstance)])
  • 提供IConfiguration進行注冊。(創建實例instanceA=new ConfigureFromConfigurationOptions<TOptions>(setupAction),之后注冊[services.AddInstance(IConfigureOptions<T>, configureInstance)])
    public static class OptionsServiceCollectionExtensions
    {
        public static IServiceCollection AddOptions([NotNull]this IServiceCollection services)
        {
            services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
            return services;
        }

        private static bool IsAction(Type type)
        {
            return (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Action<>));
        }

        private static IEnumerable<Type> FindIConfigureOptions(Type type)
        {
            var serviceTypes = type.GetTypeInfo().ImplementedInterfaces
                .Where(t => t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition() == typeof(IConfigureOptions<>));
            if (!serviceTypes.Any())
            {
                string error = "TODO: No IConfigureOptions<> found.";
                if (IsAction(type))
                {
                    error += " did you mean Configure(Action<T>)";
                }
                throw new InvalidOperationException(error);
            }
            return serviceTypes;
        }

        public static IServiceCollection ConfigureOptions([NotNull]this IServiceCollection services, Type configureType)
        {
            var serviceTypes = FindIConfigureOptions(configureType);
            foreach (var serviceType in serviceTypes)
            {
                services.AddTransient(serviceType, configureType);
            }
            return services;
        }

        public static IServiceCollection ConfigureOptions<TSetup>([NotNull]this IServiceCollection services)
        {
            return services.ConfigureOptions(typeof(TSetup));
        }

        public static IServiceCollection ConfigureOptions([NotNull]this IServiceCollection services, [NotNull]object configureInstance)
        {
            var serviceTypes = FindIConfigureOptions(configureInstance.GetType());
            foreach (var serviceType in serviceTypes)
            {
                services.AddInstance(serviceType, configureInstance);
            }
            return services;
        }

        public static IServiceCollection Configure<TOptions>([NotNull]this IServiceCollection services,
            [NotNull] Action<TOptions> setupAction,
            string optionsName)
        {
            return services.Configure(setupAction, OptionsConstants.DefaultOrder, optionsName);
        }

        public static IServiceCollection Configure<TOptions>([NotNull]this IServiceCollection services,
            [NotNull] Action<TOptions> setupAction,
            int order = OptionsConstants.DefaultOrder,
            string optionsName = "")
        {
            services.ConfigureOptions(new ConfigureOptions<TOptions>(setupAction)
            {
                Name = optionsName,
                Order = order
            });
            return services;
        }

        public static IServiceCollection Configure<TOptions>([NotNull]this IServiceCollection services,
            [NotNull] IConfiguration config, string optionsName)
        {
            return services.Configure<TOptions>(config, OptionsConstants.ConfigurationOrder, optionsName);
        }

        public static IServiceCollection Configure<TOptions>([NotNull]this IServiceCollection services,
            [NotNull] IConfiguration config,
            int order = OptionsConstants.ConfigurationOrder, 
            string optionsName = "")
        {
            services.ConfigureOptions(new ConfigureFromConfigurationOptions<TOptions>(config)
            {
                Name = optionsName,
                Order = order
            });
            return services;
        }
    }
OptionsServiceCollectionExtensions

我們可以看到OptionsServiceCollectionExtensions內部使用的是泛型注冊,不同的方法只是注冊的方式不一樣,最終結果注冊的都是IConfigureOptions<T>對象。

獲取注入的類和接口
OptionsServiceCollectionExtensions中獲取方式的注冊:

  • services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));

我們獲取的時候,IOptions<T>,那么獲取的一定是OptionsManager<T>對象,那么我們看下相關的實現代碼:

    public interface IOptions<out TOptions> where TOptions : class,new()
    {
        TOptions Options { get; }
        TOptions GetNamedOptions(string name);
    }
IOptions
    public class OptionsManager<TOptions> : IOptions<TOptions> where TOptions : class,new()
    {
        private object _mapLock = new object();
        private Dictionary<string, TOptions> _namedOptions = new Dictionary<string, TOptions>(StringComparer.OrdinalIgnoreCase);
        private IEnumerable<IConfigureOptions<TOptions>> _setups;

        public OptionsManager(IEnumerable<IConfigureOptions<TOptions>> setups)
        {
            _setups = setups;
        }

        public virtual TOptions GetNamedOptions([NotNull] string name)
        {
            if (!_namedOptions.ContainsKey(name))
            {
                lock (_mapLock)
                {
                    if (!_namedOptions.ContainsKey(name))
                    {
                        _namedOptions[name] = Configure(name);
                    }
                }
            }
            return _namedOptions[name];
        }

        public virtual TOptions Configure(string optionsName = "")
        {
            return _setups == null 
                ? new TOptions() 
                : _setups.OrderBy(setup => setup.Order)
                         .Aggregate(new TOptions(),
                                    (options, setup) =>
                                    {
                                        setup.Configure(options, optionsName);
                                        return options;
                                    });
        }

        public virtual TOptions Options
        {
            get
            {
                return GetNamedOptions("");
            }
        }
    }
OptionsManager

由於OptionsManager是使用類類型進行注入的,所以一定會調用OptionsManager類的構造函數,但是構造函數中包含參數(類型為IEnumerable<IConfigureOptions<T>>),那么系統就會遞歸獲取其參數的注入方式。那么我們就會獲取到所有IConfigureOptions<T>的注入項。之后我們就可以根據我們獲取到的IConfigureOptions<T>類型,進行相關的排序,獲取實例。

獲取IEnumerable<T>的注入方式,請詳細見( [Asp.net 5] DependencyInjection項目代碼分析4-微軟的實現(5)(IEnumerable<>補充)

OptionsManager詳解

對於OptionsManager類中_setups是已經獲取到的IConfigureOptions<T>集合。那么我們獲取Options/GetNamedOptions([NotNull] string name)時候,實際上內部在第一次會執行Configure方法

        public virtual TOptions Configure(string optionsName = "")
        {
            return _setups == null 
                ? new TOptions() 
                : _setups.OrderBy(setup => setup.Order)
                         .Aggregate(new TOptions(),
                                    (options, setup) =>
                                    {
                                        setup.Configure(options, optionsName);
                                        return options;
                                    });
        }

該方法首先判斷setups是否有值。如果沒值創建新的;如果有值,則排序后,進行函數聚合。

 Aggregate聚合函數:該函數包含倆個參數初始值T A,返回為T類型的Func(T,item,T)。系統會便利內部的所有item,之后使用初始值A,以及當前item進行函數計算,之后返回A‘,然后將A’賦值A,進行下一次循環,最后得到的A進行返回。

測試用例

    public class FakeOptions
    {
        public FakeOptions()
        {
            Message = "";
        }

        public string Message { get; set; }
    }
    public class FakeOptionsSetupA : ConfigureOptions<FakeOptions>
    {
        public FakeOptionsSetupA() : base(o => o.Message += "A")
        {
            Order = -1;
        }
    }

    public class FakeOptionsSetupB : ConfigureOptions<FakeOptions>
    {
        public FakeOptionsSetupB() : base(o => o.Message += "B")
        {
            Order = 10;
        }
    }

    public class FakeOptionsSetupC : ConfigureOptions<FakeOptions>
    {
        public FakeOptionsSetupC() : base(o => o.Message += "C")
        {
            Order = 1000;
        }
    }
測試基礎類
        public void SetupCallsSortedInOrder()
        {
            var services = new ServiceCollection().AddOptions();
            var dic = new Dictionary<string, string>
            {
                {"Message", "!"},
            };
            var builder = new ConfigurationBuilder(new MemoryConfigurationSource(dic));
            var config = builder.Build();
            services.Configure<FakeOptions>(o => o.Message += "Igetstomped", -100000);
            services.Configure<FakeOptions>(config);
            services.Configure<FakeOptions>(o => o.Message += "a", -100);
            services.ConfigureOptions<FakeOptionsSetupC>();
            services.ConfigureOptions(new FakeOptionsSetupB());
            services.ConfigureOptions(typeof(FakeOptionsSetupA));
            services.Configure<FakeOptions>(o => o.Message += "z", 10000);

            var service = services.BuildServiceProvider().GetService<IOptions<FakeOptions>>();
            Assert.NotNull(service);
            var options = service.Options;
            Assert.NotNull(options);
            Assert.Equal("!aABCz", options.Message);
        }
public void NamedSetupDoNotCollideWithEachOther()
        {
            var services = new ServiceCollection().AddOptions();
            var dic = new Dictionary<string, string>
            {
                {"Message", "!"},
            };
            var builder = new ConfigurationBuilder(new MemoryConfigurationSource(dic));
            var config = builder.Build();

            services.ConfigureOptions(new FakeOptionsSetupB { Name = "2" });
            services.Configure<FakeOptions>(o => o.Message += "Z", 10000, "2");

            services.ConfigureOptions(new FakeOptionsSetupB { Name = "3" });
            services.Configure<FakeOptions>(config, "3");
            services.Configure<FakeOptions>(o => o.Message += "z", 10000, "3");

            var service = services.BuildServiceProvider().GetService<IOptions<FakeOptions>>();
            Assert.NotNull(service);
            var options = service.Options;
            Assert.NotNull(options);
            Assert.Equal("", options.Message);

            var options2 = service.GetNamedOptions("2");
            Assert.NotNull(options2);
            Assert.Equal("BZ", options2.Message);

            var options3 = service.GetNamedOptions("3");
            Assert.NotNull(options3);
            Assert.Equal("!Bz", options3.Message);

        }

 


免責聲明!

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



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