1. 程式人生 > >.Net Core 中的 DI使用 - IOC原則

.Net Core 中的 DI使用 - IOC原則

fig rip assign 經典 快速 argument 好處 如圖所示 web api

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

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.最後

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

.Net Core 中的 DI使用 - IOC原則