1. 程式人生 > >C#反射與特性(六):設計一個仿ASP.NETCore依賴注入Web

C#反射與特性(六):設計一個仿ASP.NETCore依賴注入Web

目錄

  • 1,編寫依賴注入框架
    • 1.1 路由索引
    • 1.2 依賴例項化
    • 1.3 例項化型別、依賴注入、呼叫方法
  • 2,編寫控制器和引數型別
    • 2.1 編寫型別
    • 2.2 實現控制器
  • 3,實現低配山寨 ASP.NET Core

【微信平臺,此文僅授權《NCC 開源社群》訂閱號釋出】

從前面第四篇開始,進入了實踐練習;第五篇實現了例項化一個型別以及對成員方法等的呼叫。當然,還有一些操作尚將在後面的章節進行介紹。

因為本系列屬於實踐練習,所以系列文章可能比較多,內容比較長。要學會一種技術,最好的方法是跟著例子程式碼寫一次,執行除錯。

本篇文章屬於階段練習,將前面學習到的所有知識點進行總結,實現一個依賴注入功能,仿照 ASP.NET Core 訪問 API,自動傳遞引數以及執行方法,最後返回結果。

本章的程式碼已上傳至 https://gitee.com/whuanle/codes/pby1q6amnzosgkxw830c470

效果:

對使用者效果

  • 使用者能夠訪問 Controller
  • 使用者能夠訪問 Action
  • 訪問 Action 時,傳遞引數

程式要求效果

  • 例項化型別
  • 識別型別建構函式型別
  • 根據建構函式型別動態例項化型別並且注入
  • 動態呼叫合適的過載方法

1,編寫依賴注入框架

寫完後的程式碼大概是這樣的

筆者直接在 Program 類裡面寫了,程式碼量為 200 行左右(包括詳細註釋、空白隔行)。

開始編寫程式碼前,請先引入以下名稱空間:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

在 Program 中,增加以下程式碼

        private static Assembly assembly = Assembly.GetCallingAssembly();
        private static Type[] types;
        static Program()
        {
            types = assembly.GetTypes();
        }

上面程式碼的作用是,獲取到當前程式的程式集,並且獲取元資料資訊。

這是反射第一步。

1.1 路由索引

ASP.NET Core 中的路由規則十分豐富,我們自定義各種 URL 規則。主要原理是程式在執行時,將 Controller 、Action 的 [route] 等特性收集起來,生成路由表。

程式執行的基礎是型別、方法,ASP.NET Core 中的 Controller 即是 Class,Action 即 Method。

從前面的學習中,我們瞭解到,通過反射例項化和呼叫一個型別的成員,只需要確定型別名稱、方法名稱即可。

對於路由表,我們可以假設(不是指ASP.NET Core的原理)使用者訪問 URL 時,先從路由表中對比,如果有結果,則將對應的 Class 、Method 拿到手,通過反射機制呼叫例項化型別呼叫函式。

這裡不實現這麼複雜的結構,只實現 Controller-Action 層次的路由。

1.1.1 判斷控制器 Controller 是否存在

Program 中,新增一個方法,用於判斷當前程式集中是否存在此控制器。

        /// <summary>
        /// 判斷是否有此控制器,並且返回 Type
        /// </summary>
        /// <param name="controllerName">控制器名稱(不帶Controller)</param>
        /// <returns></returns>
        private static (bool, Type) IsHasController(string controllerName)
        {
            // 不分大小寫

            string name = controllerName + "Controller";
            if (!types.Any(x => x.Name.ToLower() == name.ToLower()))
                return (false, null);

            return (true, types.FirstOrDefault(x => x.Name.ToLower() == name.ToLower()));
        }

程式碼非常簡單,而且有 Linq 的加持,幾行程式碼就 OK。

實現原理:

判斷程式集中是否具有 {var}Controller 命名的型別,例如 HomeController

如果存在,則獲取此控制器的 Type 。

1.1.2 判斷 Action 是否存在

Action 是在 Controller 裡面的(方法在型別裡面),所以我們這裡只需要判斷以下就行。

        /// <summary>
        /// 判斷一個控制器中是否具有此方法
        /// </summary>
        /// <param name="type">控制器型別</param>
        /// <param name="actionName">Action名稱</param>
        /// <returns></returns>
        private static bool IsHasAction(Type type, string actionName)
        {
            // 不分大小寫

            return type.GetMethods().Any(x => x.Name.ToLower() == actionName.ToLower());
        }

