【C#】依赖注入及Autofac

什么是依赖?

当一个类需要另一个类协作来完成工作的时候就产生了依赖。比如我们在AccountController这个控制器需要完成和用户相关的注册、登录 等事情。其中的登录我们由EF结合Idnetity来完成,所以我们封装了一个EFLoginService。这里AccountController就有一个ILoginService的依赖。

什么是注入?

依赖注入(Dependency injection,DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术。

注入体现的是一个IOC(控制反转的的思想),把依赖的创建丢给其它人,自己只负责使用,其它人丢给你依赖的这个过程理解为注入。

        private readonly IAccountAppService _accountAppService;
        /// <summary>
        /// 构造
        /// </summary>
        /// <param name="accountAppService">账户中心接口</param>
        public AccountController(
            IAccountAppService accountAppService
            )
        {
    
    
            _accountAppService = accountAppService;
        }

为什么要反转?

为了在业务变化的时候尽少改动代码可能造成的问题。
比如我们现在要把从EF中去验证登录改为从Redis去读,于是我们加了一个 RedisLoginService。这个时候我们只需要在原来注入的地方改一下就可以了。

// 用Redis来替换原来的EF登录 var controller = new AccountController(new RedisLoginService()); controller.Login(userName, password);

何为容器

上面我们在使用AccountController的时候,我们自己通过代码创建了一个IAccountServce的实例。想象一下,一个系统中如果有100个这样的地方,我们是不是要在100个地方做这样的事情? 控制是反转了,依赖的创建也移交到了外部。现在的问题是依赖太多,我们需要一个地方统一管理系统中所有的依赖,容器诞生了。
容器负责两件事情:

  • 绑定服务与实例之间的关系
  • 获取实例,并对实例进行管理(创建与销毁)

默认容器

ASP.NET Core 的底层设计支持和使用依赖注入。ASP.NET Core
应用程序可以利用内置的框架服务将它们注入到启动类Startup的方法中,并且应用程序服务ConfigureServices能够配置注入。

在内置的IoC容器中,在Startup.cs类文件ConfigureServices方法中,注入依赖方式如下:
public void ConfigureServices(IServiceCollection services)

{
    
    
    services.AddControllers();
    services.AddTransient<ITransientService, TransientService>();
    services.AddSingleton<ISingletonService, SingletonService>();
    services.AddScoped<IScopedService, ScopedService>();
    services.AddScoped<ITestService, TestService>();
}

Autofac框架

AutoFac是一个开源的轻量级的依赖注入容器,也是.net下比较流行的实现依赖注入的工具之一。

将Autofac整合到你的应用的基本流程如下:

  • 按照 控制反转 (IoC) 的思想构建你的应用.
  • 添加Autofac引用.
  • 在应用的 startup 处
  • 创建 ContainerBuilder.
  • 注册组件.
  • 创建容器,将其保存以备后续使用.
  • 应用执行阶段
  • 从容器中创建一个生命周期.
  • 在此生命周期作用域内解析组件实例.

框架使用

1、NuGet引入包

Install-Package Autofac -Version 4.8.1 Install-Package
Autofac.Extensions.DependencyInjection -Version 4.3.0

2、在ConfigureServices中创建容器对象,并返回IServiceProvider

一些需用到的方法:创建Runtime帮助获取项目程序集,排除所有的系统程序集(Microsoft.***、System.***等)、Nuget下载包

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
 
namespace Dianshi.P2P.Core.Client.WebApi.Auth
{
    
    
    [ApiExplorerSettings(IgnoreApi = true)]
    public class RuntimeHelper
    {
    
    
        /// <summary>
        /// 获取项目程序集,排除所有的系统程序集(Microsoft.***、System.***等)、Nuget下载包
        /// </summary>
        /// <returns></returns>
        public static IList<Assembly> GetAllAssemblies()
        {
    
    
            List<Assembly> list = new List<Assembly>();
            var deps = DependencyContext.Default;
            //排除所有的系统程序集、Nuget下载包
            var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package");
            foreach (var lib in libs)
            {
    
    
                try
                {
    
    
                    var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name));
                    list.Add(assembly);
                }
                catch (Exception ex)
                {
    
    
                    //
                }
            }
            return list;
        }
 
        public static Assembly GetAssembly(string assemblyName)
        {
    
    
            return GetAllAssemblies().FirstOrDefault(f => f.FullName.Contains(assemblyName));
        }
 
        public static IList<Type> GetAllTypes()
        {
    
    
            List<Type> list = new List<Type>();
            foreach (var assembly in GetAllAssemblies())
            {
    
    
                var typeinfos = assembly.DefinedTypes;
                foreach (var typeinfo in typeinfos)
                {
    
    
                    list.Add(typeinfo.AsType());
                }
            }
            return list;
        }
 
        /// <summary>
        /// 根据AssemblyName获取所有的类
        /// </summary>
        /// <param name="assemblyName"></param>
        /// <returns></returns>
        public static IList<Type> GetTypesByAssembly(string assemblyName)
        {
    
    
            List<Type> list = new List<Type>();
            var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(assemblyName));
            var typeinfos = assembly.DefinedTypes;
            foreach (var typeinfo in typeinfos)
            {
    
    
                list.Add(typeinfo.AsType());
            }
            return list;
        }
 
        public static Type GetImplementType(string typeName, Type baseInterfaceType)
        {
    
    
            return GetAllTypes().FirstOrDefault(t =>
            {
    
    
                if (t.Name == typeName && t.GetTypeInfo().GetInterfaces().Any(b => b.Name == baseInterfaceType.Name))
                {
    
    
                    var typeinfo = t.GetTypeInfo();
                    return typeinfo.IsClass && !typeinfo.IsAbstract && !typeinfo.IsGenericType;
                }
                return false;
            });
        }
    }
}

