1. 程式人生 > >.Net Core 中的 DI使用

.Net Core 中的 DI使用

概要:因為不知道寫啥,所以隨便找個東西亂說幾句,嗯,就這樣,就是這個目的。

1.IOC是啥呢?

  IOC - Inversion of Control,即控制反轉的意思,這裡要搞明白的就是,它是一種思想,一種用於設計的方式(手段),(並不是前幾天園子中剛出的一片說是原則),OO原則不包含它,再說下,他不是原則!!

  那麼,既然是控制反轉,怎麼反轉的?對吧,說到重點了吧。很簡單,通過一個容器,將物件註冊到這個容器之後,由這個容器來建立物件,從而免去了手動建立物件以及建立後物件(資源)的獲取。

  你可能又會問,有什麼好處?我直接new不也一樣的嗎?對的,你說的很對,但是這樣做必然導致了物件之間的耦合度增加了,既不方便測試,又不方便複用;IOC卻很好的解決了這些問題,可以i很容易創建出一個鬆耦合的應用框架,同時更方便於測試。

  常用的 IOC工具有 Autofac,castle windsor,unit,structMap等,本人使用過的只有 autofac,unit,還有自帶的mef,,,也能算一個吧,還有現在的core的 DependencyInJection。

2.Core中的DI是啥?

依賴注入的有三種方式:屬性,建構函式,介面注入;

  在core之前,我們在.net framework中使用 autofac的時候,這三種方式我們可以隨意使用的,比較方便(因為重點不是在這,所以不說core之前),但是有一點是,在web api和 web中屬性注入稍微有點不同,自行擴充套件吧。

  core中我們使用DI(dependency injection)的時候,屬性注入好像還不支援,所以跳過這個,我們使用更多的是通過自定義介面使用建構函式注入。下面會有演示。

  演示前我們先弄清楚 core中的這個 dependencyInJection到底是個啥,他是有啥構成的。這是git上提供的原始碼:https://github.com/aspnet/DependencyInjection,但是,,那麼一大陀東西你肯定不想看,想走捷徑吧,所以這裡簡要說一下,看圖:

當我們建立了一個core 的專案之後,我們,會看到 startUp.cs的ConfigureServices使用了一個IServiceCollection的引數,這個東西,就是我們1中所說的IOC的容器,他的構成如圖所示,是由一系列的ServiceDescriptor組成,是一個集合物件,而本質上而言,

ServiceDescriptor也是一個容器,其中定義了物件了型別以及生命週期,說白了控制生命週期的,也是有他決定的(生命週期:Scoped:本次完整請求的生命,Singleton:伴隨整個應用程式神一樣存在的宣告,Transient:瞬時宣告,用一下消失)。

另外,我們還會見到另一個東西,IServiceProvider,這個是服務的提供器,IServiceCollection在獲取一系列的ServiceDescriptor之後其實他還是並沒有建立我們所需要的實現物件的,比如AssemblyFinder實現了IAssemblyFinder的介面,此時我們只是將其加入IserviceCollection,

直接使用IAssemblyFinder獲取到的一定是null物件,這裡是通過BuildServiceProvider()這個方法,將這二者對映在一起的,此時這個容器才真正的建立完成,這時候我們再使用 IAssemblyFinder的時候便可以正常。這個動作好比是我們自己通過Activator 反射建立某個介面的實現類,

當然其內部也是這個樣的實現道理。如果需要更深入見這篇文章:https://www.cnblogs.com/cheesebar/p/7675214.html

 好了,扯了這麼多理論,說一千道一萬不如來一個實戰,下面就看怎麼用。

3.怎麼用?

  首先我們先快速建立一個專案,並建立ICustomerService介面和CustomerServiceImpl實現類,同時在startup.cs的ConfigureService中註冊到services容器:

  

  測試程式碼: 

public interface ICustomerService : IDependency
    {
        Task<string> GetCustomerInfo();
    }
public class CustomerServiceImpl : ICustomerService
    {
        public async Task<string> GetCustomerInfo()
        {
            return await Task.FromResult("放了一年的牛了");
        }
    }
