關於 Abp 替換了 DryIoc 框架之後的問題

来源:https://www.cnblogs.com/myzony/archive/2018/12/10/10095036.html
-Advertisement-
Play Games

在之前有些過一篇文章 "《使用 DryIoc 替換 Abp 的 DI 框架》" ,在該文章裡面我嘗試通過以替換 內部的 來實現使用我們自己的 DI 框架。替換了之後我們基本上是可以正常使用了,不過仍然還存在有以下兩個比較顯著的問題。 1. 攔截器功能無法正常使用,需要重覆遞歸查找真實類型,消耗性能。 ...


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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、單例模式是什麼? 定義:確保一個類僅僅能產生一個實例,並且提供一個全局訪問點來獲取該實例。 二、單例模式怎麼用? 1 class SingleCase 2 { 3 public string Name{get;set;} 4 public static SingleCase mySingle = ...
  • 例:字元串 string str="2,3,5,7,9," 去掉最後一個逗號 ","; 常用的方法: 1.SubString()方法 str=str.SubString(0,str.Length - 1); 2.Remove()方法 str=str.Remove(str.Length-1,1); 3 ...
  • 1.打開visual Studio 2. 通過菜單Edit -->Find and Replace -->Replace In File ,或者使用 ctrl + Shift + H 打開在文件中查找對話框,如下: Find What: 填寫查找語句的地方,可以是入任何查找關鍵字,也可以是正則表達式 ...
  • 最基礎的網頁設計,就是給你一個圖片你做成一個網頁,當然,我的工作是C#,個人網頁的功底不是很高首先先認識一下網頁的一些相關知識: 一般的,現在一個html網頁一般包含html文件,css文件,js文件,img文件這幾個部分css文件,全名叫成疊樣式表稍後會說說,js呢,這個文章暫時先不說現在說說網頁 ...
  • 今天在用到EasyUI 的Tree,TreeGrid,每次轉出這個數據格式非常不爽,就自己寫了段HELPER 輸出到前端: JsonConvert.SerializeObject(TreeDataHelper<T>.GetTreeDataFromList(tList, x1 => x1.Id, x1 ...
  • 1. 首先繼承一個listbox,來獲得按住ctrl鍵時,點擊的item 2 在listbox 的調用處: 獲得listbox 的選中項:SelectedItemsList 3 在mouseleftdown事件裡面添加處理程式 ...
  • 基於Visual Studio .NET2015的單元測試 如果類或者方法沒有用public修飾,會提示錯誤。 l Assert.Inconclusive() 表示一個未驗證的測試 l Assert.AreEqual() 測試指定的值是否相等,如果相等,則測試通過 l AreSame() 用於驗證指 ...
  • 在開發Winform程式界面的時候,我們往往會使用一些較好看的圖表,以便能夠為我們的程式界面增色,良好的圖標設置可以讓界面看起來更加美觀舒服,而且也比較容易理解,圖標我們可以通過一些網站獲取各種場景的圖標資源,不過本篇隨筆主要介紹如何利用DevExpress的內置圖標資源來實現界面圖標的設置。 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...