IOC
基础概念
Microsoft.Extensions.DependencyInjection.Abstractions:抽象包
Microsoft.Extensions.DependencyInjection:实现包
IServiceCollection:用于注册服务(菜谱,记录了每一道菜的制作流程)
ServiceCollection:IServiceCollection接口默认的派生类
ServiceDescriptor:服务描述,(描述某一到菜的制作流程)
IServiceProvider:用于解析服务(厨师,可以通过菜名点菜)
ActivatorUtilities:有些服务我们不想注册到容器,但是这个服务依赖了容器中的服务。此时可以通过ActivatorUtilities来创建。
使用容器不是说解耦合,解耦合还是得通过接口。依赖注入就是基于耦合来依赖注入的,来创建服务的。只不过可以简化这个实列化过程和后续的维护。
依赖:DbContext和IConnection,就是依赖关系,注意不要出现循环依赖。
注册:吧服务添加到IServiceCollection的过程。
注入:容器通过依赖关系,查找IConnection实列,把IConnection实列注入到DbContext的构造器的过程。
手动注入:根据依赖关系手动创建依赖的对象,并且注入到目标服务的过程。
自动注入:根据依赖关系容器通过反射创建依赖的对象,并且注入到目标服务的过程。
public class IConnection
{
}
public class SqlConnection : IConnection
{
}
public class DbContext
{
//DbContext依赖IConnection
public DbContext(IConnection connection)
{
}
}
IConnection connection = new SqlConnectioon();
//注入:手动注入,容器可以自动注入
var context = new DbContext(connection);
服务注册
//创建容器
IServiceCollection services = new ServiceCollection();
//1.通过ServiceDescriptor创建,写框架时有用(万能公式)
services.Add(new ServiceDescriptor(typeof(IConnection),typeof(SqlConnection),ServiceLifetime.Singleton));
//2.泛型方式,此时服务类型为IConnection
services.AddSingleton<IConnection, SqlConnection>();
//3.委托注册,可以定义创建逻辑,在注册服务时也能解析服务
services.AddSingleton(sp =>
{
//sp:是容器实列可以用于解析以注册的服务(IServiceProvider)
var connection = sp.GetService<IConnection>();
return new DbContext(connection,"fff");
//高级
//return ActivatorUtilities.CreateInstance<IConnection>(sp,"fff");
});
//4.服务类型和实现类型相同
services.AddSingleton<SqlConnection>();
//5.泛型注册,这样可以获取到所有Logger<>的完整类型(泛型参数不要写死)
services.AddSingleton(typeof(Logger<>));
//6.反射的方式,写框架很有用
services.AddSingleton(typeof(SqlConnection));
//7.注意使用反射构建泛型参数,此时注册的是Logger<Program>服务(ps:写框架的人会用到)
services.AddSingleton(typeof(Logger<>).MakeGenericType(typeof(Program)));
//8.替换服务,如果服务已注册则不进行注册。一般写框架会用到,如果框架使用了Try...那么你可以使用自定义的服务在它之前进行替换
services.TryAddSingleton(typeof(SqlConnection));
设计模式
设计模式就是解决特定问题的套路,使用设计模式可以方便沟通、理解和相互学习。不要把设计模式学死了。
工厂模式-(侧重对象管理)
1.工厂模式主要用于实现对象的创建,多实例的管理,命名对象的管理,也可以用于管理Provider。和Manager模式的区别是,工厂模式一般不负责执行业务。
2.由于微软的容器只能更加类型来解析服务,有时候我们需要通过名称来解析服务,此时需要使用工厂模式。
3.命名模式的支持,比如ILoggerFactory
public class Connection
{
}
public class ConnectionFactory
{
private IServiceProvider _serviceProvider;
private Dictionary<string, Type> _connections;
public ConnectionFactory(IServiceProvider provider, Dictionary<string, Type> connections)
{
_serviceProvider = provider;
_connections = connections;
}
public Connection? Get(string name)
{
if (_connections.TryGetValue(name, out Type? connectionType))
{
return _serviceProvider.GetService(connectionType) as Connection;
}
return default;
}
}
构造者模式-(侧重对象构建)
1.通过一个构造器(Builder)来提供丰富的api来构造目标对象。简化目标对象的创建,丰富目标对象的创建方式。构造器一般要提供一个Build用来返回被构造的对象的实列。
2.IServiceCollection:就是IServiceProvider的构造者
3.注意区分构造函数
public class Connection
{
}
public class ConnectionFactoryBuilder
{
private Dictionary<string, Connection> _connections = new();
public ConnectionFactoryBuilder Add(string name,Connection connection)
{
_connections.Add(name, connection);
return this;//一般要支持链式调用
}
public ConnectionFactory Build()
{
return new ConnectionFactory(_connections);
}
}
public static class ConnectionFactoryBuilderExtensions
{
public static ConnectionFactoryBuilder Add(this ConnectionFactoryBuilder builder, Connection connection)
{
var name = connection.GetType().Name;
builder.Add(name, connection);
return this;//一般要支持链式调用
}
}
提供者模式-(侧重业务)
1.提供者模式一般支持用户实现,并且支持多实现的。比如日志有控制台提供程序,Debug提供程序,自定义提供程序。
2.提供者更加倾向于业务,一般提供者都是设计成可以支持用户去实现,支持多种扩展的。
3.提供者模式和工厂模式相识,一般我们希望用户可以自定义并且支持多实现的时候使用提供者模式。
4.Provider和工厂模式的区别是,Provider更加倾向于业务逻辑的封装。
public interface IConfigurationProvider
{
string Get(string key);
}
public class JsonConfigurationProvider: IConfigurationProvider
{
}
public class XmlConfigurationProvider : IConfigurationProvider
{
}
管理者模式-(侧重业务管理)
1.用于管理模式,当我们有多个策略需要一个管理者来管理的时候,可以使用Manager模式。管理者模式可以用于管理Provider。
2.管理者模式和工厂模式也很像,但是管理者模式除了管理对象,还负责执行业务。
public class ConfigurationManager
{
private List<IConfigurationProvider> _providers = new ();
public void AddProvider(IConfigurationProvider provider)
{
_providers.Add(provider);
}
public string Get(string key)
{
foreach(var item in _providers)
{
var value = item.Get(key);
if(value != null)
{
return value;
}
}
return default;
}
}
基本使用
IServiceCollection services = new ServiceCollection();
services.AddSingleton<IConnection, SqlConnection>();
services.AddSingleton<IConnection, MySqlConnection>();
services.AddSingleton<AService>();
IServiceProvider container = services.BuildServiceProvider();
var connection = container.GetRequiredService<IConnection>();
var service = container.GetService<AService>();
Console.WriteLine(connection.GetType().Name);
服务解析
//创建容器
IServiceCollection services = new ServiceCollection();
//注册服务
services.AddSingleton<IConnection, SqlConnection>();
//构建容器
IServiceProvider container = services.BuildServiceProvider();
//解析服务
var connection = container.GetService<IConnection>();
//解析服务,如果解析不到会抛出异常
var connection = container.GetRequiredService<IConnection>();
//解析所有IConnection类型的服务
IEnumerable<IConnection> connections = container.GetServices<IConnection>();
//解析一个没有注册到容器但是依赖了容器已注册的服务,写框架常用
ActivatorUtilities.CreateInstance<DbContext>(container);
生命周期
根容器:生命周期与应用程序一致。
子容器:声明周期由开发者决定。
Singleton:同一个容器无论是否是根容器解析出来的实列都是唯一的。
Transient:每次解析都是一个新的实列
Scoped:同一个IServiceScope解析出来的实列是唯一的。
Scoped要点:
1.不要通过根容器来解析Scope实例的服务,因为根容器在程序运行过程中不会释放。那么解析出来的服务也不会释放。
2.Scope的范围有多大不是却决于一次http请求,而是却决于你何时释放。
3.IServiceScope会记录下由它解析出来的服务,如果IServiceScope实列被释放,那么由它解析出来的实列都将被释放。
4.注意虽然根容器和子容器都实现了IServiceProvider接口,但是他们的实现类不一样。
5.单实列的服务不要去依赖一个Scope级别的服务。
搭建测试案例
public class Connection
{
public string Id { get; }
//每次实列化的时候执行一次,得到一个唯一id
public Connection()
{
Id = Guid.NewGuid().ToString();
}
}
var services = new ServiceCollection();
services.AddScoped<Connection>();
var container = services.BuildServiceProvider(new ServiceProviderOptions()
{
ValidateScopes = true,//指示是否可以通过根容器来解析Scope实列。
ValidateOnBuild = true//构建之前时检查是否有依赖没有注册的服务
});
//测试scoped
var connection1 = container.GetRequiredService<Connection>();
Console.WriteLine(connection1.Id);
using(var scope = container.CreateScope())
{
var connection2 = scope.ServiceProvider.GetRequiredService<Connection>();
Console.WriteLine(connection2.Id);
}
组件扫描
组件扫描可以通过接口或者特性的方式。这里我们展示使用特性的方式,因为特性可以配置参数。
//定义一个注解
[AttributeUsage(AttributeTargets.Class)]
public class InjectionAttribute : Attribute
{
public Type? ServiceType { get; set; }
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient;
}
public static class DependencyInjectionExtensions
{
//扫描
public static IServiceCollection AddInjectionServices<T>(this IServiceCollection services)
{
var serviceTypes = GetInjectionServiceTypeList(typeof(T).Assembly);
foreach (var item in serviceTypes)
{
var injection = item.GetCustomAttribute<InjectionAttribute>();
if (injection!.ServiceType == null)
{
services.Add(new ServiceDescriptor(item, item, injection.Lifetime));
}
else
{
services.Add(new ServiceDescriptor(injection!.ServiceType, item, injection.Lifetime));
}
}
return services;
}
private static IEnumerable<Type> GetInjectionServiceTypeList(Assembly assembly)
{
var serviceType = assembly.GetTypes()
.Where(a => a.IsClass)
.Where(a => a.GetCustomAttribute<InjectionAttribute>() != null)
.Where(a => !a.IsAbstract);
return serviceType;
}
}
public interface ILogger<T>
{
void Log();
}
//注入
[Injection(ServiceType = typeof(ILogger<>), Lifetime = ServiceLifetime.Singleton)]
internal class Logger<T>: ILogger<T>
{
public void Log()
{
Console.WriteLine(typeof(T).Name+":success!");
}
}
public static void TestScanner()
{
var services = new ServiceCollection();
//扫描组件
services.AddInjectionServices<Program>();
var container = services.BuildServiceProvider();
var logger = container.GetRequiredService<ILogger<Program>>();
logger.Log();
}
基本原理
自定义IOC需要实现一下两步:
1.编写一个ContailerBuilder,用于注册服务的描述信息,并且能够兼容IServiceCollection注册的服务描述。
2.实现IServiceProvider接口,通过加载ContailerBuilder,并解析服务。
思考为什么是这两步?
1.因为微软的大部分组件都是基于IServiceProvider来进行服务解析的。因此这个接口必须实现。
2.ServiceCollection注册服务的描述信息很简单。你需要更加复杂的容器实现,因此ServiceDescriptor无法描述你的服务类型。因此你需要写一个ContailerBuilder用于记录你的服务的描述信息。
3.需要兼容IServiceCollection注册的服务的描述。比如autofac支持属性注册,但是通过ServiceDescriptor无法描述。因为很多框架的服务注册是基于IServiceCollection,因此你必须能兼容微软的IOC的全部能力。
原理
我们需要一个ContainerBuilder和一个Container类和服务描述类ServiceDescriptor。ContainerBuilder本质是一个集合,用于记录用户注册的服务组件,以及描述信息。
ServiceDescriptor:服务描述信息(服务类型、实列类型,生命周期,创建委托等等),告诉Container将来如何解析实列化服务。
ContainerBuilder:用于记录描述信息的集合,提供api快速便捷的构建容器。
Container:用于解析服务,创建实列。
注册过程:就是创建服务描述的过程,向ContainerBuilder添加服务描述,比如告诉容器这个服务的生命周期,服务类型,实现类型,创建方式,是否支持属性注入,配置实例化时的回调,释放时的回调等等。
构建过程:创建容器的过程,完成容器的一些初始化,并讲服务注册的描述信息传递给容器。
解析过程:一般通过服务的类型,我们可以理解为它是服务的key,通过服务的key找到服务注册的描述信息。
如果是普通注册的服务,那么解析这个实列的时候,找到这个实列的构造器,得到这个实列依赖的其他服务,创建依赖的实列,这是一个递归的过程。
如果是委托注册的服务,那么解析这个实列的时候,调用委托返回实列。
如果是命名注册的服务,那么一般是通过一个工厂模式来解析。
实列化的方式可以参考OOP中的几种实列化方式,反射,表达式树,Emit等技术。
Autofac
autofac提供了更多的功能,比如属性注入,组件扫描等等非常丰富的功能。我也很少使用。微软的IOC容器只支持构造函数的依赖关系注入但是基本够用,如果还要其它需求的可以选择使用autofac。
public static void TestAutofac()
{
var services = new ServiceCollection();
//微软的容器注册服务
services.AddScoped(typeof(ILogger<>),typeof(Logger<>));
var builder = new ContainerBuilder();
//autofac容器注册服务
builder.RegisterType<CService>().PropertiesAutowired()
.As<CService>()
.InstancePerLifetimeScope();
builder.Populate(services);//将IServiceCollection中的服务注册到autofac
//使用AutofacServiceProvider的实现方案,创建容器
//加载autofac中的服务
IServiceProvider container = new AutofacServiceProvider(builder.Build());
var logger = container.GetRequiredService<ILogger<Program>>();
var service = container.GetRequiredService<CService>();
}