實現原理:

判斷一個型別中,是否存在 {actionname} 這個方法。

這裡不返回 MethodInfo,而是返回 bool ,是因為考慮到,方法是可以過載的,我們要根據請求時的引數,確定使用哪個方法。

所以這裡只做判斷,獲取 MethodInfo 的過程在後面。

1.2 依賴例項化

意思是,獲取一個型別的建構函式中,所有引數資訊,並且為每一個型別實現自動建立例項。

傳入引數:

需要進行依賴注入的型別的 Type。

返回資料:

建構函式引數的例項物件列表(反射都是object)。

        /// <summary>
        /// 例項化依賴
        /// </summary>
        /// <param name="type">要被例項化依賴注入的型別</param>
        public static object[] CreateType(Type type)
        {
            // 這裡只使用一個建構函式
            ConstructorInfo construct = type.GetConstructors().FirstOrDefault();

            // 獲取型別的建構函式引數
            ParameterInfo[] paramList = construct.GetParameters();

            // 依賴注入的物件列表
            List<object> objectList = new List<object>();

            // 為建構函式的每個引數型別,例項化一個型別
            foreach (ParameterInfo item in paramList)
            {
                //獲取引數型別:item.ParameterType.Name

                // 獲取程式中,哪個型別實現了 item 的介面

                Type who = types.FirstOrDefault(x => x.GetInterfaces().Any(z => z.Name == item.ParameterType.Name));

                // 例項化
                object create = Activator.CreateInstance(who, new object[] { });
                objectList.Add(create);
            }
            return objectList.ToArray();
        }

這裡有兩個點:

① 對於一個型別來說,可能有多個建構函式;

② 使用 ASP.NET Core 編寫一個控制器時,估計沒誰會寫兩個建構函式吧。。。

基於以上兩點,我們只要一個建構函式就行,不需要考慮很多情況,我們預設:一個控制器只允許定義一個建構函式,不能定義多個建構函式。

過程實現原理:

獲取到建構函式後,接著獲取建構函式中的引數列表(ParameterInfo[])。

這裡又有幾個問題

  • 引數是介面型別

  • 引數是抽象型別
  • 引數是正常的 Class 型別

那麼,按照以上劃分,要考慮的情況更加多了。這裡我們根據依賴倒置原則,我們約定,建構函式中的型別,只允許是介面。

因為這裡沒有 IOC 容器,只是簡單的反射實現,所以我們不需要考慮那麼多情況(200行程式碼還想怎麼樣。。。)。

後面我們查詢有哪個型別實現了此介面,就把這個型別例項化做引數傳遞進去。

注:後面會持續推出更多實戰型教程,敬請期待;可以關注微信訂閱號 《NCC 開源社群》,獲取最新資訊。

1.3 例項化型別、依賴注入、呼叫方法

目前來到了依賴注入的最後階段,例項化一個型別、注入依賴、呼叫方法。

        /// <summary>
        /// 實現依賴注入、呼叫方法
        /// </summary>
        /// <param name="type">型別</param>
        /// <param name="actionName">方法名稱</param>
        /// <param name="paramList">呼叫方法的引數列表</param>
        /// <returns></returns>
        private static object StartASPNETCORE(Type type, string actionName, params object[] paramList)
        {
            // 獲取 Action 過載方法 
            // 名字一樣,引數個數一致
            MethodInfo method = type.GetMethods()
                .FirstOrDefault(x => x.Name.ToLower() == actionName.ToLower()
                && x.GetParameters().Length == paramList.Length);

            // 引數有問題,找不到合適的 Action 過載進行呼叫
            // 報 405 
            if (method == null)
                return "405";

            // 例項化控制器

            // 獲取依賴物件
            object[] inject = CreateType(type);
            // 注入依賴,例項化物件
            object example = Activator.CreateInstance(type, inject);

            // 執行方法並且返回執行結果
            object result;
            try
            {
                result = method.Invoke(example, paramList);
                return result;
            }
            catch
            {
                // 報 500
                result = "500";
                return result;
            }
        }

