【asp.net core】實現動態 Web API

  • 2020 年 3 月 10 日
  • 筆記

序言:

遠程工作已經一個月了,最近也算是比較閑,每天早上起床打個卡,快速弄完當天要做的工作之後就快樂摸魚去了。之前在用 ABP 框架(舊版)的時候就覺得應用服務層寫起來真的爽,為什麼實現了個 IApplicationService 的空接口就可以變成 Web API,可惜的是之前一直沒空去研究這一塊的原理及其實現,園子里也找不到相關實現原理的文章(舊版 ABP 的倒是有,但是 asp.net core 無法參考)。最近閑起來,就看了一下 abp vnext 的源碼,並且也參考了一下曉晨Master 介紹的 Panda.DynamicWebApi。我自己也簡單實現了一遍動態 Web API,不禁感嘆 asp.net core 設計之精妙。

abp vnexthttps://abp.io

Panda.DynamicWebApihttps://github.com/pdafx/Panda.DynamicWebApi

這裡先感謝這兩個庫的相關人員,沒有他們的工作,本文也出現不了。另外在此聲明,本文意在探究其實現原理並實現一個簡易版本,若無把握請勿用於生產環境。

正文:

首先先創建我們的解決方案如下:

Snipaste_2020-03-09_20-34-28

因為動態 Web API 這一功能是與業務無關的,而且為了復用,我們應該把這一功能的實現寫到一個單獨的類庫當中。上圖中 Demo 項目是 asp.net core 3.1 版本的 Web API 項目,用於演示我們的簡易動態 Web API,而 SimpleDynamicWebAPI 的 .net standard 2.0 項目則是我們的簡易動態 Web API 項目。

要實現動態 Web API,首先要做的第一件事情就是要有一個規則,來判定一個類是不是動態 Web API。在 abp vnext 當中,主要提供兩種方式一個是實現 IRemoteService 接口(實際開發過程中一般都是實現 IApplicationService 接口),另一種方式標記 RemoteServiceAttribute。而在 Panda.DynamicWebApi 中,則是實現 IDynamicWebApi 接口並且標記 DynamicWebApi。因為本文是要實現簡易版本,因此只選空接口方式。在 SimpleDynamicWebAPI 項目中創建如下空接口:

namespace SimpleDynamicWebAPI  {      public interface IApplicationService      {      }  }

接下來,我們有了 IApplicationService 接口,我們也知道實現了這個接口的類是要成為動態 Web API 的,但這個是我們所知道的規則,asp.net core 框架它是不知道的,我們需要把這個規則告訴它。

這一塊 abp vnext 有點複雜,我們參考 Panda.DynamicWebAPI 的實現:

Snipaste_2020-03-09_20-55-02

https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiServiceExtensions.cs#L46

Snipaste_2020-03-09_20-55-48

https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiControllerFeatureProvider.cs

上面圖中 DynamicWebApiControllerFeatureProvider 的 IsController 方法很明顯了。查看 msdn

Snipaste_2020-03-09_21-03-07

粗俗點翻譯過來就是判斷一個類是不是控制器。

接下來開始依樣畫葫蘆。首先一點 ControllerFeatureProvider 類是屬於 asp.net core 的,理論上是位於 Microsoft.AspNetCore.Mvc.Core 這個 nuget 包的,但是這個包的 3.x 版本並沒有發佈在 nuget 上。如果我們的 SimpleDynamicWebAPI 引用 2.x 版本的,而 Demo 項目又是 3.x 版本的,則很可能會引起衝突。保險起見,我們修改 SimpleDynamicWebAPI 為一個 asp.net core 的類庫。反正這個庫本來也不可能會被其它類型諸如 WPF 的項目引用。

修改 SimpleDynamicWebAPI.csproj 如下:

<Project Sdk="Microsoft.NET.Sdk.Web">    <PropertyGroup>      <TargetFramework>netcoreapp3.1</TargetFramework>      <OutputType>Library</OutputType>    </PropertyGroup>  </Project>

接下來創建 ApplicationServiceControllerFeatureProvider 類,並修改代碼如下:

