标签:
为什么要改
最近公司在推广SOA框架,第一次正经接触这种技术(之前也有但还是忽略掉吧),感觉挺好,就想自己也折腾一下,实现一个简单的SOA框架
用过mvc进行开发,印象之中WebApi和Mvc好像是一样的,带着这样的预设开始玩WebApi,然后被虐得找不到着北。
被虐的原因,是Mvc和WebApi在细节上差别还是有点大,例如:
- 在Mvc中,一个Controller中的所有公共方法一般情况下可以响应POST方法,而WebApi中不行
- 在Mvc中,一个Action方法中的参数即可来自Url,也可以来自Form,而WebApi中不是这样,具体的规则好像是除非你在参数中加了[FromBody],否则这个参数永远也无法从Form中获取
这是这两种技术我知道的最大的差别,其他的没发现或者说是没注意,也有可能这些差别是因为我不会用,毕竟接触WebApi时间不长。如果我有些地方说错了,请指正。
就这两个不同点,我查了很多资料,也没有办法解决,第一个还好,加个特性就行了,第二个的话好像就算加了[FromBody]也还是不行,感觉就是一堆限制。接着,既然这么多让我不爽的地方,那我就来改造它吧。
改造的目标,有以下几个:
- 不再限制控制器必须以Controller结尾,其实这个并不是必须,只是被限制着确实不太舒服
- 所有方法可以响应所有的请求方法,如果存在方法名相同的方法,那么才需要特性来区分
- Action中的参数优先从Url中获取,再从Body中获取,从Body中获取的时候,优先假设Body中的数据是表单参数,若不是则将Body中的数据当作json或xml数据进行获取
定下了目标之后,感觉微软为什么要这样设计WebApi呢,或许它有它的道理。
目标好定,做起来真是头大,一开始想参考公司的SOA框架的实现,但因为我用了OWIN技术来进行宿主,而看了公司的框架好像不是用的这个,总之就是看了半天没看懂应该从哪个地方开始,反而是越看越糊,毕竟不是完全一样的技术,所以还是自己弄吧。
OK,废话了这么多,进入正题吧。首先来一个链接,没了这个文章我就不可能改造成功:http://www.cnblogs.com/beginor/archive/2012/03/22/2411496.html
OWIN宿主
其实这个网上很多,我主要是为了贴代码,不然的话下面几小节写不下去
- 
[assembly: OwinStartup(typeof(Startup))]//这句是在IIS宿主的时候使用的,作用是.Net会查找Startup类来启动整个服务  
- 
namespace Xinchen.SOA.Server  
- 
{  
- 
    public class Startup  
- 
    {  
- 
        public void Configuration(IAppBuilder appBuilder)  
- 
        {  
- 
            HttpConfiguration config = new HttpConfiguration();  
- 
            config.Routes.MapHttpRoute(  
- 
                name: "DefaultApi",  
- 
                routeTemplate: "{controller}/{action}"  
- 
            );  
- 
            config.Services.Add(typeof(ValueProviderFactory), new MyValueProviderFactory());//自定义参数查找,实现第三个目标  
- 
            config.Services.Replace(typeof(IHttpControllerSelector), new ControllerSelector(config));//自定义控制器查找,实现第一个目标  
- 
            config.Services.Replace(typeof(IHttpActionSelector), new HttpActionSelector());//自定义Action查找,实现第二个目标  
- 
            appBuilder.UseWebApi(config);  
- 
        }  
- 
    }  
- 
}  
省略了部分不太重要的代码,Services.Add和Replace从字面就能明白是什么意思,但我没有试过是否必须要像上面那样写才行
对控制器的限制
- 
public class ControllerSelector : IHttpControllerSelector  
- 
{  
- 
    HttpConfiguration _config;  
- 
    IDictionary<string, HttpControllerDescriptor> _desriptors = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);  
- 
    public ControllerSelector(HttpConfiguration config)  
- 
    {  
- 
        _config = config;  
- 
    }  
- 
  
- 
    void InitControllers()  
- 
    {  
- 
        if (_desriptors.Count <= 0)  
- 
        {  
- 
            lock (_desriptors)  
- 
            {  
- 
                if (_desriptors.Count <= 0)  
- 
                {  
- 
                    var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(x => !x.GlobalAssemblyCache && !x.IsDynamic);  
- 
                    var controllerTypes = new List<Type>();  
- 
                    foreach (var ass in assemblies)  
- 
                    {  
- 
                        controllerTypes.AddRange(ass.GetExportedTypes().Where(x => typeof(ApiController).IsAssignableFrom(x)));  
- 
                    }  
- 
                    var descriptors = new Dictionary<string, HttpControllerDescriptor>();  
- 
                    foreach (var controllerType in controllerTypes)  
- 
                    {  
- 
                        var descriptor = new HttpControllerDescriptor(_config, controllerType.Name, controllerType);  
- 
                        _desriptors.Add(descriptor.ControllerName, descriptor);  
- 
                    }  
- 
                }  
- 
            }  
- 
        }  
- 
    }  
