在之前有些過一篇文章 "《使用 DryIoc 替換 Abp 的 DI 框架》" ,在該文章裡面我嘗試通過以替換 內部的 來實現使用我們自己的 DI 框架。替換了之後我們基本上是可以正常使用了,不過仍然還存在有以下兩個比較顯著的問題。 1. 攔截器功能無法正常使用,需要重覆遞歸查找真實類型,消耗性能。 ...
在之前有些過一篇文章 《使用 DryIoc 替換 Abp 的 DI 框架》 ,在該文章裡面我嘗試通過以替換 IocManager
內部的 IContainer
來實現使用我們自己的 DI 框架。替換了之後我們基本上是可以正常使用了,不過仍然還存在有以下兩個比較顯著的問題。
- 攔截器功能無法正常使用,需要重覆遞歸查找真實類型,消耗性能。
- 針對於通過
IServiceCollection.AddScoped()
方法添加的 Scoped 類型的解析存在問題。
下麵我們就來針對於上述問題進行問題的分析與解決。
1. 問題 1
1.1 現象與原因
首先,來看一下問題 1 ,針對於問題 1 我在 Github 上面向作者請教了一下,造成嵌套註冊的原因很簡單。因為之所以我們解析的時候,原來的註冊類型會解析出來代理類。
關於上述原因可以參考 DryIoc 的 Github 問題 #50 。
這是因為 DryIoc 是通過替換了原有註冊類型的實現,而如果按照之前我們那篇文章的方法,每次註冊事件被觸發的時候就會針對註冊類型嵌套一層代理類。這樣如果某個類型有多個攔截器,這樣就會造成一個類型嵌套的問題,在外層的攔截器被攔截到的時候無法獲取到當前代理的真實類型。
1.2 思路與解決方法
解決思路也比較簡單,就是我們在註冊某個類型的時候,觸發了攔截器註入事件。在這個時候,我們並不真正的執行代理類的一個操作。而是將需要代理的類型與它的攔截器類型通過字典存儲起來,然後在類型完全註冊完成之後,通過遍歷這個字典,我們來一次性地為每一個註冊類型進行攔截器代理。
思路清晰了,那麼我們就可以編寫代碼來進行實現了,首先我們先為 IocManager 增加一個內部的字典,用於存儲註冊類-攔截器。
public class IocManager : IIocManager
{
// ... 其他代碼
private readonly List<IConventionalDependencyRegistrar> _conventionalRegistrars;
private readonly ConcurrentDictionary<Type, List<Type>> _waitRegisterInterceptor;
// ... 其他代碼
public IocManager()
{
_conventionalRegistrars = new List<IConventionalDependencyRegistrar>();
_waitRegisterInterceptor = new ConcurrentDictionary<Type, List<Type>>();
}
// ... 其他代碼
}
之後我們需要開放兩個方法用於為指定的註冊類型添加對應的攔截器,而不是在類型註冊事件被觸發的時候直接生成代理類。
public interface IIocRegistrar
{
// ... 其他代碼
/// <summary>
/// 為指定的類型添加攔截器
/// </summary>
/// <typeparam name="TService">註冊類型</typeparam>
/// <typeparam name="TInterceptor">攔截器類型</typeparam>
void AddInterceptor<TService, TInterceptor>() where TInterceptor : IInterceptor;
/// <summary>
/// 為指定的類型添加攔截器
/// </summary>
/// <param name="serviceType">註冊類型</param>
/// <param name="interceptor">攔截器類型</param>
void AddInterceptor(Type serviceType,Type interceptor);
// ... 其他代碼
}
public class IocManager : IIocManager
{
// ... 其他代碼
/// <inheritdoc />
public void AddInterceptor<TService, TInterceptor>() where TInterceptor : IInterceptor
{
AddInterceptor(typeof(TService),typeof(TInterceptor));
}
/// <inheritdoc />
public void AddInterceptor(Type serviceType, Type interceptorType)
{
if (_waitRegisterInterceptor.ContainsKey(serviceType))
{
var interceptors = _waitRegisterInterceptor[serviceType];
if (interceptors.Contains(interceptorType)) return;
_waitRegisterInterceptor[serviceType].Add(interceptorType);
}
else
{
_waitRegisterInterceptor.TryAdd(serviceType, new List<Type> {interceptorType});
}
}
// ... 其他代碼
}
然後針對所有攔截器的監聽事件進行替換,例如工作單元攔截器:
internal static class UnitOfWorkRegistrar
{
/// <summary>
/// 註冊器初始化方法
/// </summary>
/// <param name="iocManager">IOC 管理器</param>
public static void Initialize(IIocManager iocManager)
{
// 事件監聽處理
iocManager.RegisterTypeEventHandler += (manager, type, implementationType) =>
{
HandleTypesWithUnitOfWorkAttribute(iocManager,type,implementationType.GetTypeInfo());
HandleConventionalUnitOfWorkTypes(iocManager,type, implementationType.GetTypeInfo());
};
// 校驗當前註冊類型是否帶有 UnitOfWork 特性,如果有則註入攔截器
private static void HandleTypesWithUnitOfWorkAttribute(IIocManager iocManager,Type serviceType,TypeInfo implementationType)
{
if (IsUnitOfWorkType(implementationType) || AnyMethodHasUnitOfWork(implementationType))
{
// 添加攔截器
iocManager.AddInterceptor(serviceType,typeof(UnitOfWorkInterceptor));
}
}
// 處理特定類型的工作單元攔截器
private static void HandleConventionalUnitOfWorkTypes(IIocManager iocManager,Type serviceType,TypeInfo implementationType)
{
// ... 其他代碼
if (uowOptions.IsConventionalUowClass(implementationType.AsType()))
{
// 添加攔截器
iocManager.AddInterceptor(serviceType,typeof(UnitOfWorkInterceptor));
}
}
// ... 其他代碼
}
}
處理完成之後,我們需要在 RegisterAssemblyByConvention()
方法的內部真正地執行攔截器與代理類的生成工作,邏輯很簡單,遍歷之前的 _waitRegisterInterceptor 字典,依次使用 ProxyUtils 與 DryIoc 進行代理類的生成與綁定。
public class IocManager : IIocManager
{
// ... 其他代碼
/// <summary>
/// 使用已經存在的規約註冊器來註冊整個程式集內的所有類型。
/// </summary>
/// <param name="assembly">等待註冊的程式集</param>
/// <param name="config">附加的配置項參數</param>
public void RegisterAssemblyByConvention(Assembly assembly, ConventionalRegistrationConfig config)
{
var context = new ConventionalRegistrationContext(assembly, this, config);
foreach (var registerer in _conventionalRegistrars)
{
registerer.RegisterAssembly(context);
}
if (config.InstallInstallers)
{
this.Install(assembly);
}
// 這裡使用 TPL 並行庫的原因是因為存在大量倉儲類型與應用服務需要註冊,應最大限度利用 CPU 來進行操作
Parallel.ForEach(_waitRegisterInterceptor, keyValue =>
{
var proxyBuilder = new DefaultProxyBuilder();
Type proxyType;
if (keyValue.Key.IsInterface)
proxyType = proxyBuilder.CreateInterfaceProxyTypeWithTargetInterface(keyValue.Key, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
else if (keyValue.Key.IsClass())
proxyType = proxyBuilder.CreateClassProxyTypeWithTarget(keyValue.Key,ArrayTools.Empty<Type>(),ProxyGenerationOptions.Default);
else
throw new ArgumentException($"類型 {keyValue.Value} 不支持進行攔截器服務集成。");
var decoratorSetup = Setup.DecoratorWith(useDecorateeReuse: true);
// 使用 ProxyBuilder 創建好的代理類替換原有類型的實現
IocContainer.Register(keyValue.Key,proxyType,
made: Made.Of(type=>type.GetConstructors().SingleOrDefault(c=>c.GetParameters().Length != 0),
Parameters.Of.Type<IInterceptor[]>(request =>
{
var objects = new List<object>();
foreach (var interceptor in keyValue.Value)
{
objects.Add(request.Container.Resolve(interceptor));
}
return objects.Cast<IInterceptor>().ToArray();
}),
PropertiesAndFields.Auto),
setup: decoratorSetup);
});
_waitRegisterInterceptor.Clear();
}
// ... 其他代碼
}
這樣的話,在調用控制器或者應用服務方法的時候能夠正確的獲取到真實的代理類型。
圖:
可以看到攔截器不像原來那樣是多個層級的情況,而是直接註入到代理類當中。
通過 invocation
參數,我們也可以直接獲取到被代理對象的真實類型。
2. 問題 2
2.1 現象與原因
問題 2 則是由於 DryIoc 的 Adapter 針對於 Scoped 生命周期對象的處理不同而引起的,比較典型的情況就是在 Startup
類當中使用 IServiceCollection.AddDbContxt<TDbContext>()
方法註入了一個 DbContext 類型,因為其方法內部預設是使用 ServiceLifeTime.Scoped
周期來進行註入的。
public static IServiceCollection AddDbContext<TContextService, TContextImplementation>(
[NotNull] this IServiceCollection serviceCollection,
[CanBeNull] Action<DbContextOptionsBuilder> optionsAction = null,
ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
where TContextImplementation : DbContext, TContextService
=> AddDbContext<TContextService, TContextImplementation>(
serviceCollection,
optionsAction == null
? (Action<IServiceProvider, DbContextOptionsBuilder>)null
: (p, b) => optionsAction.Invoke(b), contextLifetime, optionsLifetime);
按照正常的邏輯,一個 Scoped 對象的生命周期應該是與一個請求一致的,當請求結束之後該對象被釋放,而且在該請求的生命周期範圍內,通過 Ioc 容器解析出來的 Scoped 對象應該是同一個。如果有新的請求,則會創建一個新的 Scoped 對象。
但是使用 DryIoc 替換了原有 Abp 容器之後,現在如果在一個控制器方法當中解析一個 Scoped 周期的對象,不論是幾次請求獲得的都是同一個對象。因為這種現象的存在,在 Abp 的 UnitOfWorkBase
當中完成一次資料庫查詢操作之後,會調用 DbContext
的 Dispose()
方法釋放掉 DbContext
。這樣的話,在第二次請求因為獲取的是同一個 DbContext,這樣的話就會拋出對象已經被關閉的異常信息。
除了開發人員自己註入的 Scoped 對象,在 Abp 的 Zero 模塊內部重寫了 Microsoft.Identity 相關組件,而這些組件也是通過 IServiceCollection.AddScoped()
方法與 IServiceCollection.TryAddScoped()
進行註入的。
public static AbpIdentityBuilder AddAbpIdentity<TTenant, TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction)
where TTenant : AbpTenant<TUser>
where TRole : AbpRole<TUser>, new()
where TUser : AbpUser<TUser>
{
services.AddSingleton<IAbpZeroEntityTypes>(new AbpZeroEntityTypes
{
Tenant = typeof(TTenant),
Role = typeof(TRole),
User = typeof(TUser)
});
//AbpTenantManager
services.TryAddScoped<AbpTenantManager<TTenant, TUser>>();
//AbpEditionManager
services.TryAddScoped<AbpEditionManager>();
//AbpRoleManager
services.TryAddScoped<AbpRoleManager<TRole, TUser>>();
services.TryAddScoped(typeof(RoleManager<TRole>), provider => provider.GetService(typeof(AbpRoleManager<TRole, TUser>)));
//AbpUserManager
services.TryAddScoped<AbpUserManager<TRole, TUser>>();
services.TryAddScoped(typeof(UserManager<TUser>), provider => provider.GetService(typeof(AbpUserManager<TRole, TUser>)));
//SignInManager
services.TryAddScoped<AbpSignInManager<TTenant, TRole, TUser>>();
services.TryAddScoped(typeof(SignInManager<TUser>), provider => provider.GetService(typeof(AbpSignInManager<TTenant, TRole, TUser>)));
// ... 其他註入代碼
return new AbpIdentityBuilder(services.AddIdentity<TUser, TRole>(setupAction), typeof(TTenant));
}
以上代碼與 DbContext 產生的異常現象一致,都會導致每次請求獲取的都是同一個對象,而 Abp 在底層會在每次請求結束後進行釋放,這樣也會造成後續請求訪問到已經被釋放的對象。
上面這些僅僅是替換 DryIoc 框架後產生的異常現象,具體的原因在於 DryIoc 官方編寫的 DryIoc.Microsoft.DependencyInjection 擴展。這是針對於 ASP.NET Core 自帶的 DI 框架進行替換的 Adapter 適配器,大體原理就是通過實現 IServiceScopeFactory
介面與 IServiceScope
介面替換掉原有 DI 框架的實現。以實現接管容器註冊與生命周期的管理。
這裡的重點就是 IServiceScopeFactory
介面,通過名字我們可以得知這是一個工廠,他擁有一個 CreateScope()
方法以創建一個 Scoped 範圍。在 MVC 處理請求的時候,通過 CreateScope()
方法獲得一個子容器,請求結束之後調用子容器的 Dispose()
方法進行釋放。
偽代碼大概如下:
public void Request()
{
var factory = serviceProvider.GetService<IServiceScopeFactory>();
using(var scoped = factory.CreateScope())
{
scoped.Resove<HomeController>().Index();
scoped.Resove<TestDbContext>();
}
}
public class HomeController : Controller
{
public HomeController(TestDbContext t1)
{
// 這裡的 t1 在 scoped 子容器釋放之後會被釋放
}
public IActionResult Index()
{
var t2 = IocManager.Instance.Resove<TestDbContext>();
}
}
可以看到它通過 using 語句塊包裹了 CreateScope()
方法,在 HomeController
解析的時候,其內部的 t1 對象是通過子容器進行解析創建出來的,那麼它的生命周期跟隨子容器的銷毀而被銷毀。子容器銷毀的時間則是在一次 Http 請求結束之後,那麼我們每次請求的時候 t1 的值都會不一樣。
而 t2 則有點特殊,因為我們重寫 IocManager
類的時候就已經知道這個 Instance
是一個靜態實例,而我們在這裡通過 Instance 進行解析出來的對象是從這個靜態實例的容器當中解析的。這個靜態容器是不會隨著請求的結束而被釋放,因此每次請求得到的 t2 值都是一樣的。
2.1 思路與解決方法
思路比較簡單,只需要在 IocManager
的 Resolve()
方法進行解析的時候,通過靜態容器 IContainer
同樣創建一個子容器即可。
更改原來的解析方法 Resolve()
,在解析的時候通過 IocContainer
的 OpenScope()
創建一個新的子容器,然後通過這個子容器進行實例解析。下麵是針對 TestApplicationService
的 GetScopedObject()
方法進行測試的結果。
子容器:
351e8576-6f70-4c9b-8cda-02d46a22455d
a4af414b-103e-4972-b7e2-8b8b067c1ce1
04bd79d5-33a2-4e2c-87ae-e72f345c4232
Ioc 靜態容器:
2e5dfd1f-36d9-4d62-94cd-c6cc66e316ef
2e5dfd1f-36d9-4d62-94cd-c6cc66e316ef
2e5dfd1f-36d9-4d62-94cd-c6cc66e316ef
雖然直接通過 OpenScope()
來構建子容器是可以解決 Scope 對象每次請求都為一個對象的 BUG,但是解析出來的子容器沒有調用 Dispose()
方法進行釋放。
目前有一個臨時的解決思路,即在 IIocManager
增加一個屬性欄位 ChildContainer
,用於存儲每次請求創建的臨時 Scope 對象,之後 IocManager 內部優先使用 ChildContainer 進行對象解析。
首先我們來到 IIocManager
介面,為其添加一個 ChildContainer 只讀屬性與 InitializeChildContainer()
的初始化方法。
public interface IIocManager : IIocRegistrar, IIocResolver, IDisposable
{
// ... 其他代碼
/// <summary>
/// 子容器
/// </summary>
/// <remarks>本屬性的值一般是由 DryIocAdapter 當中創建,而不應該在其他地方進行賦值。</remarks>
IResolverContext ChildContainer { get; }
/// <summary>
/// 初始化子容器
/// </summary>
/// <param name="container">用於初始化 IocManager 內部的子容器</param>
void InitializeChildContainer(IResolverContext container);
}
在 IocManager
類型當中實現這兩個新增的方法和屬性,並且更改一個 Resolve()
方法的內部邏輯,優先使用子容器進行對象解析。
public class IocManager : IIocManager
{
// ... 其他代碼
/// <inheritdoc />
public IResolverContext ChildContainer { get; private set; }
/// <inheritdoc />
public void InitializeChildContainer(IResolverContext container)
{
ChildContainer = container;
}
/// <summary>
/// 從 Ioc 容器當中獲取一個對象
/// 返回的對象必須通過 (see <see cref="IIocResolver.Release"/>) 進行釋放。
/// </summary>
/// <typeparam name="T">需要解析的目標類型</typeparam>
/// <returns>解析出來的實例對象</returns>
public T Resolve<T>()
{
if (ChildContainer == null) return IocContainer.Resolve<T>();
if (!ChildContainer.IsDisposed) return ChildContainer.Resolve<T>();
return IocContainer.Resolve<T>();
}
// ... 其他代碼
}
這裡僅更改了其中一個解析方法作為示範,如果正式使用的時候,請將 IocManager
的所有 Resolve()
實現都進行相應的更改。
效果圖:
因為是同一個請求,所以 Scope 生命周期的對象在這個請求的生存周期內應該解析的都是同一個對象。下麵是第二次請求時的情況:
可以看到,第二次請求的時候解析出來的 ScopeClass
類型實例都是同一個對象,其 Guid 值都變成 abd004e0-3792-4e6d-85b3-e721d8dde009
。
3. 演示項目的 GitHub 地址
https://github.com/GameBelial/Abp-DryIoc