如何改寫(xiě)WebApi部分默認(rèn)規(guī)則
為什么要改
最近公司在推廣SOA框架,第一次正經(jīng)接觸這種技術(shù)(之前也有但還是忽略掉吧),感覺(jué)挺好,就想自己也折騰一下,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的SOA框架
用過(guò)mvc進(jìn)行開(kāi)發(fā),印象之中WebApi和Mvc好像是一樣的,帶著這樣的預(yù)設(shè)開(kāi)始玩WebApi,然后被虐得找不到著北。
被虐的原因,是Mvc和WebApi在細(xì)節(jié)上差別還是有點(diǎn)大,例如:
- 在Mvc中,一個(gè)Controller中的所有公共方法一般情況下可以響應(yīng)POST方法,而WebApi中不行
- 在Mvc中,一個(gè)Action方法中的參數(shù)即可來(lái)自Url,也可以來(lái)自Form,而WebApi中不是這樣,具體的規(guī)則好像是除非你在參數(shù)中加了[FromBody],否則這個(gè)參數(shù)永遠(yuǎn)也無(wú)法從Form中獲取
這是這兩種技術(shù)我知道的最大的差別,其他的沒(méi)發(fā)現(xiàn)或者說(shuō)是沒(méi)注意,也有可能這些差別是因?yàn)槲也粫?huì)用,畢竟接觸WebApi時(shí)間不長(zhǎng)。如果我有些地方說(shuō)錯(cuò)了,請(qǐng)指正。
就這兩個(gè)不同點(diǎn),我查了很多資料,也沒(méi)有辦法解決,第一個(gè)還好,加個(gè)特性就行了,第二個(gè)的話好像就算加了[FromBody]也還是不行,感覺(jué)就是一堆限制。接著,既然這么多讓我不爽的地方,那我就來(lái)改造它吧。
改造的目標(biāo),有以下幾個(gè):
- 不再限制控制器必須以Controller結(jié)尾,其實(shí)這個(gè)并不是必須,只是被限制著確實(shí)不太舒服
- 所有方法可以響應(yīng)所有的請(qǐng)求方法,如果存在方法名相同的方法,那么才需要特性來(lái)區(qū)分
- Action中的參數(shù)優(yōu)先從Url中獲取,再?gòu)腂ody中獲取,從Body中獲取的時(shí)候,優(yōu)先假設(shè)Body中的數(shù)據(jù)是表單參數(shù),若不是則將Body中的數(shù)據(jù)當(dāng)作json或xml數(shù)據(jù)進(jìn)行獲取
定下了目標(biāo)之后,感覺(jué)微軟為什么要這樣設(shè)計(jì)WebApi呢,或許它有它的道理。
目標(biāo)好定,做起來(lái)真是頭大,一開(kāi)始想?yún)⒖脊镜腟OA框架的實(shí)現(xiàn),但因?yàn)槲矣昧薕WIN技術(shù)來(lái)進(jìn)行宿主,而看了公司的框架好像不是用的這個(gè),總之就是看了半天沒(méi)看懂應(yīng)該從哪個(gè)地方開(kāi)始,反而是越看越糊,畢竟不是完全一樣的技術(shù),所以還是自己弄吧。
OK,廢話了這么多,進(jìn)入正題吧。首先來(lái)一個(gè)鏈接,沒(méi)了這個(gè)文章我就不可能改造成功:http://www.survivalescaperooms.com/beginor/archive/2012/03/22/2411496.html
OWIN宿主
其實(shí)這個(gè)網(wǎng)上很多,我主要是為了貼代碼,不然的話下面幾小節(jié)寫(xiě)不下去
- [assembly: OwinStartup(typeof(Startup))]//這句是在IIS宿主的時(shí)候使用的,作用是.Net會(huì)查找Startup類來(lái)啟動(dòng)整個(gè)服務(wù)
- 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());//自定義參數(shù)查找,實(shí)現(xiàn)第三個(gè)目標(biāo)
- config.Services.Replace(typeof(IHttpControllerSelector), new ControllerSelector(config));//自定義控制器查找,實(shí)現(xiàn)第一個(gè)目標(biāo)
- config.Services.Replace(typeof(IHttpActionSelector), new HttpActionSelector());//自定義Action查找,實(shí)現(xiàn)第二個(gè)目標(biāo)
- appBuilder.UseWebApi(config);
- }
- }
- }
省略了部分不太重要的代碼,Services.Add和Replace從字面就能明白是什么意思,但我沒(méi)有試過(guò)是否必須要像上面那樣寫(xiě)才行
對(duì)控制器的限制
- 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("沒(méi)有在路由信息中找到controller"));
- }
- return _desriptors.Get(controllerName);
- }
- }
這個(gè)其實(shí)比較簡(jiǎn)單,測(cè)試中WebApi好像沒(méi)調(diào)用GetControllerMapping方法,直接調(diào)用了SelectController方法,最后一個(gè)方法中有兩個(gè)Get方法調(diào)用,Get只是把從字典獲取值的TryGetValue功能給封裝了一下,InitControllers方法是從當(dāng)前所有的程序集中找繼承了ApiController的類,找到之后緩存起來(lái)。這段代碼整體比較簡(jiǎn)單。
對(duì)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 + "中找到多個(gè)名為" + actionName + "的方法,請(qǐng)考慮為這些方法加上AcceptVerbsAttribute特性"));
- }
- return verb.HttpMethods.Contains(httpMethod);
- });
- if (filterdMethods.Count() > 1)
- {
- throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個(gè)名為" + actionName + "的方法,并且這些方法的AcceptVerbsAttribute都含有" + httpMethod.ToString() + ",發(fā)生重復(fù)"));
- }
- else if (filterdMethods.Count() <= 0)
- {
- throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個(gè)名為" + actionName + "的方法,但沒(méi)有方法被配置為可以響應(yīng)" + httpMethod.ToString() + "請(qǐng)求"));
- }
- method = filterdMethods.FirstOrDefault();
- break;
- }
- return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, method);
- }
- }
GetActionMapping方法很簡(jiǎn)單,從控制器類型中找到所有的Action方法并返回
SelectAction方法相對(duì)復(fù)雜,其實(shí)就是第二個(gè)目標(biāo)的邏輯,代碼看起來(lái)比較多其實(shí)并有很難的地方。
對(duì)Action的參數(shù)的限制
這一塊比較難,我試了很久才成功,而且還有坑
- public class ActionValueBinder : DefaultActionValueBinder
- {
- protected override HttpParameterBinding GetParameterBinding(HttpParameterDescriptor parameter)
- {
- ParameterBindingAttribute parameterBinderAttribute = parameter.ParameterBinderAttribute;
- if (parameterBinderAttribute == null)
- {
- ParameterBindingRulesCollection parameterBindingRules = parameter.Configuratio