如何改寫WebApi部分默認規則


為什么要改

最近公司在推廣SOA框架,第一次正經接觸這種技術(之前也有但還是忽略掉吧),感覺挺好,就想自己也折騰一下,實現一個簡單的SOA框架

用過mvc進行開發,印象之中WebApi和Mvc好像是一樣的,帶着這樣的預設開始玩WebApi,然后被虐得找不到着北。

被虐的原因,是Mvc和WebApi在細節上差別還是有點大,例如:

  1. 在Mvc中,一個Controller中的所有公共方法一般情況下可以響應POST方法,而WebApi中不行
  2. 在Mvc中,一個Action方法中的參數即可來自Url,也可以來自Form,而WebApi中不是這樣,具體的規則好像是除非你在參數中加了[FromBody],否則這個參數永遠也無法從Form中獲取

這是這兩種技術我知道的最大的差別,其他的沒發現或者說是沒注意,也有可能這些差別是因為我不會用,畢竟接觸WebApi時間不長。如果我有些地方說錯了,請指正。

就這兩個不同點,我查了很多資料,也沒有辦法解決,第一個還好,加個特性就行了,第二個的話好像就算加了[FromBody]也還是不行,感覺就是一堆限制。接着,既然這么多讓我不爽的地方,那我就來改造它吧。

改造的目標,有以下幾個:

  1. 不再限制控制器必須以Controller結尾,其實這個並不是必須,只是被限制着確實不太舒服
  2. 所有方法可以響應所有的請求方法,如果存在方法名相同的方法,那么才需要特性來區分
  3. Action中的參數優先從Url中獲取,再從Body中獲取,從Body中獲取的時候,優先假設Body中的數據是表單參數,若不是則將Body中的數據當作json或xml數據進行獲取

定下了目標之后,感覺微軟為什么要這樣設計WebApi呢,或許它有它的道理。

目標好定,做起來真是頭大,一開始想參考公司的SOA框架的實現,但因為我用了OWIN技術來進行宿主,而看了公司的框架好像不是用的這個,總之就是看了半天沒看懂應該從哪個地方開始,反而是越看越糊,畢竟不是完全一樣的技術,所以還是自己弄吧。

OK,廢話了這么多,進入正題吧。首先來一個鏈接,沒了這個文章我就不可能改造成功:http://www.cnblogs.com/beginor/archive/2012/03/22/2411496.html

OWIN宿主

其實這個網上很多,我主要是為了貼代碼,不然的話下面幾小節寫不下去

  1. [assembly: OwinStartup(typeof(Startup))]//這句是在IIS宿主的時候使用的,作用是.Net會查找Startup類來啟動整個服務
  2. namespace Xinchen.SOA.Server
  3. {
  4.     public class Startup
  5.     {
  6.         public void Configuration(IAppBuilder appBuilder)
  7.         {
  8.             HttpConfiguration config = new HttpConfiguration();
  9.             config.Routes.MapHttpRoute(
  10.                 name: "DefaultApi",
  11.                 routeTemplate: "{controller}/{action}"
  12.             );
  13.             config.Services.Add(typeof(ValueProviderFactory), new MyValueProviderFactory());//自定義參數查找,實現第三個目標
  14.             config.Services.Replace(typeof(IHttpControllerSelector), new ControllerSelector(config));//自定義控制器查找,實現第一個目標
  15.             config.Services.Replace(typeof(IHttpActionSelector), new HttpActionSelector());//自定義Action查找,實現第二個目標
  16.             appBuilder.UseWebApi(config);
  17.         }
  18.     }
  19. }

省略了部分不太重要的代碼,Services.Add和Replace從字面就能明白是什么意思,但我沒有試過是否必須要像上面那樣寫才行

對控制器的限制

  1. public class ControllerSelector : IHttpControllerSelector
  2. {
  3.     HttpConfiguration _config;
  4.     IDictionary<string, HttpControllerDescriptor> _desriptors = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
  5.     public ControllerSelector(HttpConfiguration config)
  6.     {
  7.         _config = config;
  8.     }
  9.  
  10.     void InitControllers()
  11.     {
  12.         if (_desriptors.Count <= 0)
  13.         {
  14.             lock (_desriptors)
  15.             {
  16.                 if (_desriptors.Count <= 0)
  17.                 {
  18.                     var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(x => !x.GlobalAssemblyCache && !x.IsDynamic);
  19.                     var controllerTypes = new List<Type>();
  20.                     foreach (var ass in assemblies)
  21.                     {
  22.                         controllerTypes.AddRange(ass.GetExportedTypes().Where(x => typeof(ApiController).IsAssignableFrom(x)));
  23.                     }
  24.                     var descriptors = new Dictionary<string, HttpControllerDescriptor>();
  25.                     foreach (var controllerType in controllerTypes)
  26.                     {
  27.                         var descriptor = new HttpControllerDescriptor(_config, controllerType.Name, controllerType);
  28.                         _desriptors.Add(descriptor.ControllerName, descriptor);
  29.                     }
  30.                 }
  31.             }
  32.         }
  33.     }
  34.  
  35.     public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
  36.     {
  37.         InitControllers();
  38.         return _desriptors;
  39.     }
  40.  
  41.     public System.Web.Http.Controllers.HttpControllerDescriptor SelectController(System.Net.Http.HttpRequestMessage request)
  42.     {
  43.         InitControllers();
  44.         var routeData = request.GetRouteData();
  45.         var controllerName = Convert.ToString(routeData.Values.Get("controller"));
  46.         if (string.IsNullOrWhiteSpace(controllerName))
  47.         {
  48.             throw new ArgumentException(string.Format("沒有在路由信息中找到controller"));
  49.         }
  50.  
  51.         return _desriptors.Get(controllerName);
  52.     }
  53.  
  54. }

