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();
}
實現過程和原理:
- 判斷 URL 是否存在(路由)
- 接收使用者輸入的引數
- 依賴注入實現
- 呼叫方法,傳輸引數,返回實現結果