startUp.cs的ConfigureService中註冊到容器:
public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<ICustomerService, CustomerServiceImpl>();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }
//controller中的注入
private readonly ICustomerService _customerService;
        public ValuesController(ICustomerService customerService)
        {
            _customerService = customerService;
        }

        // GET api/values
        [HttpGet]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            return new string[] { await _customerService.GetCustomerInfo()};
        }
View Code

  然後我們在controller中注入並檢視結果:

  

  這就實現了我們想要的效果了,這個比較簡單,入門都算不上。

   可以看到我們註冊到services容器中的時候,是一一對應寫入的(services.AddTransient<ICustomerService, CustomerServiceImpl>();),但是實際開發會有很多個介面和介面的實現類,多個人同時改這個檔案(startup.cs),那還不壞事兒了嘛,集體提交肯定出現衝突或者重複或者遺漏.對吧,而且不方便測試;所以:怎麼優雅一點?

4.怎麼優雅的使用?

   實際開發過程中,我們不可能去手動一個一個的注入的,除非你專案組就你一個人。所以,需要實現按自動注入,還是就上面的測試,再加一個 IProductService和和他的實現類ProductServiceImpl,

public interface IProductService  { Task<string> GetProductInfo(); }

public class ProductServiceImpl : IProductService { public async Task<string> GetProductInfo() { return await Task.FromResult("我是一個產品"); } }

同時controller中修改下:

private readonly ICustomerService _customerService;
        private readonly IProductService _productService;
        public ValuesController(ICustomerService customerService, IProductService productService)
        {
            _customerService = customerService;
            _productService = productService;
        }

        // GET api/values
        [HttpGet]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            return new string[] { await _customerService.GetCustomerInfo(), await _productService.GetProductInfo() };
        }
View Code

  實現自動注入就需要有一個物件的查詢的依據,也就是一個基物件,按照我們以往使用Autofac的習慣,我們會定義一個IDependency介面:好,那我們就定義一個

public interface IDependency { }

   然後修改 ICustomerService和IProductService的介面,都去繼承這個IDependency.

  修改完成後重點來了,怎麼通過Idependency獲取的呢?像autofac的使用一樣?當然已經有dependencyInjection的擴充套件外掛可以支援 scan對應的依賴項並註冊到IOC ,但是畢竟是人家的東西,所以我們自己搞搞。也好解決,我們獲取到當前應用的依賴項,然後找到Idependecy對應的實現物件(類),

  1).使用 DependencyContext 獲取當前應用的依賴項:

  該物件在Microsoft.Extensions.DependencyModel空間下,可以通過 DependencyContext.Default獲取到當前應用依賴的所有物件(dll),如下實現:

DependencyContext context = DependencyContext.Default;
string[] fullDllNames = context
                .CompileLibraries
                .SelectMany(m => m.Assemblies)
                .Distinct().Select(m => m.Replace(".dll", ""))
                .ToArray();
View Code

  因為我們下面將使用Assembly.Load(dll的名稱)載入對應的dll物件,所以這裡把字尾名給替換掉。

  但是這裡有個問題,這樣獲取到的物件包含了 微軟的一系列東西,不是我們注入所需要的,或者說不是我們自定義的物件,所以需要過濾掉。不必要的物件包含(我在測試時候大致列出來這幾個)

string[] 不需要的程式及物件 =
            {
                "System",
                "Microsoft",
                "netstandard",
                "dotnet",
                "Window",
                "mscorlib",
                "Newtonsoft",
                "Remotion.Linq"
            };
View Code

  所以我們再過濾掉上面這幾個不需要的物件

//只取物件名稱
            List<string> shortNames = new List<string>();
            fullDllNames.ToList().ForEach(name =>
            {
                var n = name.Substring(name.LastIndexOf('/') + 1);
                if (!不需要的程式及物件.Any(non => n.StartsWith(non)))
                    shortNames.Add(n);
            });
View Code

  最後,就是使用Assembly.Load載入獲取並過濾之後的程式集物件了,同時獲取到IDependency的子物件的實現類集合物件(types)