using Microsoft.AspNetCore.Mvc.Controllers;  using System.Reflection;    namespace SimpleDynamicWebAPI  {      public class ApplicationServiceControllerFeatureProvider : ControllerFeatureProvider      {          protected override bool IsController(TypeInfo typeInfo)          {              if (typeof(IApplicationService).IsAssignableFrom(typeInfo))              {                  if (!typeInfo.IsInterface &&                      !typeInfo.IsAbstract &&                      !typeInfo.IsGenericType &&                      typeInfo.IsPublic)                  {                      return true;                  }              }                return false;          }      }  }

首先先要判斷是不是實現了 IApplicationService 接口,這個是我們一開始所定下的規則。

接下來,1、如果一個接口即使它實現了 IApplicationService,但它仍然不能是一個控制器,那是因為接口是無法實例化的;2、抽象類同理,也是因為無法實例化;3、泛型類也不允許,因為需要確切的類型才能實例化;4、public 代表着公開,可被外界訪問,如果一個類不是 public 的,那麼就不應該成為一個動態 Web API 控制器。

接下來就是要把這個 ApplicationServiceControllerFeatureProvider 加入到 asp.net core 框架中。

創建 SimpleDynamicWebApiExtensions 擴展類,修改代碼如下:

using Microsoft.Extensions.DependencyInjection;  using System;    namespace SimpleDynamicWebAPI  {      public static class SimpleDynamicWebApiExtensions      {          public static IMvcBuilder AddDynamicWebApi(this IMvcBuilder builder)          {              if (builder == null)              {                  throw new ArgumentNullException(nameof(builder));              }                builder.ConfigureApplicationPartManager(applicationPartManager =>              {                  applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());              });              return builder;          }            public static IMvcCoreBuilder AddDynamicWebApi(this IMvcCoreBuilder builder)          {              if (builder == null)              {                  throw new ArgumentNullException(nameof(builder));              }                builder.ConfigureApplicationPartManager(applicationPartManager =>              {                  applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());              });              return builder;          }      }  }

因為 ConfigureApplicationPartManager 擴展方法分別在 IMvcBuilder 和 IMvcCoreBuilder 上都有,所以我們也只能寫兩遍。當然參照 abp vnext 或 Panda.DynamicWebApi 從 services 中獲取 ApplicationPartManager 對象實例也是可行的。

接下來回到 Demo 項目,在 AddControllers 後面加上 AddDynamicWebApi:

public void ConfigureServices(IServiceCollection services)  {      services.AddControllers().AddDynamicWebApi();  }

現在我們已經完成第一步了,實現了 IApplicationService 接口的類將被視作控制器處理。但僅僅這樣並不足夠,假設有多個類同時實現 IApplicationService 接口,那應該如何映射呢,如果沒錯的話,這個時候你應該會想到是——路由。我們還需要做的工作就是把這些控制器與路由配置起來。

abp vnext 這塊為了在配置過程中獲取 services 而延遲加載導致包了一層,有點複雜。這裡參考 Panda.DynamicWebApi

Snipaste_2020-03-09_22-11-13

https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiServiceExtensions.cs#L51

注釋告訴了我們這裡是配置控制器的路由,感謝作者大大。

繼續畫葫蘆,創建 ApplicationServiceConvention 類並實現 IApplicationModelConvention 接口:

using Microsoft.AspNetCore.Mvc.ApplicationModels;  using System;    namespace SimpleDynamicWebAPI  {      public class ApplicationServiceConvention : IApplicationModelConvention      {          public void Apply(ApplicationModel application)          {              throw new NotImplementedException();          }      }  }

Apply 方法的實現等下再考慮,先把它註冊到 asp.net core 框架,修改 SimpleDynamicWebApiExtensions 擴展類如下:

