關於 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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...