.Net 依賴註入深入探索,做一個DI拓展,實現一個簡易靈活的 自動依賴註入框架

来源:https://www.cnblogs.com/kong-ming/p/18441175
-Advertisement-
Play Games

一、依賴註入相關知識 1.1、依賴註入的原理和優點 依賴註入(DI),是IOC控制反轉思想 的實現。由一個DI容器,去統一管理所有的服務生命周期,服務的創建、銷毀、獲取,都是由DI容器去處理的。 依賴註入,很大程度解耦了服務之間的依賴關係,服務之間依賴的是抽象(依賴的是 服務/服務介面 的 “類型” ...


一、依賴註入相關知識

1.1、依賴註入的原理和優點

  • 依賴註入(DI),是IOC控制反轉思想 的實現。由一個DI容器,去統一管理所有的服務生命周期,服務的創建、銷毀、獲取,都是由DI容器去處理的。
  • 依賴註入,很大程度解耦了服務之間的依賴關係,服務之間依賴的是抽象(依賴的是 服務/服務介面 的 “類型”),而不是依賴具體的實現(服務不用關註他依賴的服務的創建,僅通過構造函數聲明依賴的服務類型即可拿到依賴的服務實例,實際的服務實例是由容器去創建出來的)。
  • 在獲取服務時,DI能夠解析所有服務依賴關係樹,將所有直接、間接 依賴的服務都創建了出來(不同的生命周期類型,創建的時機不同,見1.3),可以直接使用。
  • 優點:解耦、生命周期管理、提高可維護性、服務可替代性。(服務之間的依賴僅是類型,不關註依賴服務的創建,因此任何服務的改動,之間的影響是非常微小的,並且依賴的介面服務,在註冊時候介面的實現可以直接替換。)
  • .net framework中 new() 創建服務,缺點:服務之間依賴的是具體的實現,依賴的服務需要手動創建出來,任何服務的改動,影響都是非常多的地方。當服務依賴層級很深的時候,外層服務使用底層服務,需要按個從低到把所有依賴的服務都創建出來。當其中某個服務構造函數發生變化時,所有用到他的地方,創建都需要更改,影響會很大,不利於維護。並且需要手動控制一些非托管資源服務的生命周期,需要註意記憶體泄漏的問題。

1.2、.Net 服務註冊

  • 1.2.1、服務類型/服務介面類型 註冊
// 將服務註冊為介面 
services.AddTransient<IMyService, MyService>(); // 泛型註冊
services.AddScoped<IMyService, MyService>();
services.AddSingleton<IMyService, MyService>();

services.AddTransient<MyService>(); // 直接註冊服務

service.AddTransient(typeof(IMyService),typeof(MyService)); // 類型註冊
service.AddTransient(typeof(MyService));
  • 1.2.2、服務實例註冊(僅適用於單例模式服務)
MyService myService = new MyService(); // 先new一個實例,然後註冊為單例模式服務
services.AddSingleton(myService); // 直接註冊
services.AddSingleton<IMyService>(myService); // 註冊為介面
  • 1.2.3、ServiceDescriptor 使用服務描述類,註冊
builder.Services.Add(new ServiceDescriptor(typeof(IMyService), typeof(MyService), ServiceLifetime.Transient)); // 註冊的服務類型,服務的實現類型,服務的生命周期
builder.Services.Add(ServiceDescriptor.Transient(typeof(IMyService), typeof(MyService)));

// 替換服務,已存在就替換;不存在,就新增
builder.Services.Replace(ServiceDescriptor.Transient(typeof(IMyService), typeof(MyService))); // Replace:如果IMyService服務已存在,就替換;不存在,就新增。

1.3、服務的三種生命周期類型,ServiceLifetime 枚舉

Singleton 單例模式

  • 從DI中拿到該服務,任何時候拿到的都是同一個實例。
  • 單例服務,一般是程式運行時就創建,也可指定為第一次訪問該服務時創建。單例服務 程式不會自動釋放,一般情況下不需要釋放;但需要註意記憶體問題,若包含非托管資源,需要註意避免記憶體泄漏問題。
  • 單例服務,不可直接依賴作用域服務。因為單例服務在程式運行時創建,這時候並沒有任何服務作用域,直接依賴作用域服務會在程式運行時就拋異常。
  • 若單例服務,需要用到作用域服務,通常需要在合適的時機創建一個服務作用域,通過服務作用域的DI,拿到作用域服務。此作用域也需要在合適的時機手動釋放掉。

Scoped 作用域模式

  • DI的同一個服務作用域中,拿到的是同一個實例。
  • 在服務作用域創建時,會將所有作用域服務都創建出來。作用域釋放時候,會釋放掉。
  • .Net Web程式 底層,所有控制器類都會自動註冊為作用域服務。在每次發起http請求時,都會自動創建一個當前http請求的服務作用域,在請求結束時候,釋放掉。

Transient 瞬時模式

  • 從DI中,每次獲取該服務時,都會創建一個新實例,每次獲取到該服務都不是同一實例。
  • 當服務不再被引用時,GC會自動回收釋放;或者當服務作用域結束時候,也會釋放掉瞬時服務。

1.6、服務註入、服務獲取

  • 常規是構造函數註入,也有第三方依賴註入框架如:Autofac 等,實現了屬性註入。但.net 推薦做法還是常規構造函數註入。
  • 通過 IServiceProvider,可以拿到所有註冊的服務。
public class EFCoreController : ControllerBase
{
    IMyTestService _myTestService;
    IServiceProvider _serviceProvider; // DI服務提供器,是單例模式。通過他可以獲取到DI中所有單例、瞬時服務。若在某個服務作用域內,則也可以獲取到所有作用域服務。此處控制器中,是在http請求的服務作用域內。
    public EFCoreController(
        IServiceProvider serviceProvider,
        IMyTestService myTestService)
    {
        _myTestService = serviceProvider;
        _myTestService = myTestService;
        _serviceProvider.GetService<T>(); // 從DI中獲取服務,若服務未創建,獲取不到,返回 null
        _serviceProvider.GetRequiredService<T>(); // 從DI中獲取服務,若服務未創建,獲取不到,則會拋異常
    }
}

1.5、依賴註入高級用法

  • 手動創建服務作用域,拿到作用域服務實例(通常用於在單例服務中,需要用到作用域服務的情況)
    (如:RabbitMQ的發佈訂閱,在BackgroundService後臺任務(後臺任務是單例模式)中註冊RabbitMQ消費者,RabbitMQ發佈事件,後臺任務中的消費者接受消息時,可以通過消息中的 人員信息 等其他信息,手動創建一個服務作用域,並手動特殊處理一些有狀態的服務,比如用戶信息上下文等。在消息消費完成時,釋放服務作用域。)
var app = builder.Build();

IServiceProvider rootServiceProvider = app.Services; // web程式運行時的DI,可以訪問單例服務和瞬時服務,但無法訪問作用域服務。
using (IServiceScope serviceScope = rootServiceProvider.CreateScope()) // IServiceProvider.CreateScope() 創建一個服務作用域,此時所有作用域服務都會創建出來。
{
    IServiceProvider serviceProvider = serviceScope.ServiceProvider; // 使用服務作用域中的DI,可以訪問 作用域服務
    TestScopeService? testScopeService = serviceProvider.GetService<TestScopeService>(); // 測試拿到作用域服務實例
}

二、設計一個輕量級 自動依賴註入框架 【設計目標】

2.1、設計目標

  • 將想要註入的服務,標註一個特性,就可以直接註入到DI中
  • 該特性可以指定將該服務註冊為某個介面的實現
  • 該特性可以指定該服務的生命周期類型
  • 在DI中,一鍵註入整個程式集中所有指定了該特性的服務

2.2、使用示例

2.2.1、在要註入的服務類上,標註CoreDI特性,指定要註冊的服務類型、註冊的服務生命周期類型

 [CoreDI(typeof(ITestSigletonService), ServiceLifetime.Singleton)] // 將該服務以介面形式註冊到DI(這個類必須是該介面的實現);指定生命周期類型
 public class TestSingletonService: ITestSigletonService
 {
     public void Test()
     {
         Console.WriteLine("單例服務");
     }
 }

 [CoreDI]  // 不指定介面,直接註冊該類;不指定生命周期類型,預設是作用域生命周期
 public class TestScopeService
 {
     public void Test()
     {
         Console.WriteLine("作用域服務");
     }
 }

2.2.2、將當前程式集類所有標註了CoreDI特性的服務,全部註入到DI

builder.Services.AddCoreDIServices(typeof(TestSingletonService).Assembly); 

三、設計一個輕量級 自動依賴註入框架 【代碼實現】

3.1、CoreDIAttribute

/// <summary>
/// 特性 將服務註入到DI中 
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class CoreDIAttribute : Attribute
{
    /// <summary>
    /// 要註入的介面類型
    /// </summary>
    public Type? InterfaceType { get; private set; }
    /// <summary>
    /// 服務的生命周期
    /// </summary>
    public ServiceLifetime ServiceLifetime { get; private set; }

    public CoreDIAttribute(Type? interfaceType = null, ServiceLifetime serviceLifeTime = ServiceLifetime.Scoped)
    {
        if (!Enum.IsDefined(serviceLifeTime))
            throw new Exception($"【CoreDI】 The Enum '{nameof(ServiceLifetime)}' value error.");
        InterfaceType = interfaceType;
        ServiceLifetime = serviceLifeTime;
    }
}

3.2、CoreDIServiceExtensions

/// <summary>
/// 依賴註入 拓展
/// </summary>
public static class CoreDIServiceExtensions
{
    /// <summary>
    /// 將這些程式集中帶有CoreDI特性的服務自動註入到DI容器
    /// </summary>
    public static void AddCoreDIServices(this IServiceCollection services, params Assembly[] assemblies)
    {
        // 找到這些程式集中包含CoreDI特性的所有服務,註冊到DI
        foreach (var assembly in assemblies)
        {
            Dictionary<Type, IEnumerable<CoreDIAttribute>> dic = assembly.GetTypes().ToDictionary(x => x, x => x.GetCustomAttributes<CoreDIAttribute>());
            foreach (var item in dic)
            {
                Type type = item.Key;
                IEnumerable<CoreDIAttribute> attributes = item.Value;
                if (attributes.Count() == 0)
                    continue;
                foreach (CoreDIAttribute di in attributes)
                {
                    if (di.InterfaceType != null && !type.GetInterfaces().Any(x => x == di.InterfaceType)) // 若服務註冊為介面形式,則必須是該介面的實現類
                        throw new Exception($"【CoreDI】 The Service '{type?.Name}' can not be resolving to the Service '{di.InterfaceType.Name}' ");
                    services.Replace(new ServiceDescriptor(di.InterfaceType ?? type, type, di.ServiceLifetime)); // 使用服務描述類 ServiceDescriptor 將服務註冊到DI,存在就替換,不存在就新增;從CoreDI特性中拿到指定的生命周期類型
                }
            }
        }
    }
}

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

-Advertisement-
Play Games
更多相關文章
  • 前言 我們在使用IDEA開發時,經常是和Git一起使用的,這樣能方便的管理我們的代碼。 git的一個特點就是可以有很多分支,這些分支能讓我們區分不同的功能等,非常方便。 有時候,我們需要查看下某個文件中,當前分支與某個分支的差異,應該如何操作呢? 如何查看不同分支的git差異 首先,我們找到我們要對 ...
  • 機緣 電腦信息技術的需要持續學習的興趣和熱情。大學學習電腦編程開發即使是短暫,不會太漫長。大學一年級對於信息科學技術的興趣只增不減。一個剛步入大學校園的高中畢業生,對於將來發生的任何事情都是十分憧憬和好奇。大學的圖書館和自習室經常都會有不同系學生的光顧。高中趕鴨子上架,大學很輕鬆,人很多。 南方 ...
  • 人工智慧artificial intelligence 是電腦互聯網信息技術多年積累和不同互聯網公司共同構造的產物。人工智慧和大數據領域聯繫很大。每個軟體公司都有公司的業務數據和目標客戶。中央倉庫和私有倉庫保存著公有數據和私有數據。Java應用開發中的Maven是對實體本地jar包的一種互聯網分佈 ...
  • 正文 忽然想起,昨天有一個財政局的工作人員,過來轉款。那時候已經快下班了。我們給他辦完之後,他說他沒想到這麼快。看來還得回去,明明都打算不回去了來著。 我有些疑惑。照理來說,財政局應該挺好玩。我這麼問他。他苦笑了一下,說要是好玩就回去上班了,天天都不想去單位。然後開始感慨我們的銀行工作好。 我實在沒 ...
  • 大家好,我是風箏 個人博客:【古時的風箏】。 本文目的為個人學習記錄及知識分享。如果有什麼不正確、不嚴謹的地方請及時指正,不勝感激。 每一個贊都是我前進的動力。 公眾號:「古時的風箏」 前幾天順手改的一個安卓啟動器,開源沒幾天,沒想到GitHub上已經獲得了40多顆星,要知道我前段時間花 ...
  • 部署SpringBoot項目(通關版) 一、概述 使用 java -jar 命令直接部署項目的JAR包和使用Docker製作鏡像進行部署是兩種常見的部署方式。以下是對這兩種方式的概述和簡要的優劣勢分析: 1.1、使用 java -jar 命令直接部署項目的JAR包 概述: 通過 java -jar ...
  • 在 Python 中,字元串格式化是將變數插入到字元串中的一種方式,Python 提供了多種字元串格式化的方法,包括舊式的 % 格式化、新式的 str.format 方法以及 f-string(格式化字元串字面量)。 ...
  • 一:背景 1. 講故事 前些天在看 AOT的時候關註了下 源生成器,挺有意思的一個東西,今天寫一篇文章簡單的分享下。 二:源生成器探究之旅 1. 源生成器是什麼 簡單來說,源生成器是Roslyn編譯器給程式員開的一道口子,在這個口子里可以塞入一些自定義的cs代碼,讓Roslyn編譯器在編譯代碼的時候 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...