using Microsoft.AspNetCore.Mvc;  using Microsoft.Extensions.DependencyInjection;  using System;    namespace SimpleDynamicWebAPI  {      public static class SimpleDynamicWebApiExtensions      {          public static IMvcBuilder AddDynamicWebApi(this IMvcBuilder builder)          {              if (builder == null)              {                  throw new ArgumentNullException(nameof(builder));              }                builder.ConfigureApplicationPartManager(applicationPartManager =>              {                  applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());              });                builder.Services.Configure<MvcOptions>(options =>              {                  options.Conventions.Add(new ApplicationServiceConvention());              });                return builder;          }            public static IMvcCoreBuilder AddDynamicWebApi(this IMvcCoreBuilder builder)          {              if (builder == null)              {                  throw new ArgumentNullException(nameof(builder));              }                builder.ConfigureApplicationPartManager(applicationPartManager =>              {                  applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());              });                builder.Services.Configure<MvcOptions>(options =>              {                  options.Conventions.Add(new ApplicationServiceConvention());              });                return builder;          }      }  }

對服務容器中的 MvcOptions 進行配置,添加上 ApplicationServiceConvention。ok,接下來回到考慮 Apply 方法實現的問題了。

這裡參考 abp vnext:

Snipaste_2020-03-09_22-26-48

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L33

上圖中的 ApplyForControllers 方法的方法體關鍵部分很好懂,foreach 遍歷了所有的控制器,如果控制器實現了 IRemoteService 接口或者標記了 RemoteServiceAttribute,則調用 ConfigureRemoteService 進一步處理。因為我們的簡易版本是只有接口,else 部分的我們就不需要了。

修改 ApplicationServiceConvention 代碼如下:

using Microsoft.AspNetCore.Mvc.ApplicationModels;  using System;    namespace SimpleDynamicWebAPI  {      public class ApplicationServiceConvention : IApplicationModelConvention      {          public void Apply(ApplicationModel application)          {              foreach (var controller in application.Controllers)              {                  if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))                  {                      ConfigureApplicationService(controller);                  }              }          }            private void ConfigureApplicationService(ControllerModel controller)          {              throw new NotImplementedException();          }      }  }

那 abp vnext 的 ConfigureRemoteService 方法中又幹了什麼呢?跳轉到 ConfigureRemoteService 的實現:

Snipaste_2020-03-09_22-36-53

做了三件事情。

1、ConfigureApiExplorer。ApiExplorer,簡單點說就是 API 是否可被發現。舉個栗子,加入你寫了一個 Web API,項目又配置了 swagger,而且你又想 swagger 不顯示這個 Web API 的話,那麼可以在 Action 上加上:

[ApiExplorerSettings(IgnoreApi = true)]

具體這裡就不說了,大家可以自行 google。

2、ConfigureSelector。Selector,選擇器,可能不太好理解。但是第三個明顯是配置參數,那麼第二這個只能是配置路由了,這個方法將會是我們的關鍵。

3、ConfigureParameters。第二點說了,配置參數。

那麼繼續修改我們的 ApplicationServiceConvention 類並且實現我們的 ConfigureApiExplorer:

using Microsoft.AspNetCore.Mvc.ApplicationModels;  using System;    namespace SimpleDynamicWebAPI  {      public class ApplicationServiceConvention : IApplicationModelConvention      {          public void Apply(ApplicationModel application)          {              foreach (var controller in application.Controllers)              {                  if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))                  {                      ConfigureApplicationService(controller);                  }              }          }            private void ConfigureApplicationService(ControllerModel controller)          {              ConfigureApiExplorer(controller);              ConfigureSelector(controller);              ConfigureParameters(controller);          }            private void ConfigureApiExplorer(ControllerModel controller)          {              if (!controller.ApiExplorer.IsVisible.HasValue)              {                  controller.ApiExplorer.IsVisible = true;              }                foreach (var action in controller.Actions)              {                  if (!action.ApiExplorer.IsVisible.HasValue)                  {                      action.ApiExplorer.IsVisible = true;                  }              }          }            private void ConfigureSelector(ControllerModel controller)          {              throw new NotImplementedException();          }            private void ConfigureParameters(ControllerModel controller)          {              throw new NotImplementedException();          }      }  }

ConfigureApiExplorer 這塊,IsVisible 只要沒有值,就無腦設為 true 好了。

接下來 ConfigureSelector 看 abp vnext 的實現:

Snipaste_2020-03-09_23-01-01

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L170

