在之前有些过一篇文章 《使用 DryIoc 替换 Abp 的 DI 框架》 ,在该文章里面我尝试通过以替换 IocManager 内部的 IContainer 来实现使用我们自己的 DI 框架。替换了之后我们基本上是可以正常使用了,不过仍然还存在有以下两个比较显著的问题。

  1. 拦截器功能无法正常使用,需要重复递归查找真实类型,消耗性能。
  2. 针对于通过 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 当中完成一次数据库查询操作之后,会调用 DbContextDispose() 方法释放掉 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 思路与解决方法

思路比较简单,只需要在 IocManagerResolve() 方法进行解析的时候,通过静态容器 IContainer 同样创建一个子容器即可。

更改原来的解析方法 Resolve() ,在解析的时候通过 IocContainerOpenScope() 创建一个新的子容器,然后通过这个子容器进行实例解析。下面是针对 TestApplicationServiceGetScopedObject() 方法进行测试的结果。

子容器:
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

关于 Abp 替换了 DryIoc 框架之后的问题的更多相关文章

  1. abp vNext微服务框架分析

    本文转载自:https://www.cnblogs.com/william-xu/p/11245738.html abp vNext新框架的热度一直都很高,于是最近上手将vNext的微服务Demo做了 ...

  2. abp.zero 9.0框架的前端Angular使用说明

    abp.zero 9.0框架的前端Angular使用说明 目录 abp.zero 9.0框架的前端Angular使用说明 摘要 1 部署及启动 1.1 依赖包安装 1.2 使用yarn安装依赖包 1. ...

  3. 使用 DryIoc 替换 Abp 的 DI 框架

    一.背景 你说我 Castle Windsor 库用得好好的,为啥要大费周章的替换成 DryIoc 库呢?那就是性能,DryIoc 是一款优秀而且轻量级的 DI 框架,整个项目代码就两个文件,加起来代 ...

  4. ABP框架 - 启动配置

    文档目录 本节内容: 配置ABP 替换内置服务 配置模块 为一个模块创建配置 ABP在启动时,提供基础框架和模型来配置和模块化. 置ABP 在预初始化事件中进行配置,示例: kid1412注:XmlL ...

  5. ABP使用及框架解析系列 - [Unit of Work part.2-框架实现]

    前言 ABP ABP是“ASP.NET Boilerplate Project”的简称. ABP的官方网站:http://www.aspnetboilerplate.com ABP在Github上的开 ...

  6. 实战框架ABP

    abp及实战框架概述 接触abp也快一年了,有过大半年的abp项目开发经验,目前项目中所用的abp框架版本为0.10.3,最新的abp框架已经到了1.4,并且支持了asp.net core.关于abp ...

  7. ABP框架 - 多层结构

    文档目录 本节内容: 简介 ABP结构 多层 其它层(通用) 领域(Core)层 应用层 基础层 Web & 表示层 其它 总结 简介 一个应用的代码库的分层是一个广为接受的技术,用来减少复杂 ...

  8. ABP框架 - 功能管理

    文档目录 本节内容: 简介 关于 IFeatureValueStore 功能类型 Boolean 功能 Value 功能 定义功能 基本功能属性 其它功能属性 功能层次 检查功能 使用Requires ...

  9. 手工搭建基于ABP的框架(2) - 访问数据库

    为了防止不提供原网址的转载,特在这里加上原文链接: http://www.cnblogs.com/skabyy/p/7517397.html 本篇我们实现数据库的访问.我们将实现两种数据库访问方法来访 ...

随机推荐

  1. VS 2017显示“高级保存选项”命令操作方法

    Visual Studio提供“高级保存选项”功能,它能指定特定代码文件的编码规范和行尾所使用的换行符.在Visual Studio 2017中,该命令默认是没有显示在“文件”菜单中的.用户需要手工设 ...

  2. mysql触发器trigger 实例详解

    mysql触发器trigger 实例详解 (转自 https://www.cnblogs.com/phpper/p/7587031.html)   MySQL好像从5.0.2版本就开始支持触发器的功能 ...

  3. H5新特性-视频,音频-Flash-canvas绘图

    json格式 json - > AJAX json:数据格式,通常是以字符串形式表示 对象 {"name":"james","age" ...

  4. java -version显示版本和JAvA_HOME配置不一样

    当你需要安装多个版本的jdk时,可能会遇到更改了JAVA_HOME后java -version不变的情况. 一般情况下,将你的JAVA_HOME改为你要用的jdk的安装路径,然后你使用的就是这个版本的 ...

  5. PHP中逻辑运算符的高效用法---&&和||

    偶尔遇到这个,查了一下,所以就摘录了. 逻辑运算符无非是将值进行逻辑运算.还有其它用法吗?首先让我们先看一下下面的代码,然后我们再一起展开话题.提前给出结论就是(&&)or(||)”这 ...

  6. Java基础之一

    移位操作符 移位操作符只可用来处理整数类型. <<:左移位操作符,按照操作符右侧指定的位数将操作符左边的操作数向左移动,在低位补0. >>:“有符号”右移位操作符,按照操作符右 ...

  7. PHP中this,self,parent的区别

    {一}PHP中this,self,parent的区别之一this篇 面向对象编程(OOP,Object OrientedProgramming)现已经成为编程人员的一项基本技能.利用OOP的思想进行P ...

  8. 文件在线预览doc,docx转换pdf(一)

    文件在线预览doc,docx转换pdf(一) 1. 前言 文档转换是一个是一块硬骨头,但是也是必不可少的,我们正好做的知识库产品中,也面临着同样的问题,文档转换,精准的全文搜索,知识的转换率,是知识库 ...

  9. .NET CORE迁移踩坑

    https://www.cnblogs.com/leolaw/p/10740678.html

  10. FreeRTOS学习目录

    1.关于实时操作系统的一些概念性问题 2.freeRTOS与裸机程序相比有什么区别?? 3.freeRTOSConfig.h文件对FreeRTOS进行系统配置 4.FreeRTOS的内存管理