輕量級.Net Core服務註冊工具CodeDi發佈啦

爲何作這麼一個工具

由於咱們的系統每每時面向接口編程的,因此在開發Asp .net core項目的時候,必定會有大量大接口及其對應的實現要在ConfigureService註冊到ServiceCollection中,傳統的作法是加了一個服務,咱們就要註冊一次(service.AddService()),又好比,當一個接口有多個實現,在構造函數中獲取服務也不是很友好,而據我所知, .Net Core目前是沒有什麼自帶的庫或者方法解決這些問題,固然,若是引入第三方容器如AutoFac這些問題時能迎刃而解的,可是如何在不引入第三方容器來解決這個問題呢?
因此我就設計了這樣的一個輕量級工具.html

首先,放上該項目的Github地址(記得Star哦!!)git

https://github.com/liuzhenyulive/CodeDiBuild statusgithub

CodeDi是一個基於 .Net Standard的工具庫,它能幫助咱們自動地在Asp .net core或者 .net core項目中完成服務的註冊.編程

Overview

CodeDi 是 Code Dependency Injection的意思,在上次我在看了由依樂祝寫的<.NET Core中的一個接口多種實現的依賴注入與動態選擇看這篇就夠了>後,回想起我以前遇到的那些問題,感受撥雲見日,因此,我就開始着手寫這個工具了.json

如何使用CodeDi

安裝Nuget包

CodeDi的Nuget包已經發布到了 nuget.org,您能夠經過如下指令在您的項目中安裝CodeDiapp

PM> Install-Package CodeDi

ConfigureServices中的配置

方法 1

您能夠在StartupConfigureService方法中添加AddCodeDi完成對CodeDi的調用.服務的註冊CodeDi會自動爲您完成.ide

public void ConfigureServices(IServiceCollection services)
        {
            services.AddCoreDi();
            services.AddMvc();
        }

方法 2

您也能夠在AddCodeDi方法中傳入一個Action<CodeDiOptions>參數,在這個action中,您能夠對CodeDiOptions的屬性進行配置.函數

public void ConfigureServices(IServiceCollection services)
        {
            services.AddCoreDi(options =>
            {
                options.DefaultServiceLifetime = ServiceLifetime.Scoped;

            });
            services.AddMvc();
        }

方法 3

固然您也能夠直接給AddCodeDi()方法直接傳入一個CodeDiOptions實例.工具

public void ConfigureServices(IServiceCollection services)
        {
            services.AddCoreDi(new CodeDiOptions()
            {
                DefaultServiceLifetime = ServiceLifetime.Scoped
            });
            services.AddMvc();
        }

你也能夠在appsetting.json文件中配置CodeDiOptions的信息,並經過Configuration.Bind("CodeDiOptions", options)把配置信息綁定到一個CodeDiOptions實例.ui

appsetting.json file

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "CodeDiOptions": {
    "DefaultServiceLifetime": 1,
    "AssemblyNames": [
      "*CodeDi"
    ],
    "AssemblyPaths": [
      "C:\\MyBox\\Github\\CodeDI\\CodeDI\\bin\\Debug\\netstandard2.0"
    ],
    "IgnoreAssemblies": [
      "*Test"
    ],
    "IncludeSystemAssemblies": false,
    "IgnoreInterface": [
      "*Say"
    ],
    "InterfaceMappings": {
      "*Say": "*English"
    },
    "ServiceLifeTimeMappings": {
      "*Say": 0
    }
  }
}

ConfigureService方法

public void ConfigureServices(IServiceCollection services)
        {
            var options=new CodeDiOptions();
            Configuration.Bind("CodeDiOptions", options);
            services.AddCoreDi(options);
            services.AddMvc();
        }

CodeDiOptions詳解