首先第一行 RemoveEmptySelectors 這是一個關鍵點。雖然我們的動態 Web API 控制器一開始並沒有配置路由,但實際上 asp.net core 框架會為此生成一些空白信息。abp vnext 在這裡就抹除掉了這些空白信息。而 Panda.DynamicWebApi 雖然沒有這樣干,但是後面的判斷邏輯就相對複雜了一些(大大別打我)。

我們的 RemoveEmptySelectors:

private void RemoveEmptySelectors(IList<SelectorModel> selectors)  {      for (var i = selectors.Count - 1; i >= 0; i--)      {          var selector = selectors[i];          if (selector.AttributeRouteModel == null &&              (selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&              (selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))          {              selectors.Remove(selector);          }      }  }

使用倒序刪除小技巧,就不需要擔心下標越界的問題了。

if 第一行明顯可以看出判斷路由信息是否存在,第二行判斷的 Action 的約束,而約束則是指 HttpGet、HttpPost 這種約束,第三行判斷了端點元數據信息,例如標記了什麼 Attribute 之類的。假如這些都沒有,那麼這條 selector 就可以斷定為空白信息了。

接下來回到 abp vnext 代碼截圖的 181 行:

Snipaste_2020-03-09_23-25-51

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L181

假如移除過空白信息後仍然有路由的話,則後續不進行處理。

接下來的 foreach 就開始處理 Action 了。先完善我們的代碼,再開始處理 Action 的路由:

using Microsoft.AspNetCore.Mvc.ApplicationModels;  using System;  using System.Collections.Generic;  using System.Linq;    namespace SimpleDynamicWebAPI  {      public class ApplicationServiceConvention : IApplicationModelConvention      {          public void Apply(ApplicationModel application)          {              foreach (var controller in application.Controllers)              {                  if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))                  {                      ConfigureApplicationService(controller);                  }              }          }            private void ConfigureApplicationService(ControllerModel controller)          {              ConfigureApiExplorer(controller);              ConfigureSelector(controller);              ConfigureParameters(controller);          }            private void ConfigureApiExplorer(ControllerModel controller)          {              if (!controller.ApiExplorer.IsVisible.HasValue)              {                  controller.ApiExplorer.IsVisible = true;              }                foreach (var action in controller.Actions)              {                  if (!action.ApiExplorer.IsVisible.HasValue)                  {                      action.ApiExplorer.IsVisible = true;                  }              }          }            private void ConfigureSelector(ControllerModel controller)          {              RemoveEmptySelectors(controller.Selectors);                if (controller.Selectors.Any(temp => temp.AttributeRouteModel != null))              {                  return;              }                foreach (var action in controller.Actions)              {                  ConfigureSelector(action);              }          }            private void ConfigureSelector(ActionModel action)          {              throw new NotImplementedException();          }            private void ConfigureParameters(ControllerModel controller)          {              throw new NotImplementedException();          }            private void RemoveEmptySelectors(IList<SelectorModel> selectors)          {              for (var i = selectors.Count - 1; i >= 0; i--)              {                  var selector = selectors[i];                  if (selector.AttributeRouteModel == null &&                      (selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&                      (selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))                  {                      selectors.Remove(selector);                  }              }          }      }  }

開始處理 Action 的路由,參考 abp vnext 的 194 行到 212 行:

Snipaste_2020-03-09_23-32-13

第一行仍然是移除空白信息。

關鍵在最後的判斷,假如沒有 selector 的話,加上就是了。但是如果已經有了呢?那就修改唄。舉個栗子,假如我們實現 IApplicationService 接口的類的一個方法標記了 HttpGet,那麼這個 Action 是有約束的,但是它卻是沒有路由的。這幾行無論是 abp vnext 還是 Panda.DynamicWebApi 都是一樣的。

初步實現添加 selector 方法,這裡我叫它 AddApplicationServiceSelector:

private void AddApplicationServiceSelector(ActionModel action)  {      var selector = new SelectorModel();      selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));      selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));        action.Selectors.Add(selector);  }    private string CalculateRouteTemplate(ActionModel action)  {      throw new NotImplementedException();  }    private string GetHttpMethod(ActionModel action)  {      throw new NotImplementedException();  }