List<Assembly> assemblies = new List<Assembly>();
            foreach (var fileName in shortNames)
            {
                AssemblyName assemblyName = new AssemblyName(fileName);
                try { assemblies.Add(Assembly.Load(assemblyName)); }
                catch { }
            }
            var baseType = typeof(IDependency);
            Type[] types = assemblies.SelectMany(assembly => assembly.GetTypes())
                .Where(type => type.IsClass && baseType.IsAssignableFrom(type)).Distinct().ToArray();
View Code

此時再看我們的使用效果:

在跟目錄再新增一個以來擴充套件類:其中的實現就是上面說的獲取程式及以及註冊到services容器:

這裡也就是上面說的 完整的程式碼:

public static class DependencyExtensions
    {
        public static IServiceCollection RegisterServices(this IServiceCollection services)
        {
            //之前的實現,
            //services.AddTransient<ICustomerService, CustomerServiceImpl>();
            //services.AddTransient<IProductService, ProductServiceImpl>();

            //現在的實現
            Type[] types = GetDependencyTypes();
            types?.ToList().ForEach(t =>
            {
                var @interface = t.GetInterfaces().Where(it => it.GetType() != typeof(IDependency)).FirstOrDefault();
                //services.AddTransient(@interface.GetType(), t.GetType());
                services.AddTransient(@interface.GetTypeInfo(),t.GetTypeInfo());
            });

            return services;
        }

        private static Type[] GetDependencyTypes()
        {
            string[] 不需要的程式及物件 =
            {
                "System",
                "Microsoft",
                "netstandard",
                "dotnet",
                "Window",
                "mscorlib",
                "Newtonsoft",
                "Remotion.Linq"
            };

            DependencyContext context = DependencyContext.Default;//depnedencyModel空間下,如果是 傳統.netfx,可以使用 通過 Directory.GetFiles獲取 AppDomain.CurrentDomain.BaseDirectory獲取的目錄下的dll及.exe物件;
            string[] fullDllNames = context
                .CompileLibraries
                .SelectMany(m => m.Assemblies)
                .Distinct().Select(m => m.Replace(".dll", ""))
                .ToArray();

            //只取物件名稱
            List<string> shortNames = new List<string>();
            fullDllNames.ToList().ForEach(name =>
            {
                var n = name.Substring(name.LastIndexOf('/') + 1);
                if (!不需要的程式及物件.Any(non => n.StartsWith(non)))
                    shortNames.Add(n);
            });

            List<Assembly> assemblies = new List<Assembly>();
            foreach (var fileName in shortNames)
            {
                AssemblyName assemblyName = new AssemblyName(fileName);
                try { assemblies.Add(Assembly.Load(assemblyName)); }
                catch { }
            }
            var baseType = typeof(IDependency);
            Type[] types = assemblies.SelectMany(assembly => assembly.GetTypes())
                .Where(type => type.IsClass && baseType.IsAssignableFrom(type)).Distinct().ToArray();
            return types;
        }
    }
View Code

注:獲取程式集的這個實現可以單獨放到一個類中實現,然後註冊成為singleton物件,同時在該類中定義一個私有的 幾何物件,用於存放第一次獲取的物件集合(types),以後的再訪問直接從這個變數中拿出來,減少不必要的資源耗費和提升效能

此時startup.cs中只需要一行程式碼就好了:

services.RegisterServices();

看結果:

過濾之後的僅有我們定義的兩個物件。

  

5.怎麼更優雅的使用?

  以上只是獲取我們開發過程中使用的一些業務或者邏輯實現物件的獲取,集體開發的時候 假設沒人或者每個小組開發各自模組時候,建立各自的應用程式及物件(類似模組式或外掛式開發),上面那樣豈不是不能滿足了?每個組的每個模組都定義一次啊?不現實是吧。比如:如果按照DDD的經典四層分層的話,身份驗證或者授權 功能的實現應該是在基礎設施曾(Infrastructure)實現的,再比如 如果按照聚合邊界的劃分,不同域可能是單獨一個應用程式集包含獨自的上下文物件,那麼個開發人員開發各自模組,這時候就需要一個統一的DI注入的約束了,否則將會變得很亂很糟糕。所以我們可以將這些獨立的模組可統稱為Module模組,用誰就注入誰。

  如下(模組的基類):