屬性名稱 屬性描述 數據類型 默認值
AssemblyPaths 在指定目錄下加載Dll程序集 string[] Bin目錄
AssemblyNames 選擇要加載的程序集名稱 (支持通配符) string[] *
IgnoreAssemblies 忽略的程序集名稱 (支持通配符) string[] null
IncludeSystemAssemblies 是否包含系統程序集(當爲false時,會忽略含有System,Microsoft,CppCodeProvider,WebMatrix,SMDiagnostics,Newtonsoft關鍵詞和在App_Web,App_global目錄下的程序集) bool false
IgnoreInterface 忽略的接口 (支持通配符) string[] null
InterfaceMappings 接口對應的服務 (支持通配符) ,當一個接口有多個實現時,若是不進行配置,則多個實現都會註冊到SerciceCollection中 Dictionary<string, string> null
DefaultServiceLifetime 默認的服務生命週期 ServuceLifetime( Singleton,Scoped,Transient) ServiceLifetime.Scope
ServiceLifeTimeMappings 指定某個接口的服務生命週期,不指定爲默認的生命週期 Dictionary<string, ServiceLifetime> null

InterfaceMappings

若是 ISay 接口有SayInChineseSayInEnglish 兩個實現,咱們只想把SayInEnglish註冊到ServiceCollection

public interface ISay
    {
        string Hello();
    }

      public class SayInChinese:ISay
    {
        public string Hello()
        {
            return "您好";
        }
    }

        public class SayInEnglish:ISay
    {
        public string Hello()
        {
            return "Hello";
        }
    }

那麼咱們能夠這樣配置InterfaceMappings.

options.InterfaceMappings=new Dictionary<string, string>(){{ "ISay", "SayInChinese" } }

也就是{接口名稱(支持通配符),實現名稱(支持通配符)}

ServiceLifeTimeMappings

若是咱們但願ISay接口的服務的生命週期爲Singleton,咱們能夠這樣配置ServiceLifeTimeMappings.

options.ServiceLifeTimeMappings = new Dictionary<string, ServiceLifetime>(){{"*Say",ServiceLifetime.Singleton}};

也就是也就是{接口名稱(支持通配符),Servicelifetime}

關於ServiceLifetime: https://github.com/aspnet/DependencyInjection/blob/master/src/DI.Abstractions/ServiceLifetime.cs

獲取服務實例

固然, 您能夠和以前同樣,直接在構造函數中進行依賴的注入,可是當某個接口有多個實現並且都註冊到了ServiceCollection中,獲取就沒有那麼方便了,您能夠用ICodeDiServiceProvider 來幫助您獲取服務實例.

例如,當 ISay 接口有 SayInChineseSayInEnglish兩個實現, 咱們咱們如何獲取咱們想要的服務實例呢?

public interface ISay
    {
        string Hello();
    }

      public class SayInChinese:ISay
    {
        public string Hello()
        {
            return "您好";
        }
    }

        public class SayInEnglish:ISay
    {
        public string Hello()
        {
            return "Hello";
        }
    }
public class HomeController : Controller
    {
        private readonly ISay _say;

        public HomeController(ICodeDiServiceProvider serviceProvider)
        {
            _say = serviceProvider.GetService<ISay>("*Chinese");
        }

        public string Index()
        {
            return _say.Hello();
        }
    }

ICodeDiServiceProvider.GetService<T>(string name=null)
參數中的Name支持通配符.

CodeDi如何實現的?

既然是一個輕量級工具,那麼實現起來天然不會太複雜,我來講說比較核心的代碼.

private Dictionary<Type, List<Type>> GetInterfaceMapping(IList<Assembly> assemblies)
        {
            var mappings = new Dictionary<Type, List<Type>>();
            var allInterfaces = assemblies.SelectMany(u => u.GetTypes()).Where(u => u.IsInterface);
            foreach (var @interface in allInterfaces)
            {
                mappings.Add(@interface, assemblies.SelectMany(a =>
                        a.GetTypes().
                            Where(t =>
                                t.GetInterfaces().Contains(@interface)
                            )
                    )
                    .ToList());
            }
            return mappings;
        }

GetInterfaceMapping經過反射機制,首先獲取程序集中的全部接口allInterfaces,而後遍歷allInterfaces找到該接口對應的實現,最終,該方法返回接口和實現的匹配關係,爲Dictionary<Type, List >類型的數據.

