文章目录
什么是依赖?
当一个类需要另一个类协作来完成工作的时候就产生了依赖。比如我们在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>();