接下來我們需要添加路由並且配置約束。

要計算路由,我們先舉個栗子(嗯,第三顆栗子了)。假設我們有一個叫 BookController 的 API 控制器,有一個叫 Save 的 Action,那麼它的路由一般就是:

api/books/{id}/save

也就是說,一般 API 控制器的路由如下:

api/[controller]s(/{id})?(/[action])?

那麼我們大概能寫出如下代碼:

private string CalculateRouteTemplate(ActionModel action)  {      var routeTemplate = new StringBuilder();      routeTemplate.Append("api");        // 控制器名稱部分      var controllerName = action.Controller.ControllerName;      if (controllerName.EndsWith("ApplicationService"))      {          controllerName = controllerName.Substring(0, controllerName.Length - "ApplicationService".Length);      }      else if (controllerName.EndsWith("AppService"))      {          controllerName = controllerName.Substring(0, controllerName.Length - "AppService".Length);      }      controllerName += "s";      routeTemplate.Append($"/{controllerName}");        // id 部分      if (action.Parameters.Any(temp => temp.ParameterName == "id"))      {          routeTemplate.Append("/{id}");      }        // Action 名稱部分      var actionName = action.ActionName;      if (actionName.EndsWith("Async"))      {          actionName = actionName.Substring(0, actionName.Length - "Async".Length);      }      var trimPrefixes = new[]      {          "GetAll","GetList","Get",          "Post","Create","Add","Insert",          "Put","Update",          "Delete","Remove",          "Patch"      };      foreach (var trimPrefix in trimPrefixes)      {          if (actionName.StartsWith(trimPrefix))          {              actionName = actionName.Substring(trimPrefix.Length);              break;          }      }      if (!string.IsNullOrEmpty(actionName))      {          routeTemplate.Append($"/{actionName}");      }        return routeTemplate.ToString();  }

以 api 開頭。

控制器部分,如果名字結尾是 ApplicationService 或者 AppService,那就裁掉。並且變為複數。因為這裡是簡易版,直接加 s 了是。實際建議使用 Inflector 等之類的庫。不然 bus 這種詞直接加 s 就太奇怪了。

id 部分沒啥好說的。

最後是 Action 部分,假如是 Async 結尾的,裁掉。接下來看開頭是不是以 Get、Post、Create 等等這些開頭,是的話也裁掉,注意要先判斷 GetAll 和 GetList 然後再判斷 Get。因為最後裁掉之後有可能是空字符串,所以還需要判斷一下再確定是否添加到路由中。

通過 Action 部分的計算,之前我們剩下的 GetHttpMethod 方法也很好寫了:

private string GetHttpMethod(ActionModel action)  {      var actionName = action.ActionName;      if (actionName.StartsWith("Get"))      {          return "GET";      }        if (actionName.StartsWith("Put") || actionName.StartsWith("Update"))      {          return "PUT";      }        if (actionName.StartsWith("Delete") || actionName.StartsWith("Remove"))      {          return "DELETE";      }        if (actionName.StartsWith("Patch"))      {          return "PATCH";      }        return "POST";  }

根據 Action 名開頭返回 Http 方法就是了,如果什麼都匹配不上就假定 POST。

添加 Selector 總算寫完了,修改 Selector 還難么?實現我們自己的 NormalizeSelectorRoutes 方法:

private void NormalizeSelectorRoutes(ActionModel action)  {      foreach (var selector in action.Selectors)      {          if (selector.AttributeRouteModel == null)          {              selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));          }            if (selector.ActionConstraints.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods?.FirstOrDefault() == null)          {              selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));          }      }  }

沒有路由就給它補路由,沒有約束就給它補約束。

現在我們的 ApplicationServiceConvention 的代碼應該如下:

using Microsoft.AspNetCore.Mvc;  using Microsoft.AspNetCore.Mvc.ActionConstraints;  using Microsoft.AspNetCore.Mvc.ApplicationModels;  using System;  using System.Collections.Generic;  using System.Linq;  using System.Text;    namespace SimpleDynamicWebAPI  {      public class ApplicationServiceConvention : IApplicationModelConvention      {          public void Apply(ApplicationModel application)          {              foreach (var controller in application.Controllers)              {                  if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))                  {                      ConfigureApplicationService(controller);                  }              }          }            private void ConfigureApplicationService(ControllerModel controller)          {              ConfigureApiExplorer(controller);              ConfigureSelector(controller);              ConfigureParameters(controller);          }            private void ConfigureApiExplorer(ControllerModel controller)          {              if (!controller.ApiExplorer.IsVisible.HasValue)              {                  controller.ApiExplorer.IsVisible = true;              }                foreach (var action in controller.Actions)              {                  if (!action.ApiExplorer.IsVisible.HasValue)                  {                      action.ApiExplorer.IsVisible = true;                  }              }          }            private void ConfigureSelector(ControllerModel controller)          {              RemoveEmptySelectors(controller.Selectors);                if (controller.Selectors.Any(temp => temp.AttributeRouteModel != null))              {                  return;              }                foreach (var action in controller.Actions)              {                  ConfigureSelector(action);              }          }            private void ConfigureSelector(ActionModel action)          {              RemoveEmptySelectors(action.Selectors);                if (action.Selectors.Count <= 0)              {                  AddApplicationServiceSelector(action);              }              else              {                  NormalizeSelectorRoutes(action);              }          }            private void ConfigureParameters(ControllerModel controller)          {              throw new NotImplementedException();          }            private void NormalizeSelectorRoutes(ActionModel action)          {              foreach (var selector in action.Selectors)              {                  if (selector.AttributeRouteModel == null)                  {                      selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));                  }                    if (selector.ActionConstraints.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods?.FirstOrDefault() == null)                  {                      selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));                  }              }          }            private void AddApplicationServiceSelector(ActionModel action)          {              var selector = new SelectorModel();              selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));              selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));                action.Selectors.Add(selector);          }            private string CalculateRouteTemplate(ActionModel action)          {              var routeTemplate = new StringBuilder();              routeTemplate.Append("api");                // 控制器名稱部分              var controllerName = action.Controller.ControllerName;              if (controllerName.EndsWith("ApplicationService"))              {                  controllerName = controllerName.Substring(0, controllerName.Length - "ApplicationService".Length);              }              else if (controllerName.EndsWith("AppService"))              {                  controllerName = controllerName.Substring(0, controllerName.Length - "AppService".Length);              }              controllerName += "s";              routeTemplate.Append($"/{controllerName}");                // id 部分              if (action.Parameters.Any(temp => temp.ParameterName == "id"))              {                  routeTemplate.Append("/{id}");              }                // Action 名稱部分              var actionName = action.ActionName;              if (actionName.EndsWith("Async"))              {                  actionName = actionName.Substring(0, actionName.Length - "Async".Length);              }              var trimPrefixes = new[]              {                  "GetAll","GetList","Get",                  "Post","Create","Add","Insert",                  "Put","Update",                  "Delete","Remove",                  "Patch"              };              foreach (var trimPrefix in trimPrefixes)              {                  if (actionName.StartsWith(trimPrefix))                  {                      actionName = actionName.Substring(trimPrefix.Length);                      break;                  }              }              if (!string.IsNullOrEmpty(actionName))              {                  routeTemplate.Append($"/{actionName}");              }                return routeTemplate.ToString();          }            private string GetHttpMethod(ActionModel action)          {              var actionName = action.ActionName;              if (actionName.StartsWith("Get"))              {                  return "GET";              }                if (actionName.StartsWith("Put") || actionName.StartsWith("Update"))              {                  return "PUT";              }                if (actionName.StartsWith("Delete") || actionName.StartsWith("Remove"))              {                  return "DELETE";              }                if (actionName.StartsWith("Patch"))              {                  return "PATCH";              }                return "POST";          }            private void RemoveEmptySelectors(IList<SelectorModel> selectors)          {              for (var i = selectors.Count - 1; i >= 0; i--)              {                  var selector = selectors[i];                  if (selector.AttributeRouteModel == null &&                      (selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&                      (selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))                  {                      selectors.Remove(selector);                  }              }          }      }  }

嗯,我們差不多完成了,就剩最後一個 ConfigureParameters。繼續參考 abp vnext(這塊 Panda.DynamicWebApi 實現也幾乎一樣了):

Snipaste_2020-03-10_00-31-04

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L67

沒啥東西,主要是配置控制器的每個 Action 的每個參數的 Binding。

關鍵就後面兩個判斷,TypeHelper.IsPrimitiveExtended 類似於判斷是不是基礎類型。例如 int、long 這些就是基礎類型,是不應該加上 FromBody 綁定的,並且 abp vnext 進一步判斷像 Nullable<int>、Nullable<long>、DateTime 這些也不應該加 FromBody 綁定。這個閱讀 TypeHelper 的源碼還是很好懂的。第二個判斷則判斷了當前 Http 約束是否能用 FormBody,例如 GET、DELETE 請求是沒辦法用 FromBody 的。

因為是簡易版,我們可以實現如下:

private void ConfigureParameters(ControllerModel controller)  {      foreach (var action in controller.Actions)      {          foreach (var parameter in action.Parameters)          {              if (parameter.BindingInfo != null)              {                  continue;              }                if (parameter.ParameterType.IsClass &&                  parameter.ParameterType != typeof(string) &&                  parameter.ParameterType != typeof(IFormFile))              {                  var httpMethods = action.Selectors.SelectMany(temp => temp.ActionConstraints).OfType<HttpMethodActionConstraint>().SelectMany(temp => temp.HttpMethods).ToList();                  if (httpMethods.Contains("GET") ||                      httpMethods.Contains("DELETE") ||                      httpMethods.Contains("TRACE") ||                      httpMethods.Contains("HEAD"))                  {                      continue;                  }                    parameter.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() });              }          }      }  }