然后

 #region 依赖注入
            var builder = new ContainerBuilder();//实例化容器
            //注册所有模块module
            builder.RegisterAssemblyModules(Assembly.GetExecutingAssembly());
            //获取所有的程序集
            //var assemblys = BuildManager.GetReferencedAssemblies().Cast<Assembly>().ToArray();
            var assemblys = RuntimeHelper.GetAllAssemblies().ToArray();
 
            //注册所有继承IDependency接口的类
            //builder.RegisterAssemblyTypes().Where(type => typeof(IDependency).IsAssignableFrom(type) && !type.IsAbstract);
            //注册仓储,所有IRepository接口到Repository的映射
            builder.RegisterAssemblyTypes(assemblys).Where(t => t.Name.EndsWith("Repository") && !t.Name.StartsWith("I")).AsImplementedInterfaces();
            //注册服务,所有IApplicationService到ApplicationService的映射
            builder.RegisterAssemblyTypes(assemblys).Where(t => t.Name.EndsWith("AppService") && !t.Name.StartsWith("I")).AsImplementedInterfaces();
            builder.Populate(services);
            this.ApplicationContainer = builder.Build();
 
            return new AutofacServiceProvider(this.ApplicationContainer); //第三方IOC接管 core内置DI容器 
            //return services.BuilderInterceptableServiceProvider(builder => builder.SetDynamicProxyFactory());
            #endregion

ConfigureServices的返回值(默认为void)为IServiceProvider

测试

 public ActionResult<string> Ioc()
 {
    
    
 	//_accountAppService定义在《什么是注入?》章节下
     return _accountAppService.Login(new Application.Members.Accounts.Dto.LoginInput() {
    
     Password = "1", Phone = "2" });
 }

Autofac注册组件

通过创建 ContainerBuilder 来注册组件,并且告诉容器哪些组件,暴露了哪些服务。

使用 Register() 方法来注册实现:

ContainerBuilder 包含一组 Register() 注册方法,而组件暴露服务,可用使用 ContainerBuilder 上的 As() 方法。

即在容器初始化时候,向容器组件添加对象的操作过程。

通过梳理Autofac所有可用的注册组件方法,显示如下图展示的流程图。
在这里插入图片描述

反射注册

直接注册的组件必须是具体的类型,并可用暴露抽象和接口作为服务,但不能注册一个抽象和接口组件。

使用RegisterType()或者RegisterType(typeof(T))方法:

builder.RegisterType<TestService>().As<ITestService>();
// 或者
builder.RegisterType(typeof(TestService)).As(typeof(ITestService))

在多个构造函数时,如果需要,也可手动指定一个构造函数。

使用 UsingConstructor 方法和构造方法中代表参数类型的类型。

builder.RegisterType<TestService>()
       .UsingConstructor(typeof(TransientService), typeof(SingletonService));

实例注册

提前生成对象的实例并加入容器,以供注册组件时使用。

使用RegisterInstance()方法

// new出一个对象注册:
var output = new StringWriter();
builder.RegisterInstance(output).As<TestService>();

如果单例中存在实例且需要在容器中被组件使用时,

builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();

Lambda表达式注册

当组件创建不再是简单调用构造方法时,可用利用lambda表达式来实现一些常规反射无法实现的操作。

比如一些复杂参数注册,参数注入,以及选择参数值实现等。

builder.Register(x => new TransientService()).As<ITransientService>();
// 或者指定参数
  builder.Register(x => new TestService(x.Resolve<ITransientService>(), x.Resolve<IScopedService>(), x.Resolve<ISingletonService>()))
  .As<ITestService>().InstancePerLifetimeScope();