public abstract class Module
    {
        /// <summary>
        /// 獲取 模組啟動順序,模組啟動的順序先按級別啟動,同一級別內部再按此順序啟動,
        /// 級別預設為0,表示無依賴,需要在同級別有依賴順序的時候,再重寫為>0的順序值
        /// </summary>
        public virtual int Order => 0;
        public virtual IServiceCollection RegisterModule(IServiceCollection services)
        {
            return services;
        }

        /// <summary>
        /// 應用模組服務
        /// </summary>
        /// <param name="provider">服務提供者</param>
        public virtual void UseModule(IServiceProvider provider)
        {
        }
    }
View Code

  定義一個注入的使用基物件,其中包含了兩個個功能:

  1).將當前模組涉及的依賴項註冊到services容器的功能;

  2).在註冊到容器的物件中包含部分方法需要被呼叫之後才能初始化的物件(資源)方法,該方法將在startUp.cs的Configure方法中使用,類似UseMvc();

  3).一個用於標記注入到services容器的先後順序的標識。

  這個Order存在的必要性?是很有必要的,比如在開發過程中,每個模組獨立開發需要獨立的上下文物件,那麼豈不是要每個模組都建立一次資料庫連線配置,蛋疼吧?所以,可以將上下文訪問單獨封裝,比如我們慣用的倉儲物件,和工作單元,用來提供統一的上下文訪問入口(iuow),以及統一的領域物件的操作方法(irepository-CURD),然後將iuow注入到各個模組以便獲取上下文物件。那麼這就有個先後順序了,肯定要先註冊這個倉儲和工作單元所在的程式集(module),其次是每個業務的外掛模組。

  接下來就是定義這個Module的查詢器,其實和上面 4 中的類似,只是需要將 basetType(IDependency替換成 Module即可),assembly的過濾條件換成 type => type.IsClass &&!type.IsAbstract && baseType.IsAssignableFrom(type)

  當然,這裡的IsAssignableFrom是針對非泛型物件的,如果是泛型物件需要單獨處理下,如下,原始碼來自O#:

/// <summary>
        /// 判斷當前泛型型別是否可由指定型別的例項填充
        /// </summary>
        /// <param name="genericType">泛型型別</param>
        /// <param name="type">指定型別</param>
        /// <returns></returns>
        public static bool IsGenericAssignableFrom(this Type genericType, Type type)
        {
            genericType.CheckNotNull("genericType");
            type.CheckNotNull("type");
            if (!genericType.IsGenericType)
            {
                throw new ArgumentException("該功能只支援泛型型別的呼叫,非泛型型別可使用 IsAssignableFrom 方法。");
            }

            List<Type> allOthers = new List<Type> { type };
            if (genericType.IsInterface)
            {
                allOthers.AddRange(type.GetInterfaces());
            }

            foreach (var other in allOthers)
            {
                Type cur = other;
                while (cur != null)
                {
                    if (cur.IsGenericType)
                    {
                        cur = cur.GetGenericTypeDefinition();
                    }
                    if (cur.IsSubclassOf(genericType) || cur == genericType)
                    {
                        return true;
                    }
                    cur = cur.BaseType;
                }
            }
            return false;
        }
View Code

這時候假設我們有 A:訂單模組,B:支付模組, C:收貨地址管理模組,D:(授權)驗證模組 等等,每個模組中都會單獨定義一個繼承自Module這個抽象物件的子類,每個子物件中註冊了各自的模組所需的依賴物件到容器中,這時候我們只需要在 應用層(presentation layer)的 startup.cs中將模組注入即可:

修改 4 中獲取程式及物件的方法 獲取模組(Module)之後依次注入模組:

var moduleObjs = 通過4 中的方法獲取到的程式集物件(Module);
modules = moduleObjs
.Select(m => (Module.Module)Activator.CreateInstance(m))
.OrderBy(m => m.Order);
foreach (var m in modules)
{
services = m.RegisterModule(services);
Console.WriteLine($"模組:【{m.GetType().Name}】注入完成");
}
return services;
View Code

   如果執行專案的效果基本如下:

  

6.最後

  偷懶了,篇幅有點長了,寫多了耗費太多時間了,,