为什么要改

最近公司在推广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方法,但其实从第二次调用的时候就已经不能读了,所以才用了这个缓存。

接下来的逻辑其实都简单了。

如何改写WebApi部分默认规则的更多相关文章

  1. iptables四表五链及默认规则使用,

    网络基础 TCP/IP模型: 应用层===传输层===网络层===数据链里层===物理层 数据封装: MAC帧头+IP报头+TCP/UDP报头===HTTP请求 数据帧 TCP/UDP报头: 随机产生 ...

  2. iptables默认规则

    iptables默认规则 *filter :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [34:4104] -A INPUT -m ...

  3. WebAPI的路由规则

    1.自定义路由 public static class WebApiConfig { public static void Register(HttpConfiguration config) { / ...

  4. sql改写优化:简单规则重组实现

    我们知道sql执行是一个复杂的过程,从sql到逻辑计划,到物理计划,规则重组,优化,执行引擎,都是很复杂的.尤其是优化一节,更是内容繁多.那么,是否我们本篇要来讨论这个问题呢?答案是否定的,我们只特定 ...

  5. 一张图说明 Web Api 参数绑定默认规则

    请求如下: 控制器如下: 慎重说明:不管请求方式是 get 还是 post , 简单类型的参数,如 name 和 id ,其值都是从 url 里面去取. Web API 从 url 还是 body 获 ...

  6. 一、Asp.Net Core WebAPI——修改默认监听端口

    参考文档 添加host.json文件 { "urls": "https://localhost:44389;http://localhost:44380" } ...

  7. 第一节:WebApi的纯原生态的RestFul风格接口和路由规则介绍

    一. 原生态接口 1. 从默认路由开始分析 在WebApiConfig.cs类中的Register方法中,我们可以看到默认路由如下: 分析:请求地址在 controller 前面需要加上 api/,c ...

  8. 第二十一节:Asp.Net Core MVC和WebApi路由规则的总结和对比

    一. Core Mvc 1.传统路由 Core MVC中,默认会在 Startup类→Configure方法→UseMvc方法中,会有默认路由:routes.MapRoute("defaul ...

  9. 修改Sqlserver实例默认排序规则

    1.将sqlserver安装盘加载到虚拟光驱,这里加载到F:盘跟目录 2.cmd进入命令 3.输入命令: F:/Setup /QUIET /ACTION=REBUILDDATABASE /INSTAN ...

随机推荐

  1. HTML5 canvas图像绘制方法与像素操作属性和方法

    图像绘制方法 drawImage()        向画布上绘制图像.画布或视频 像素操作属性和方法 width                                返回 ImageData ...

  2. IE6浏览器兼容问题及部分解决方案(网上整理)

    作为一个初涉前端技术的IT菜鸟,IE浏览器的兼容问题是一个不得不跨越的坎.为了能够在不同浏览器达到同样的显示效果,就不得不花心思想出办法实现兼容.由于各大主流浏览器内核不同,各自的实现标准有所差异,因 ...

  3. .SO 出现 undefined reference

    查看本SO文件依赖哪些其他的SO文件: readelf -d ldd undefined reference 涉及的问题是  主程序及静态库不能定位地址 undefined symbol 说的问题是动 ...

  4. iTween visual Editor 0.6.1

    首先添加ITween Path编辑路径(无需路径运动的动画可忽略该步骤): 然后为需要添加动画的物体添加ITween Event脚本: 若是物体沿特定路径运动,则选中Path,并选择一个路径:  若想 ...

  5. JBOSS尝鲜

    环境搭建:1. jdk-6u16-windows-i586.exe2. jboss-5.1.0.GA-jdk6.zip JDK安装: 安装过程很简单,应该都知道怎么安装软件....安装完JDK后,需要 ...

  6. 用MarkDown来排版写作

    Latex排版系统太复杂,MD很好用,微软开源了一套Madoko的开源在线MD编辑器,它提供了一台MD扩展,可以生成PDF(中间先生成Tex,再生成的PDF),幻灯片还有html.非常方便.写作,写p ...

  7. Linux永久挂载远程网络目录

    一般我们不永久挂载可以这样: mount -t cifs -o user=administrator,password=Fjqx2012,codepage=cp936,iocharset=cp936 ...

  8. 黑马程序员_Java泛型

    泛型 概述: ClassCaseException:类型转换异常. 代码中不加泛型,编译没问题,运行可能会发生异常.但是用户解决不了,这样程序就存在安全隐患.所以我们希望在编译时期就能看出问题,程序员 ...

  9. SRM 586 DIV1

    A 考虑都是格点 , 枚举所有y+-0.5就行了. trick是避免正好在y上的点重复统计. class PiecewiseLinearFunction { public: int maximumSo ...

  10. sicily 4379 bicoloring

    题意:输入一个简单(无多重边和自环)的连通无向图,判断该图是否能用黑白两种颜色对顶点染色,使得每条边的两个端点为不同颜色. 解法:由于无自连通节点存在,所以只需进行一次宽搜,遍历所有的点和所有的边,判 ...