這個其實比較簡單,測試中WebApi好像沒調用GetControllerMapping方法,直接調用了SelectController方法,最后一個方法中有兩個Get方法調用,Get只是把從字典獲取值的TryGetValue功能給封裝了一下,InitControllers方法是從當前所有的程序集中找繼承了ApiController的類,找到之后緩存起來。這段代碼整體比較簡單。

對Action的限制

  1. public class HttpActionSelector : IHttpActionSelector
  2.     {
  3.         public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
  4.         {
  5.             var methods = controllerDescriptor.ControllerType.GetMethods();
  6.             var result = new List<HttpActionDescriptor>();
  7.             foreach (var method in methods)
  8.             {
  9.                 var descriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, method);
  10.                 result.Add(descriptor);
  11.             }
  12.             return result.ToLookup(x => x.ActionName);
  13.         }
  14.  
  15.         public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
  16.         {
  17.             var actionDescriptor = new ReflectedHttpActionDescriptor();
  18.             var routeData = controllerContext.RouteData;
  19.             object action = string.Empty;
  20.             if (!routeData.Values.TryGetValue("action", out action))
  21.             {
  22.                 throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在路由中未找到action"));
  23.             }
  24.             string actionName = action.ToString().ToLower();
  25.             var methods = controllerContext.ControllerDescriptor.ControllerType.GetMethods().Where(x => x.Name.ToLower() == actionName);
  26.             var count = methods.Count();
  27.             MethodInfo method = null;
  28.             switch (count)
  29.             {
  30.                 case 0:
  31.                     throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中未找到名為" + actionName + "的方法"));
  32.                 case 1:
  33.                     method = methods.FirstOrDefault();
  34.                     break;
  35.                 default:
  36.                     var httpMethod = controllerContext.Request.Method;
  37.                     var filterdMethods = methods.Where(x =>
  38.                         {
  39.                             var verb = x.GetCustomAttribute<AcceptVerbsAttribute>();
  40.                             if (verb == null)
  41.                             {
  42.                                 throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個名為" + actionName + "的方法,請考慮為這些方法加上AcceptVerbsAttribute特性"));
  43.                             }
  44.                             return verb.HttpMethods.Contains(httpMethod);
  45.                         });
  46.                     if (filterdMethods.Count() > 1)
  47.                     {
  48.                         throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個名為" + actionName + "的方法,並且這些方法的AcceptVerbsAttribute都含有" + httpMethod.ToString() + ",發生重復"));
  49.                     }
  50.                     else if (filterdMethods.Count() <= 0)
  51.                     {
  52.                         throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個名為" + actionName + "的方法,但沒有方法被配置為可以響應" + httpMethod.ToString() + "請求"));
  53.                     }
  54.                     method = filterdMethods.FirstOrDefault();
  55.                     break;
  56.             }
  57.             return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, method);
  58.         }
  59.     }

GetActionMapping方法很簡單,從控制器類型中找到所有的Action方法並返回

SelectAction方法相對復雜,其實就是第二個目標的邏輯,代碼看起來比較多其實並有很難的地方。

對Action的參數的限制

這一塊比較難,我試了很久才成功,而且還有坑

  1. public class ActionValueBinder : DefaultActionValueBinder
  2.     {
  3.         protected override HttpParameterBinding GetParameterBinding(HttpParameterDescriptor parameter)
  4.         {
  5.             ParameterBindingAttribute parameterBinderAttribute = parameter.ParameterBinderAttribute;
  6.             if (parameterBinderAttribute == null)
  7.             {
  8.                 ParameterBindingRulesCollection parameterBindingRules = parameter.Configuration.ParameterBindingRules;
  9.                 if (parameterBindingRules != null)
  10.                 {
  11.                     HttpParameterBinding binding = parameterBindingRules.LookupBinding(parameter);
  12.                     if (binding != null)
  13.                     {
  14.                         return binding;
  15.                     }
  16.                 }
  17.                 if (TypeHelper.IsValueType(parameter.ParameterType))
  18.                 {
  19.                     return parameter.BindWithAttribute(new ValueProviderAttribute(typeof(MyValueProviderFactory)));
  20.                 }
  21.                 parameterBinderAttribute = new FromBodyAttribute();
  22.             }
  23.             return parameterBinderAttribute.GetBinding(parameter);
  24.         }
  25.     }