- 
  
- 
    public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()  
- 
    {  
- 
        InitControllers();  
- 
        return _desriptors;  
- 
    }  
- 
  
- 
    public System.Web.Http.Controllers.HttpControllerDescriptor SelectController(System.Net.Http.HttpRequestMessage request)  
- 
    {  
- 
        InitControllers();  
- 
        var routeData = request.GetRouteData();  
- 
        var controllerName = Convert.ToString(routeData.Values.Get("controller"));  
- 
        if (string.IsNullOrWhiteSpace(controllerName))  
- 
        {  
- 
            throw new ArgumentException(string.Format("没有在路由信息中找到controller"));  
- 
        }  
- 
  
- 
        return _desriptors.Get(controllerName);  
- 
    }  
- 
  
- 
}  
这个其实比较简单,测试中WebApi好像没调用GetControllerMapping方法,直接调用了SelectController方法,最后一个方法中有两个Get方法调用,Get只是把从字典获取值的TryGetValue功能给封装了一下,InitControllers方法是从当前所有的程序集中找继承了ApiController的类,找到之后缓存起来。这段代码整体比较简单。
对Action的限制
- 
public class HttpActionSelector : IHttpActionSelector  
- 
    {  
- 
        public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)  
- 
        {  
- 
            var methods = controllerDescriptor.ControllerType.GetMethods();  
- 
            var result = new List<HttpActionDescriptor>();  
- 
            foreach (var method in methods)  
- 
            {  
- 
                var descriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, method);  
- 
                result.Add(descriptor);  
- 
            }  
- 
            return result.ToLookup(x => x.ActionName);  
- 
        }  
- 
  
- 
        public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)  
- 
        {  
- 
            var actionDescriptor = new ReflectedHttpActionDescriptor();  
- 
            var routeData = controllerContext.RouteData;  
- 
            object action = string.Empty;  
- 
            if (!routeData.Values.TryGetValue("action", out action))  
- 
            {  
- 
                throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在路由中未找到action"));  
- 
            }  
- 
            string actionName = action.ToString().ToLower();  
- 
            var methods = controllerContext.ControllerDescriptor.ControllerType.GetMethods().Where(x => x.Name.ToLower() == actionName);  
- 
            var count = methods.Count();  
- 
            MethodInfo method = null;  
- 
            switch (count)  
- 
            {  
- 
                case 0:  
- 
                    throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中未找到名为" + actionName + "的方法"));  
- 
                case 1:  
- 
                    method = methods.FirstOrDefault();  
- 
                    break;  
- 
                default:  
- 
                    var httpMethod = controllerContext.Request.Method;  
- 
                    var filterdMethods = methods.Where(x =>  
- 
                        {  
- 
                            var verb = x.GetCustomAttribute<AcceptVerbsAttribute>();  
- 
                            if (verb == null)  
- 
                            {  
- 
                                throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多个名为" + actionName + "的方法,请考虑为这些方法加上AcceptVerbsAttribute特性"));  
- 
                            }  
- 
                            return verb.HttpMethods.Contains(httpMethod);  
- 
                        });  
- 
                    if (filterdMethods.Count() > 1)  
- 
                    {  
- 
                        throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多个名为" + actionName + "的方法,并且这些方法的AcceptVerbsAttribute都含有" + httpMethod.ToString() + ",发生重复"));  
- 
                    }  
- 
                    else if (filterdMethods.Count() <= 0)  
- 
                    {  
- 
                        throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多个名为" + actionName + "的方法,但没有方法被配置为可以响应" + httpMethod.ToString() + "请求"));  
- 
                    }  
- 
                    method = filterdMethods.FirstOrDefault();  
- 
                    break;  
- 
            }  
- 
            return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, method);  
- 
        }  
- 
    }  
GetActionMapping方法很简单,从控制器类型中找到所有的Action方法并返回 
SelectAction方法相对复杂,其实就是第二个目标的逻辑,代码看起来比较多其实并有很难的地方。 
对Action的参数的限制
这一块比较难,我试了很久才成功,而且还有坑
- 
public class ActionValueBinder : DefaultActionValueBinder  
- 
    {  
- 
        protected override HttpParameterBinding GetParameterBinding(HttpParameterDescriptor parameter)  
- 
        {  
- 
            ParameterBindingAttribute parameterBinderAttribute = parameter.ParameterBinderAttribute;  
- 
            if (parameterBinderAttribute == null)  
- 
            {  
- 
                ParameterBindingRulesCollection parameterBindingRules = parameter.Configuration.ParameterBindingRules;  
- 
                if (parameterBindingRules != null)  
- 
                {  
- 
                    HttpParameterBinding binding = parameterBindingRules.LookupBinding(parameter);  
- 
                    if (binding != null)  
- 
                    {  
- 
                        return binding;  
- 
                    }  
- 
                }  
- 
                if (TypeHelper.IsValueType(parameter.ParameterType))  
- 
                {  
- 
                    return parameter.BindWithAttribute(new ValueProviderAttribute(typeof(MyValueProviderFactory)));  
- 
                }  
- 
                parameterBinderAttribute = new FromBodyAttribute();  
- 
            }  
- 
            return parameterBinderAttribute.GetBinding(parameter);  
- 
        }  
- 
    }  