private void AddToService(Dictionary<Type, List<Type>> interfaceMappings)
        {
            foreach (var mapping in interfaceMappings)
            {
                if (mapping.Key.FullName == null || (_options.IgnoreInterface != null &&
                   _options.IgnoreInterface.Any(i => mapping.Key.FullName.Matches(i))))
                    continue;

                if (mapping.Key.FullName != null && _options.InterfaceMappings != null &&
                    _options.InterfaceMappings.Any(u => mapping.Key.FullName.Matches(u.Key)))
                {
                    foreach (var item in mapping.Value.Where(value => value.FullName != null).
                        Where(value => value.FullName.Matches(_options.InterfaceMappings.FirstOrDefault(u => mapping.Key.FullName.Matches(u.Key)).Value)))
                    {
                        AddToService(mapping.Key, item);
                    }
                    continue;
                }

                foreach (var item in mapping.Value)
                {
                    AddToService(mapping.Key, item);
                }
            }
        }

該方法要判斷CodeDiOptions中是否忽略了該接口,同時,是否指定實現映射關係.
什麼叫實現映射關係呢?參見InterfaceMappings
若是指定了,那麼就按指定的來實現,若是沒指定,就會把每一個實現都註冊到ServiceCollection中.

private readonly IServiceCollection _service;
        private readonly CodeDiOptions _options;
        private readonly ServiceDescriptor[] _addedService;

        public CodeDiService(IServiceCollection service, CodeDiOptions options)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _options = options ?? new CodeDiOptions();
            _addedService = new ServiceDescriptor[service.Count];
            service.CopyTo(_addedService, 0);
            //在構造函數中,咱們經過這種方式把Service中已經添加的服務讀取出來
            //後面進行服務註冊時,會進行判斷,避免重複添加
        }

        private void AddToService(Type serviceType, Type implementationType)
        {
            ServiceLifetime serviceLifetime;
            try
            {
                serviceLifetime = _options.DefaultServiceLifetime;
                if (_options.ServiceLifeTimeMappings != null && serviceType.FullName != null)
                {
                    var lifeTimeMapping =
                        _options.ServiceLifeTimeMappings.FirstOrDefault(u => serviceType.FullName.Matches(u.Key));

                    serviceLifetime = lifeTimeMapping.Key != null ? lifeTimeMapping.Value : _options.DefaultServiceLifetime;

                }
            }
            catch
            {
                throw new Exception("Service Life Time Only Can be set in range of 0-2");
            }

            if (_addedService.Where(u => u.ServiceType == serviceType).Any(u => u.ImplementationType == implementationType))
                return;
            _service.Add(new ServiceDescriptor(serviceType, implementationType, serviceLifetime));
        }

AddToService中,要判斷有沒有對接口的生命週期進行配置,參見ServiceLifeTimeMappings,若是沒有配置,就按DefaultServiceLifetime進行配置,DefaultServiceLifetime若是沒有修改的狀況下時ServiceLifetime.Scoped,即每一個Request建立一個實例.

private readonly IServiceProvider _serviceProvider;
        public CodeDiServiceProvider(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
        public T GetService<T>(string name) where T : class
        {
            return _serviceProvider.GetService<IEnumerable<T>>().FirstOrDefault(u => u.GetType().Name.Matches( name));
        }

這CodeDiServiceProvider的實現代碼,這裏參考了依樂祝寫的<.NET Core中的一個接口多種實現的依賴注入與動態選擇看這篇就夠了>給出的一種解決方案,即當某個接口註冊了多個實現,其實能夠經過IEnumerable 獲取全部的實現,CodeDiServiceProvider對其進行了封裝.

Enjoy it

只要進行一次簡單的CodeDi配置,之後系統中添加了新的接口以及對應的服務實現後,就不用再去一個個地Add到IServiceCollection中了.

若是有問題,歡迎Issue,歡迎PR.
最後,賞個Star唄! 前往Star

相關文章
相關標籤/搜索