.net core 中的 DependencyInjection - IOC

概要:因为不知道写啥,所以随便找个东西乱说几句,嗯,就这样,就是这个目的。

1.IOC是啥呢?

  IOC - Inversion of Control,即控制反转的意思,这里要搞明白的就是,它是一种思想,一种用于设计的方式(DI)(DI 是手段),(并不是前几天园子中刚出的一片说是原则),OO原则不包含它,再说下,他不是原则!!原则指的是:依赖倒置(Dependency Inversion Principle, DIP)。

  那么,既然是控制反转,怎么反转的?对吧,说到重点了吧。很简单,通过一个容器,将对象注册到这个容器之后,由这个容器来创建对象,从而免去了手动创建对象以及创建后对象(资源)的获取。

  你可能又会问,有什么好处?我直接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.最后

  偷懒了,篇幅有点长了,写多了耗费太多时间了,,

 

posted @ 2018-11-02 14:55  esoftor  阅读(3281)  评论(2编辑  收藏  举报