泛型注册

支持泛型注册操作,使用 RegisterGeneric() 方法:

builder.RegisterGeneric(typeof(NHibernateRepository<>))
       .As(typeof(IRepository<>))
       .InstancePerLifetimeScope();

条件注册

在一些特殊场景,可能需要通过加上判断条件,来决定是否执行该条注册语句。

两种方法:

1、OnlyIf() - 提供一个表达式, 表示只有满足条件,才会执行语句。

builder.RegisterType<Manager>()
       .As<IManager>()
       .OnlyIf(reg =>
         reg.IsRegistered(new TypedService(typeof(IService))) &&
         reg.IsRegistered(new TypedService(typeof(HandlerB))));

2、IfNotRegistered() - 表示没有其他服务注册的情况下,就执行语句。
方法在 ContainerBuilder.Build() 时执行并且以实际组件注册的顺序执行。

builder.RegisterType<ServiceA>()
       .As<IService>();
builder.RegisterType<ServiceB>()
       .As<IService>()
       .IfNotRegistered(typeof(IService));

属性注入

构造方法参数注入是一种传值给组件的首选的方法。

在构造函数中是直接使用服务类型作为参数,然后AutoFac解析该类时,就会去容器内部已存在的组件中查找,然后将匹配的对象注入到构造函数中去。

但你同样也可以使用属性方法注入来传值。

是将容器内对应的组件直接注入到类内的属性中去,在注册该属性所属类的时候,需要使用PropertiesAutowired()方法额外标注。

这里不讨论属性注入的好坏,也不做说明服务层属性怎么注入,只讨论说明控制器中属性如何实现注入

1、注册组件方法,并使用属性注入PropertiesAutowired()标注。

builder.RegisterType<TransientService>().As<ITransientService>().PropertiesAutowired();

2、在控制器中使用属性来接收, 其中注入属性必须标注为public

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    
    

    public ITransientService _transientService {
    
     get; set; }

    [HttpGet]
    public JsonResult Get()
    {
    
    
        var data1 =   _transientService.GetGuid();
        return new JsonResult(new {
    
    
            data1
        });
    }
}

程序集注册

当我们需要实现批量注册的时候,也可以使用程序集的方式来注册,这也是常用的方法。

可通过指定过滤类型,服务,扫描模块等方式来找到需要注册的组件。

var assemblies = Assembly.GetExecutingAssembly();

builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类
.Where(c => c.Name.EndsWith("Service"))
.PublicOnly()//只要public访问权限的
.Where(cc => cc.IsClass)//只要class型(主要为了排除值和interface类型)
.AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)

说明:

  • RegisterAssemblyTypes() :接收包含一个或多个程序集的数组作为参数
  • RegisterAssemblyModules() : 接收模块作为参数,进行模块扫描注册
  • PublicOnly() :指定公有方法被注册
  • Where() :要过滤注册的类型
  • Except() :要排除的类型
  • As() :反射出其实现的接口
  • AsImplementedInterfaces() : 自动以其实现的所有接口类型暴露(包括IDisposable接口)

Autofac暴露服务

上面提到了注册组件时, 我们得告诉Autofac, 组件暴露了哪些服务。

在上面注册实现中,大部分使用到了As() 方法。

当然,Autofac也提供了其他标注来暴露服务的方法。

默认暴露自身类型服务

常用的几种方法如下:

builder.RegisterType<CallLogger>();//不标注,默认以自身类型暴露服务
builder.RegisterType<CallLogger>().AsSelf();
builder.RegisterType<CallLogger>().As<CallLogger>();
builder.RegisterType<CallLogger>().As(typeof(CallLogger));

多个暴露服务类型

以其实现的接口(interface)暴露服务,暴露的类型可以是多个,比如CallLogger类实现了ILogger接口和ICallInterceptor接口。

暴露服务后, 可以解析基于该服务的组件了. 但请注意, 一旦将组件暴露为一个特定的服务, 默认的服务 (组件类型) 将被覆盖。

所以,为了防止被其他服务覆盖,可以使用 AsSelf() 方法。

Autofac解析服务

Copybuilder.RegisterType<CallLogger>()
       .As<ILogger>()
       .As<ICallInterceptor>()
       .AsSelf();

这样你既可以实现组件暴露一系列特定的服务, 又可以让它暴露默认的服务。

程序集注册指定暴露类型

1、可通过指定接口类型暴露服务,使用As() 方法

publi void ConfigureContainer(ContainerBuilder builder)
{
    
    
    builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类
        .Where(cc =>cc.Name.EndsWith("Repository")|//筛选
               cc.Name.EndsWith("Service"))
        .As(x=>x.GetInterfaces()[0])//反射出其实现的接口,并指定以其实现的第一个接口类型暴露
}

