嘿嘿! 如何理解IOC呢?我们可以通过一个现实世界的模型来进行解释。比如有一本菜谱这个菜谱就是我们的IServiceCollection,里面记录了菜(Service)的描述信息(ServiceDescriptor)菜名(ServiceDescriptor.ServiceType)以及菜具体制作方法(ServiceDescriptor.ImplementationType),通过菜名(ServiceType)告诉厨师(IServiceProvider)制作(实列化、解析)出来我们要吃的菜。这就是IOC技术。
依赖项
Microsoft.Extensions.DependencyInjection.Abstractions:抽象包,用于扩展容器
Microsoft.Extensions.DependencyInjection:实现包,实现IOC的基本功能
核心接口 Service:就是我们需要的服务实列(菜)
ServiceDescriptor:用于描述服务的信息。比如服务名(ServiceType)、实现类(ImplementationType)、生命周期(Lifetime)。(某道菜的制作描述信息)
public interface IServiceCollection : ICollection
IServiceProvider:用于解析服务实列,根容器和子容器实现类不同(厨师)实现类里面有字段用于标记是否是根容器,以及记录所有解析的实列,为将来释放做准备。
ActivatorUtilities:用于解析一个容器中不存在,但是依赖了容器中的服务的实列。
关键字
依赖:如果一个类A的构造器中有一个类B的参数,我们说A依赖B
注入:如果A依赖B,要想实列化A,就必须先实列化B,然后把B载入A的构造器的过程
依赖注入:IOC容器根据反射得到一个类的依赖关系,自动帮你载入依赖项的过程(注意循环依赖问题)
如何避免循环依赖
循环依赖是指两个或多个模块之间相互依赖,形成了闭环。这种情况下,编译器或运行时环境无法确定模块的加载顺序,会导致编译错误或运行时错误。为了避免循环依赖,可以尝试以下方法:
1. 重新设计架构:重新审视系统的整体设计,尽量避免出现循环依赖的情况。将功能模块划分得更加清晰,降低模块之间的耦合性。
2. 使用依赖注入:通过依赖注入来解决循环依赖问题。将模块的依赖关系交给外部容器管理,并通过构造函数、属性注入等方式将依赖项注入到模块中,而不是在模块内部直接引用依赖的模块。
public class ClassA
{
public ClassB ClassBInstance { get; set; }
public void MethodA()
{
Console.WriteLine("MethodA in ClassA is called.");
ClassBInstance.MethodB();
}
}
public class ClassB
{
public ClassA ClassAInstance { get; set; }
public void MethodB()
{
Console.WriteLine("MethodB in ClassB is called.");
ClassAInstance.MethodA();
}
}
3. 抽象接口或基类:通过引入抽象接口或基类来解耦模块之间的直接依赖关系。模块之间通过依赖于接口或基类,而不是直接依赖于具体实现类,降低了循环依赖的可能性。
// Interface IA
public interface IA {
void setB(B b);
}
// Interface IB
public interface IB {
void setA(A a);
}
// Class A
public class A implements IA {
private B b;
@Override
public void setB(B b) {
this.b = b;
}
}
// Class B
public class B implements IB {
private A a;
@Override
public void setA(A a) {
this.a = a;
}
}
4. 重新组织模块结构:如果存在循环依赖,可以考虑重新组织模块的结构,将相互依赖的功能提取到独立的模块中,形成单向依赖关系。
5. 使用延迟加载:将模块的加载推迟到真正需要使用时再进行,可以避免在模块加载阶段出现循环依赖的问题。
6. 使用中间件或事件驱动:将模块之间的通信和交互通过中间件或事件驱动的方式进行,避免直接的循环依赖。
7. 使用工厂模式或服务定位器:通过引入工厂模式或服务定位器来解决循环依赖问题。通过工厂或定位器来获取需要的模块实例,而不是直接引用。
以上是一些常见的方法来避免循环依赖问题,在实际项目中可以根据具体情况选择合适的策略。同时,保持良好的设计原则和架构规范,以及使用静态代码分析工具等方法也可以帮助发现并解决潜在的循环依赖问题。
服务描述
public class ServiceDescriptor
{
//服务类型,解析时通过服务类型查找
public Type ServiceType { get; }
//实现类型必须是具体类,不能是抽象类或者接口(必须实现或者继承ServiceType)
public Type? ImplementationType { get; }
//描述生命周期
public ServiceLifetime Lifetime { get; }
//用于保存工厂
public Func
//用于保存单实例
public object? ImplementationInstance { get; }
}
1.万能方法
//需要安装:Microsoft.Extensions.DependencyInjection
//创建IServiceCollection实列
IServiceCollection services = new ServiceCollection();
//由于IServiceCollection实现了IList
//因此下面是一个万能公式,其它的都是扩展方法,本质调用的还是这个万能公式,包括委托的方式(他的实现类型是一个委托)
services.Add(new ServiceDescriptor(typeof(IConnection),typeof(SqlDbConnection),ServiceLifetime.Singleton));
2.泛型接口
//泛型接口需要提前知道类型
services.AddSingleton
3.反射接口
//反射的方式在编写框架时十分有用,无反射无框架
services.AddSingleton(typeof(IDbConnection), typeof(SqlDbConnection));
4.委托方式
//当我们构建的对象需要编写逻辑时,委托方式十分有用
services.AddSingleton
//低级用法
//假设DbContext依赖IDbConnection,并且需要一个name
//sp是一个IServiceProvider的实列
//委托方式在注册的同时还能进行预解析
//sp到底是根容器还是子容器由解析时的IServiceProvider
services.AddSingleton(sp =>
{
var connection = sp.GetRequiredService
return new DbContext(connection, "c1");
});
//高级用法
services.AddSingleton(sp =>
{
return ActivatorUtilities.CreateInstance
});
5.泛型注册
//注册泛型时,只能使用反射接口,并且泛型参数不要写入,解析时来确立,如果有多个泛型参数使用逗号隔开
services.AddSingleton(typeof(ILogger<>), typeof(ConsoleLogger<>));
6.尝试注册
//如果IDbConnection已注册则后续的services.TryAddSingleton(typeof(IDbConnection), typeof(SqlDbConnection));不会注册新的实现
services.TryAddSingleton(typeof(IDbConnection), typeof(SqlDbConnection));
7.默认注册
IServiceCollection services = new ServiceCollection();
var sp = services.BuildServiceProvider();
var sp1 = sp.GetRequiredService
IServiceProvider container = services.BuildServiceProvider(new ServiceProviderOptions
{
ValidateOnBuild = true,//构建时检查是否有依赖没有注册的服务
ValidateScopes = true,//在解析服务时检查是否通过根容器来解析Scoped类型的实列
});
//如果同一个服务类型,注册多个实现,那么默认获取最后一个实现。
services.AddSingleton
services.AddSingleton
IServiceProvider container = services.BuildServiceProvider();
//如果服务未注册,返回null
IDbConnection? connection = container.GetService
//服务不存在讲引发异常
IDbConnection connection = container.GetRequiredService
//获取IDbConnection所有实现
IEnumerable
//假设DbContext依赖IDbConnection,并且需要一个name,但是容器没有注册DbContext
var context = ActivatorUtilities.CreateInstance
容器除了会帮我们创建对象,还负责对象的销毁,特别对于托管资源。对于实例化过程部深入解析
不要试图通过根容器来解析Scoped或者Transient生命周期的实列
单实例的对象不能依赖一个Scoped或者Transient生命周期的实列
在Debug模式下可以看到容器是否是根容器,以及容器解析的实列,容器会记录由它解析的所有实列,为释放做准备。
组件扫描可以自定义规则,比如根据实现了某个接口,或者统一后缀
这里我们演示如何通过注解来扫描,大家也可以根据接口的方式来扫描
[AttributeUsage(AttributeTargets.Class)]
public class InjectionAttribute : Attribute
{
public Type? ServiceType { get; set; }
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient;
}
public static class InjectionIServiceCollectionExtensions
{
public static IServiceCollection AddServicesByInjection
{
var serviceTypes = typeof(T).Assembly.GetTypes()
.Where(a => a.IsClass)
.Where(a => a.GetCustomAttribute
.Where(a => !a.IsAbstract);
foreach (var item in serviceTypes)
{
var injection = item.GetCustomAttribute
if (injection!.ServiceType == null)
{
services.Add(new ServiceDescriptor(item, item, injection.Lifetime));
}
else
{
services.Add(new ServiceDescriptor(injection!.ServiceType, item, injection.Lifetime));
}
}
return services;
}
}
public interface IDbConnection
{
}
[Injection(ServiceType = typeof(IDbConnection), Lifetime = ServiceLifetime.Scoped)]
public class DbConnection : IDbConnection
{
}