IOC 視頻講解 基礎概念 Microsoft.Extensions.DependencyInjection.Abstractions:抽象包 Microsoft.Extensions.DependencyInjection:實現包 IServiceCollection:用於註冊服務(菜譜,記錄了每 ...
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>();
}