實現原理:

通過 CreateType 方法,已經拿到例項化型別的建構函式的引數物件了。

這裡確定呼叫哪個過載方法的方式,是通過引數的多少,因為這裡控制檯輸入只能獲取 string,更加複雜通過引數型別獲取過載方法,可以自行另外測試。

呼叫一個方法大概以下幾個步驟(不分順序):

獲取型別例項;

獲取型別 Type;

獲取方法 MethodInfo;

方法的引數物件;

            // 獲取依賴物件
            object[] inject = CreateType(type);
            // 注入依賴,例項化物件
            object example = Activator.CreateInstance(type, inject);

上面程式碼中,就是實現非常簡單的依賴注入過程。

剩下的就是呼叫方法,通過引數多少去呼叫相應的過載方法了。

2,編寫控制器和引數型別

2.1 編寫型別

編寫一個介面

    /// <summary>
    /// 介面
    /// </summary>
    public interface ITest
    {
        string Add(string a, string b);
    }

實現介面

    /// <summary>
    /// 實現
    /// </summary>
    public class Test : ITest
    {
        public string Add(string a, string b)
        {
            Console.WriteLine("Add方法被執行");
            return a + b;
        }
    }

2.2 實現控制器

我們按照 ASP.NET Core 寫一個控制器的大概形式,實現一個低仿的山寨控制器。

    /// <summary>
    /// 需要自動例項化並且進行依賴注入的類
    /// </summary>
    public class MyClassController
    {
        private ITest _test;
        public MyClassController(ITest test)
        {
            _test = test;
        }
        
        /// <summary>
        /// 這是一個 Action
        /// </summary>
        /// <returns></returns>
        public string Action(string a, string b)
        {
            // 校驗http請求的引數
            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b))
                return "驗證不通過";
            //開始執行
            var result = _test.Add(a, b);
            Console.WriteLine("NCC社群", "牛逼");
            // 響應結果
            return result;
        }
    }

這是常見的依賴注入使用場景:

        private ITest _test;
        public MyClassController(ITest test)
        {
            _test = test;
        }

可以是一個數據庫上下文,可以各種型別。

由於控制檯輸入獲取到的是 string,為了減少麻煩,裡面只使用的 Action 方法,引數型別都是 string

3,實現低配山寨 ASP.NET Core

好吧,我承認我這跟ASP.NET Core沒關係,這個這是一個非常簡單的功能。

主要就是仿照 StartUp ,實現請求流程和資料返回。

        static void Main(string[] args)
        {

            while (true)
            {
                string read = string.Empty;
                Console.WriteLine("使用者你好,你要訪問的控制器(不需要帶Controller)");

                read = Console.ReadLine();

                // 檢查是否具有此控制器並且獲取 Type

                var hasController = IsHasController(read);

                // 找不到控制器,報 404 ,讓使用者重新請求
                if (!hasController.Item1)
                {
                    Console.WriteLine("404");
                    continue;
                }

                Console.WriteLine("控制器存在,請接著輸入要訪問的 Action");

                read = Console.ReadLine();

                // 檢查是否具有此 Action 並且獲取 Type
                bool hasAction = IsHasAction(hasController.Item2, read);

                // 找不到,繼續報 404 
                if (hasAction == false)
                {
                    Console.WriteLine("404");
                    continue;
                }

                // 目前為止,URL存在,那麼就是傳遞引數了

                Console.WriteLine("使用者你好,URL 存在,請輸入引數");
                Console.WriteLine("輸入每個引數按一下回車鍵,結束輸入請輸入0再按下回車鍵");

                // 開始接收使用者輸入的引數
                List<object> paramList = new List<object>();
                while (true)
                {
                    string param = Console.ReadLine();
                    if (param == "0")
                        break;
                    paramList.Add(param);
                }

                Console.WriteLine("輸入結束,正在傳送 http 請求 \n");

                // 使用者的請求已經校驗通過並且開始,現在來繼續仿 ASP.NET Core 執行

                object response = StartASPNETCORE(hasController.Item2, read, paramList.ToArray());

                Console.WriteLine("執行結果是:");
                Console.WriteLine(response);


                Console.ReadKey();
            }

實現過程和原理: