在之前有些过一篇文章 《使用 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
。