2、指定所有实现的接口类型进行暴露

使用AsImplementedInterfaces()函数实现,相当于一个类实现了几个接口(interface)就会暴露出几个服务,等价于上面连写多个As()的作用。

publi void ConfigureContainer(ContainerBuilder builder)
{
    
    
builder.RegisterAssemblyTypes(asm)
       .Where(t => t.Name.EndsWith("Repository"))
       .AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)
}

解析服务

在注册完组件并暴露相应的服务后, 可以从创建的容器或其生命周期中解析服务。

使用 Resolve() 方法来解析实现:

通过梳理Autofac所有可用的解析服务方法,显示如下图展示的流程图。
在这里插入图片描述
在 注册完组件并暴露相应的服务后, 你可以从创建的容器或其子 生命周期 中解析服务. 让我们使用 Resolve() 方法来实现:

var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>().As<IService>();
var container = builder.Build();

using(var scope = container.BeginLifetimeScope())
{
    
    
  var service = scope.Resolve<IService>();
}

解析时传参

当解析服务时, 需要传参,可以使用Resolve() 方法来接受可变长度的参数。

  • 可用参数类型

NamedParameter - 通过名称匹配目标参数

TypedParameter - 通过类型匹配目标参数 (需要匹配具体类型)

ResolvedParameter - 灵活的参数匹配

  • 反射组件的参数
var reader = scope.Resolve<ConfigReader>(new NamedParameter("configSectionName", "sectionName"));
  • Lambda表达式组件的参数
  • 不显式调用Resolve传参

Autofac生命周期

在这里插入图片描述
下面讲下AutoFac定义的几种生命周期作用域,并与.NET Core默认的生命周期作了简要的对比。

暂时性

每次在向服务容器进行请求时都会创建新的实例,相当于每次都new出一个。

注册方式:

使用InstancePerDependency()方法标注,如果不标注,这也是默认的选项。以下两种注册方法是等效的:

//不指定,默认就是瞬时的
builder.RegisterType<TransientService>().As<ITransientService>();

//指定其生命周期域为瞬时
builder.RegisterType<TransientService>().As<ITransientService>().InstancePerDependency();

对比:

与默认的容器中自带的生命周期AddTransient相同,也是每次都是全新的实例。
使用AddTransient()注册:

services.AddTransient<ITransientService, TransientService>()

作用域内

在每次Web请求时被创建一次实例,生命周期横贯整次请求。即在每个生命周期作用域内是单例的。

注册方式:

使用InstancePerLifetimeScope()方法标识:

builder.RegisterType<ScopedService>().As<IScopedService>().InstancePerLifetimeScope();

对比:

与默认的容器中自带的生命周期AddScoped相同,.NET Core框架自带的容器全权接管了请求和生命周期作用域的创建,使用Scoped()可以实现相同的效果。
使用AddScoped()注册:

services.AddScoped<IScopedService, ScopedService>();

匹配作用域内

即每个匹配的生命周期作用域一个实例。
该类型其实是上面的“作用域内”的其中一种,可以对实例的共享有更加精准的控制.。我们通过允许给域“打标签”,只要在这个特定的标签域内就是单例的。

  • 注册

使用InstancePerMatchingLifetimeScope(string tagName)方法注册:

var builder = new ContainerBuilder();
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("myrequest");

当你开始一个生命周期时, 提供的标签值和它就关联起来了。

  • 解析
// myrequest标签子域一
using(var scope1 = container.BeginLifetimeScope("myrequest"))
{
    
    
  for(var i = 0; i < 100; i++)
  {
    
    
    var w1 = scope1.Resolve<Worker>();
    using(var scope2 = scope1.BeginLifetimeScope())
    {
    
    
      var w2 = scope2.Resolve<Worker>();

      //解析了2次,但2次都是同一个实例(w1和w2指向同一个内存块Ⅰ)
    }
  }
}

// //myrequest标签子域二
using(var scope3 = container.BeginLifetimeScope("myrequest"))
{
    
    
  for(var i = 0; i < 100; i++)
  {
    
    
   //因为标签域内已注册过,所以可以解析成功
    var w3 = scope3.Resolve<Worker>();
    using(var scope4 = scope3.BeginLifetimeScope())
    {
    
    
      var w4 = scope4.Resolve<Worker>();
	}
  }
}

对比:

services.AddSingleton<IProductService, ProductService>();

来源

.NET Core API框架实战(五) 依赖注入 服务的注册与提供

Autofac 框架初识与应用

猜你喜欢

转载自blog.csdn.net/weixin_44231544/article/details/125561984