這個類其實就是把.Net的默認實現給改了一點點,也就是從第17行到第20行,現在的判斷邏輯是如果參數的類型為基礎類型的話,則從Url或Form表單中獲取,而這個邏輯是寫在MyValueProviderFactory中的,ValueProviderAttribute是.Net自帶的。其他並沒有改動,怕是也改不動吧,因為一時間看不懂這些代碼是什么意思。

  1. public class MyValueProviderFactory : ValueProviderFactory
  2.     {
  3.         public override IValueProvider GetValueProvider(System.Web.Http.Controllers.HttpActionContext actionContext)
  4.         {
  5.             return new ValueProvider(actionContext);
  6.         }
  7.     }

這個很簡單,略過。

  1. public class ValueProvider : IValueProvider
  2.     {
  3.         private IEnumerable<KeyValuePair<string, string>> _queryParameters;
  4.         private HttpContent _httpContent;
  5.         private HttpActionContext _context;
  6.  
  7.         public ValueProvider(HttpActionContext context)
  8.         {
  9.             _context = context;
  10.             _httpContent = context.Request.Content;
  11.             _queryParameters = context.Request.GetQueryNameValuePairs();
  12.         }
  13.         public bool ContainsPrefix(string prefix)
  14.         {
  15.             return _queryParameters.Any(x => x.Key == prefix);
  16.         }
  17.  
  18.         NameValueCollection _formDatas = (NameValueCollection)CallContext.LogicalGetData("$formDatas");
  19.  
  20.         public ValueProviderResult GetValue(string key)
  21.         {
  22.             var value = _queryParameters.FirstOrDefault(x => x.Key == key).Value;
  23.             if (string.IsNullOrWhiteSpace(value))
  24.             {
  25.                 if (_formDatas == null)
  26.                 {
  27.                     if (_httpContent.IsFormData())
  28.                     {
  29.                         if (_formDatas == null)
  30.                         {
  31.                             _formDatas = _httpContent.ReadAsFormDataAsync().Result;
  32.                             CallContext.LogicalSetData("$formDatas", _formDatas);
  33.                         }
  34.                     }
  35.                     else
  36.                     {
  37.                         throw new HttpResponseException(_context.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, string.Format("未在URL中找到名為{0}的參數,此時必須傳入表單參數或json或xml參數", key)));
  38.                     }
  39.                 }
  40.                 value = _formDatas[key];
  41.                 if (string.IsNullOrWhiteSpace(value))
  42.                 {
  43.                     throw new HttpResponseException(_context.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, string.Format("未在URL中找到名為{0}的參數,也未在表單中找到該參數", key)));
  44.                 }
  45.             }
  46.             return new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
  47.         }
  48.     }

核心是GetValue方法,首先從查詢字符串中取值,若沒有則判斷是否有緩存,若沒有的話再一次判斷Body中是否表單參數,是的話就直接讀取。這個地方其實一開始並沒有想用緩存,但如果不用的話就會出現一個問題,如果一個Action有多個參數,那么就掛了。

原因在於:

  1. WebApi在查找參數時,如果這個Action有N個參數,那么WebApi會調用ActionValueBinder的GetParameterBinding方法N次
  2. GetParameterBinding方法在被調用這N次的時候每次都會執行parameter.BindWithAttribute(new ValueProviderAttribute(typeof(MyValueProviderFactory)));
  3. BindWithAttribute方法每次都會實例化一個MyValueProviderFactory對象(是WebApi實例化的)並調用GetValueProvider方法
  4. 大家可以看到GetValueProvider每次都new了一個ValueProvider,但這個我是可以控制的,但我發現除非我弄成全局緩存,否則是沒用的,因為MyValueProviderFactory對象每次都會重新實例化。如果弄成全局緩存,那么就會影響其他的Api調用
  5. 然后ValueProvider又調用GetValue方法,然后就開始坑爹了
  6. 因為第一次GetValue的時候就會讀取Body流中的表單數據,讀取之后其實Body流就不能再讀了,再讀就成空了,所以就變成了有N個參數,就會調用N次GetValue方法,但其實從第二次調用的時候就已經不能讀了,所以才用了這個緩存。

接下來的邏輯其實都簡單了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM