關於 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

相關文章
相關標籤/搜索