白話系列之實現自己簡單的mvc式webapi框架

  • 2019 年 10 月 22 日
  • 筆記

前言:此文為極簡mvc式的api框架,只當做入門api的解析方式,並且這裡也不算是mvc框架,因為沒有view層,畢竟現在大部分都屬於前後端分離,當然也可以提供view層,因為只是將view當做文本返回.

github地址:https://github.com/BestHYC/WebAPISolution.git

演示例子:

 

 

 

目標:

1.針對Home/default進行解析.2.提供簡單的httpcontext處理.3.對mvc的框架有個最簡單的了解.4.一步步通過好的框架風格,建立自己的框架風格

關鍵還有一點就是很多東西比你想的還簡單,難得是裡面可擴展可維護的實現方式以及面面俱到的安全驗證.但是前人栽樹後人乘涼,我們借鑒就好.

一:創建controller對象

目標:通過工廠模式,獲取所有controller,並創建其對象

1.1.定義介面,並定義所有Controller繼承的基類ApiBaseController

    public interface IApiController      {      }      public abstract class ApiBaseController : IApiController      {      }  

  模擬創建一個Controller,這就是我們自己定義的控制器,其實就是你自己實現的控制器

    public class HomeController : ApiBaseController      {      }  

1.2.通過工廠模式,通過名稱調用對應的控制對象,因為控制器基本上不會改變,然後通過name找到對應的控制器

   public interface IControllerFactory      {          IApiController CreateController(String name);      }      public class DefaultControllerFactory : IControllerFactory      {          private static List<Type> controllerTypes = new List<Type>();          static DefaultControllerFactory()          {              Assembly assembly = Assembly.GetExecutingAssembly();              foreach (Type type in assembly.GetTypes().Where(type => typeof(IApiController).IsAssignableFrom(type)))              {                  controllerTypes.Add(type);              }          }          public IApiController CreateController(String name)          {              if (name.IndexOf("Controller") == -1) name += "Controller";              Type controllerType = controllerTypes.FirstOrDefault(c => String.Compare(name, c.Name, true) == 0);              if (controllerType == null)              {                  return null;              }              return (IApiController)Activator.CreateInstance(controllerType);          }      }  

ok,這樣就可以取個簡單的控制器工廠模式.

二:既然控制器已經創建,那麼同樣的情況調用裡面的方法,目前home/default,會直接解析成default方法

1.先簡單的實現出調用方法

 

    public interface ActionInvoker      {          void InvokeAction(Object type, String actionName);      }      public class DefaultActionInvoker : ActionInvoker      {          public void InvokeAction(Object controller, String actionName)          {              MethodInfo methodInfo = controller.GetType().GetMethods().First(m => String.Compare(actionName, m.Name, true) == 0);              methodInfo.Invoke(controller, null);          }      }  

 

此處非常簡單,就直接調用對應的方法名即可,這就是webapi在解析路由中出現的最簡單的實現方式,

其實理論就是如此簡單,沒其他人想的那麼困難,接下來開始會做修飾,一步步來構建一個也是簡單,但是稍微有點健全的api

三:優化程式碼,針對控制器及方法做快取