當然第一個判斷沒有 abp vnext 和 Panda.DynamicWebApi 嚴謹,但 90% 情況足夠用了。第二個判斷則把 Http 約束通通查出來,如果有 GET、DELETE 等等這些則不能加 FromBody 約束,反之則加上。

演示:

歷經千辛萬苦,我們的簡易版動態 Web API 終於完成了。接下來我們可以給 Demo 項目添加一下測試代碼以及配置 swagger 來看一下效果。

在 Demo 項目中添加測試代碼 PersonAppService:

using SimpleDynamicWebAPI;  using System.Collections.Generic;  using System.Linq;    namespace Demo.Application  {      public class CreateUpdatePersonInput      {          public string Name { get; set; }      }        public class PersonDto      {          public string Name { get; set; }      }        public class PersonAppService : IApplicationService      {          public string Create(CreateUpdatePersonInput input)          {              return $"你造了個名字叫:{input.Name} 的人";          }            public string Delete(int id)          {              return $"你把 Id:{id} 的人幹掉了";          }            public string Get(int id)          {              return $"你輸入的 Id 是:{id}";          }            public List<PersonDto> GetAll()          {              return "服務器向你扔了一堆人"                  .ToCharArray()                  .Select(temp => new PersonDto                  {                      Name = temp.ToString()                  })                  .ToList();          }            public string Update(int id, CreateUpdatePersonInput input)          {              return $"你把 Id:{id} 的人的名字改成了 {input.Name}";          }      }  }

配置 swagger 的文章園子里多得是,這裡就不貼代碼了。

完事之後跑起來。

Snipaste_2020-03-10_01-19-16

Snipaste_2020-03-10_01-19-27

感覺還行。

結語:

我們總算實現了一個非常簡易的動態 Web API,也相當於又造了一遍輪子,但在這造輪子的過程中,我們了解到了其實現的原理,假如以後發現 abp vnext 等框架的動態 Web API 滿足不了我們的時候,我們也有一定能力進行修改。最後我再次聲明,如果沒有把握的話,千萬別用於生產環境。abp vnext 這種是經過大量項目驗證的,即使有 bug,abp vnext 官方也有足夠人力去修復。

最後附上 Gayhub 源碼:https://github.com/h82258652/SimpleDynamicWebAPI