关于 Abp 替换了 DryIoc 框架以后的问题

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

  1. 拦截器功能没法正常使用,须要重复递归查找真实类型,消耗性能。
  2. 针对于经过 IServiceCollection.AddScoped() 方法添加的 Scoped 类型的解析存在问题。

下面咱们就来针对于上述问题进行问题的分析与解决。git

1. 问题 1

1.1 现象与缘由

首先,来看一下问题 1 ,针对于问题 1 我在 Github 上面向做者请教了一下,形成嵌套注册的缘由很简单。由于之因此咱们解析的时候,原来的注册类型会解析出来代理类。github

关于上述缘由能够参考 DryIoc 的 Github 问题 #50shell

这是由于 DryIoc 是经过替换了原有注册类型的实现,而若是按照以前咱们那篇文章的方法,每次注册事件被触发的时候就会针对注册类型嵌套一层代理类。这样若是某个类型有多个拦截器,这样就会形成一个类型嵌套的问题,在外层的拦截器被拦截到的时候没法获取到当前代理的真实类型。数据库

1.2 思路与解决方法

解决思路也比较简单,就是咱们在注册某个类型的时候,触发了拦截器注入事件。在这个时候,咱们并不真正的执行代理类的一个操做。而是将须要代理的类型与它的拦截器类型经过字典存储起来,而后在类型彻底注册完成以后,经过遍历这个字典,咱们来一次性地为每个注册类型进行拦截器代理。框架

思路清晰了,那么咱们就能够编写代码来进行实现了,首先咱们先为 IocManager 增长一个内部的字典,用于存储注册类-拦截器。ide

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 进行代理类的生成与绑定。ui

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

相关文章
相关标签/搜索