/// <summary>      /// 全局所有IApiController類型的操作都是由此處進行快取      /// 其他地方只做類型處理,比如 A/B,那麼是對應的是AController 還是A,都是其他地方做處理      /// 注意此處,只當做類型及方法的快取,不做任何對執行返回結果及傳遞對象的處理,保持功能單一      /// 保持路徑單一,即A/B中A控制器只有1個,B方法也只有1個,即使有重載,也必須得通過 路由名 進行區分      /// </summary>      internal static class ApiControllerActionCache      {          private static Dictionary<String, Type> s_controllerTypes = new Dictionary<string, Type>();          private static Dictionary<Type, ActionCache> s_actionCache = new Dictionary<Type, ActionCache>();          static ApiControllerActionCache()          {              Assembly assembly = Assembly.GetExecutingAssembly();              foreach (Type type in assembly.GetTypes().Where(type => typeof(IApiController).IsAssignableFrom(type)))              {                  String name = type.Name;                  if(type.GetCustomAttribute<PreRouteAttribute>() != null)                  {                      name = type.GetCustomAttribute<PreRouteAttribute>().PreRouteName;                  }                  if (s_controllerTypes.ContainsKey(name)) throw new Exception($"{name}存在相同的路由名,請保持路由唯一");                  s_controllerTypes.Add(name, type);                  s_actionCache.Add(type, new ActionCache(type));              }          }          public static Type GetController(String controllername)          {              if (!s_controllerTypes.ContainsKey(controllername)) throw new Exception("沒有此路由對應的類型");              return s_controllerTypes[controllername];          }          /// <summary>          /// 通過路由值獲取對應的委託方法          /// </summary>          /// <param name="controllername"></param>          /// <param name="actionname"></param>          /// <returns></returns>          public static Func<IApiController, Object[], Object> GetMethod(Type controller, String actionname)          {              if(!s_actionCache.ContainsKey(controller)) throw new Exception("沒有此路由對應的類型");              ActionCache cache = s_actionCache[controller];              if (!cache.ContainsKey(actionname)) throw new Exception("沒有此路由對應的方法");              return cache.GetMethodInfo(actionname);          }      }      public class ActionCache      {          private Dictionary<String, MethodInfo> m_methodinfo;          private Dictionary<MethodInfo, Func<IApiController, Object[], Object>> m_FuncCache ;          public MethodInfo this[String name]          {              get              {                  return m_methodinfo[name];              }          }          public Boolean ContainsKey(String name)          {              return m_methodinfo.ContainsKey(name);          }          private Object m_lock = new Object();          /// <summary>          /// 可以考慮延遲載入          /// </summary>          /// <param name="type"></param>          public ActionCache(Type type)          {              m_methodinfo = new Dictionary<String, MethodInfo>();              m_FuncCache = new Dictionary<MethodInfo, Func<IApiController, object[], object>>();              foreach(MethodInfo info in type.GetMethods())              {                  String name = info.Name;                  if(info.GetCustomAttribute<RouteAttribute>() != null)                  {                      name = info.GetCustomAttribute<RouteAttribute>().RouteName;                  }                  if (m_methodinfo.ContainsKey(name)) throw new Exception($"{type.Name}中{name}重複,請保持路徑唯一");                  m_methodinfo.Add(name, info);              }          }          /// <summary>          /// 通過名稱獲取對應的委託          /// </summary>          /// <param name="methodInfo"></param>          /// <returns>IApiController:傳遞的執行方法, Object[]:方法參數, Object 返回值,void為空</returns>          public Func<IApiController, Object[], Object> GetMethodInfo(String methodName)          {              MethodInfo methodInfo = m_methodinfo[methodName];              if (!m_FuncCache.ContainsKey(methodInfo))              {                  lock (m_lock)                  {                      if (!m_FuncCache.ContainsKey(methodInfo))                      {                          m_FuncCache.Add(methodInfo, CreateExecutor(methodInfo));                      }                  }              }              return m_FuncCache[methodInfo];          }          private Func<Object, Object[], Object> CreateExecutor(MethodInfo methodInfo)          {              ParameterExpression target = Expression.Parameter(typeof(Object), "target");              ParameterExpression arguments = Expression.Parameter(typeof(Object[]), "arguments");              List<Expression> parameters = new List<Expression>();              ParameterInfo[] paramInfos = methodInfo.GetParameters();              for (Int32 i = 0; i < paramInfos.Length; i++)              {                  ParameterInfo paramInfo = paramInfos[i];                  BinaryExpression getElementByIndex = Expression.ArrayIndex(arguments, Expression.Constant(i));                  UnaryExpression converToParamterType = Expression.Convert(getElementByIndex, paramInfo.ParameterType);                  parameters.Add(converToParamterType);              }              UnaryExpression instanceCast = Expression.Convert(target, methodInfo.ReflectedType);              MethodCallExpression methodCall = Expression.Call(instanceCast, methodInfo, parameters);              UnaryExpression converToObjectType = Expression.Convert(methodCall, typeof(Object));              return Expression.Lambda<Func<Object, Object[], Object>>(converToObjectType, target, arguments).Compile();          }      }

View Code

 注意:此處對全局controller做了一次快取,限制為不會通過傳的參數進行判定使用哪個方法,只允許單個介面存在,即

1.如果在不同空間下具有相同類型名的,必須具有不同的preroute特性限定,

2.如果一個類方法重載,得通過route特性限定唯一標識

3.表達式樹是通過創建一個委託,傳遞當前的controller對象調用對應的方法

以上並不算框架,只屬於單一調用方法功能實現,並做優化,接下來屬於API框架實現

四:API實現首先得確定傳輸的值及協議標準,

4.1.確定傳輸中的必須資訊,去掉其他所有的額外資訊後有如下幾點,為求簡單,先全部保存成字元串形式:

從哪裡來(URlReferrer),到哪裡去 URL(請求的介面),

URL請求的參數(a/b?query=1中的query值解析),body中保存的值(frombody),包含的請求頭參數(Headers)

所有請求處理完成後,返回的值資訊

 

public class HttpRequest      {          /// <summary>          /// 從哪裡來的          /// </summary>          public String UrlReferrer { get; }          /// <summary>          /// 到哪裡去          /// </summary>          public String Uri { get; set; }          /// <summary>          /// Uri請求參數處理          /// </summary>          public String QueryParams { get; set; }          /// <summary>          /// 請求的內容          /// </summary>          public String RequestContent { get; set; }          /// <summary>          /// 請求頭參數          /// </summary>          public String Headers { get; set; }      }      public class HttpResponse      {          /// <summary>          /// 返回的內容          /// </summary>          public String ResponseContent { get; set; }      }      public class HttpContext      {          public HttpRequest Request { get; }          public HttpResponse Response { get; }      }

View Code

 

此處只是單純做展示使用,在後期會寫一個MQ中間件時候,在做擴展.只展示HttpContext

4.2.對Http請求做一個統一介面處理

    public class UrlRoutingModule : IRoutingModule      {          public void Init(HttpBaseContext context)          {            }      }

五:通過路由模板收集

5.1.在寫API的時候,會添加一個defaultapi匹配路由,注意:這裡以MVC的解析規則實現

    
 RouteConfig.RegisterRoutes(RouteTable.Routes);
public static class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }

5.2.考慮此模板的功能及實現方式

1.因為可能有多個默認匹配,並且執行的是順序載入過程,所以有個RouteTable靜態類收集全局模板路由

2.路由有對應的默認路由及路由值(Route RouteData)及收集Collection

3.Route實現路由解析,並提供RouteData值,並提供實現

4.在解析Route的時候,通過生成的路由句柄DefaultRouterHandler去RouteData進行後續處理

5.通過DefaultApiHandler對路由進行解析,生成對應的控制器

6.通過controller,對當前的action進行解析,並綁定路由

7.調取當前執行的方法後,獲取返回的值對象,並對返回值進行處理

大體的模型調用過程如圖

 

 6:實現程式碼:請查看對應的github地址

https://github.com/BestHYC/WebAPISolution.git

7.例子:(已經準備好對應的json序列化解析,賦值粘貼即可,看上圖)