这个类其实就是把.Net的默认实现给改了一点点,也就是从第17行到第20行,现在的判断逻辑是如果参数的类型为基础类型的话,则从Url或Form表单中获取,而这个逻辑是写在MyValueProviderFactory中的,ValueProviderAttribute是.Net自带的。其他并没有改动,怕是也改不动吧,因为一时间看不懂这些代码是什么意思。
- 
public class MyValueProviderFactory : ValueProviderFactory  
- 
    {  
- 
        public override IValueProvider GetValueProvider(System.Web.Http.Controllers.HttpActionContext actionContext)  
- 
        {  
- 
            return new ValueProvider(actionContext);  
- 
        }  
- 
    }  
这个很简单,略过。
- 
public class ValueProvider : IValueProvider  
- 
    {  
- 
        private IEnumerable<KeyValuePair<string, string>> _queryParameters;  
- 
        private HttpContent _httpContent;  
- 
        private HttpActionContext _context;  
- 
  
- 
        public ValueProvider(HttpActionContext context)  
- 
        {  
- 
            _context = context;  
- 
            _httpContent = context.Request.Content;  
- 
            _queryParameters = context.Request.GetQueryNameValuePairs();  
- 
        }  
- 
        public bool ContainsPrefix(string prefix)  
- 
        {  
- 
            return _queryParameters.Any(x => x.Key == prefix);  
- 
        }  
- 
  
- 
        NameValueCollection _formDatas = (NameValueCollection)CallContext.LogicalGetData("$formDatas");  
- 
  
- 
        public ValueProviderResult GetValue(string key)  
- 
        {  
- 
            var value = _queryParameters.FirstOrDefault(x => x.Key == key).Value;  
- 
            if (string.IsNullOrWhiteSpace(value))  
- 
            {  
- 
                if (_formDatas == null)  
- 
                {  
- 
                    if (_httpContent.IsFormData())  
- 
                    {  
- 
                        if (_formDatas == null)  
- 
                        {  
- 
                            _formDatas = _httpContent.ReadAsFormDataAsync().Result;  
- 
                            CallContext.LogicalSetData("$formDatas", _formDatas);  
- 
                        }  
- 
                    }  
- 
                    else  
- 
                    {  
- 
                        throw new HttpResponseException(_context.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, string.Format("未在URL中找到名为{0}的参数,此时必须传入表单参数或json或xml参数", key)));  
- 
                    }  
- 
                }  
- 
                value = _formDatas[key];  
- 
                if (string.IsNullOrWhiteSpace(value))  
- 
                {  
- 
                    throw new HttpResponseException(_context.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, string.Format("未在URL中找到名为{0}的参数,也未在表单中找到该参数", key)));  
- 
                }  
- 
            }  
- 
            return new ValueProviderResult(value, value, CultureInfo.InvariantCulture);  
- 
        }  
- 
    }  
核心是GetValue方法,首先从查询字符串中取值,若没有则判断是否有缓存,若没有的话再一次判断Body中是否表单参数,是的话就直接读取。这个地方其实一开始并没有想用缓存,但如果不用的话就会出现一个问题,如果一个Action有多个参数,那么就挂了。
原因在于:
- WebApi在查找参数时,如果这个Action有N个参数,那么WebApi会调用ActionValueBinder的GetParameterBinding方法N次
- GetParameterBinding方法在被调用这N次的时候每次都会执行parameter.BindWithAttribute(new ValueProviderAttribute(typeof(MyValueProviderFactory)));
- BindWithAttribute方法每次都会实例化一个MyValueProviderFactory对象(是WebApi实例化的)并调用GetValueProvider方法
- 大家可以看到GetValueProvider每次都new了一个ValueProvider,但这个我是可以控制的,但我发现除非我弄成全局缓存,否则是没用的,因为MyValueProviderFactory对象每次都会重新实例化。如果弄成全局缓存,那么就会影响其他的Api调用
- 然后ValueProvider又调用GetValue方法,然后就开始坑爹了
- 因为第一次GetValue的时候就会读取Body流中的表单数据,读取之后其实Body流就不能再读了,再读就成空了,所以就变成了有N个参数,就会调用N次GetValue方法,但其实从第二次调用的时候就已经不能读了,所以才用了这个缓存。
接下来的逻辑其实都简单了。
如何改写WebApi部分默认规则
标签:
原文地址:http://www.cnblogs.com/wzxinchen/p/4678374.html