剛開始建立MVC與Web API的混合項目時,碰到好多問題,今天拿出來跟你們一塊兒分享下。有朋友私信我問項目的分層及文件夾結構在個人第一篇博客中沒說清楚,那麼接下來我就準備從這些文件怎麼分文件夾提及。問題大概有如下幾點:
一、項目層的文件夾結構
二、解決MVC的Controller和Web API的Controller類名不能相同的問題
三、給MVC不一樣命名空間的Area的註冊不一樣的路由
四、讓Web API路由配置也支持命名空間參數
五、MVC及Web API添加身份驗證及錯誤處理的過濾器
六、MVC添加自定義參數模型綁定ModelBinder
七、Web API添加自定義參數綁定HttpParameterBinding
八、讓Web API同時支持多個Get方法css
1、項目層的文件夾結構
這裏的結構談我本身的項目僅供你們參考,不合理的地方歡迎你們指出。第一篇博客中我已經跟你們說了下框架的分層及簡單說了下項目層,如今咱們再仔細說下。新建MVC或Web API時微軟已經給咱們建立好了許多的文件夾,如App_Start放全局設置,Content放樣式等、Controller放控制器類、Model數據模型、Scripts腳本、Views視圖。有些人習慣了傳統的三層架構(有些是N層),喜歡把Model文件夾、Controller文件夾等單獨一個項目出來,我感受是不必,由於在不一樣文件夾下也算是一種分層了,單獨出來最多也就是編譯出來的dll是獨立的,基本沒有太多的區別。因此我仍是從簡,沿用微軟分好的文件夾。先看個人截圖
html
我添加了區域Areas,個人思路是最外層的Model(已刪除)、Controllers、Views都只放一些共通的東西,真正的項目放在Areas中,好比上圖中Mms表明個人材料管理系統,Psi是另一個系統,Sys是個人系統管理模塊。這樣就能夠作到多個系統在一個項目中,框架的重用性不言而喻。再具體看區域中一個項目
這當中微軟生成的文件夾只有Controllers、Models、Views。其它都是我建的,好比Common放項目共通的一些類,Reports準備放報表文件、ViewModels放Knouckoutjs的ViewModel腳本文件。
接下來再看看UI庫腳本庫引入的一些控件要放置在哪裏。以下圖web
我把框架的css images js themes等都放置在Content下,css中放置項目樣式及960gs框架,js下面core是自已定義的一些共通的js包括utils.js、common.js及easyui的knouckout綁定實現knouckout.bindings.js,其它一看就懂基本不用介紹了。
json
2、解決MVC的Controller和Web API的Controller類名不能相同的問題
回到區域下的一個項目文件夾內,在Controller中咱們要建立Mvc Controller及Api Controller,假如一個收料的業務(receive)
mvc路由註冊爲~/{controller}/{action},我但願的訪問地址應該是 ~/receive/action
api中由註冊爲~/api/{controller},我但願的訪問地址應該是 ~/api/receive
那麼問題就產生了,微軟設計這個框架是經過類名去匹配的 mvc下你建立一個 receiveController繼承Controller,就不能再建立一個同名的receiveController繼承ApiController,這樣的話mvc的訪問地址和api的訪問地址必需要有一個名字不能叫receive,是否是很鬱悶。
經過查看微軟System.Web.Http的源碼,咱們發現其實這個問題也很好解決,在這個DefaultHttpControllerSelector類中,微軟有定義Controller的後綴,如圖
api
咱們只要把ApiController的後綴改爲和MVC不同,就能夠解決問題了。這個字段是個靜態只讀的Field,咱們只要把它改爲ApiContrller就解決問題了。咱們首先想到的確定是反射。好吧,就這麼作,在註冊Api路由前添加如下代碼便可完成 安全
var suffix = typeof(DefaultHttpControllerSelector).GetField("ControllerSuffix", BindingFlags.Static | BindingFlags.Public); if (suffix != null) suffix.SetValue(null, "ApiController");
3、給MVC不一樣命名空間的Area的註冊不一樣的路由
這個好辦,MVC路由配置支持命名空間,新建區域時框架會自動添加{區域名}AreaRegistration.cs文件,用於註冊本區域的路由
在這個文件中的RegisterArea方法中添加如下代碼便可 架構
context.MapRoute( this.AreaName + "default", this.AreaName + "/{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new string[] { "Zephyr.Areas."+ this.AreaName + ".Controllers" } );
其中第四個參數是命名空間參數,表示這個路由設置只在此命名空間下有效。mvc
4、讓Web API路由配置也支持命名空間參數
讓人很頭疼的是Web Api路由配置居然不支持命名空間參數,這間接讓我感受它不支持Area,微軟真會開玩笑。好吧咱們仍是本身動手。在google上找到一篇文章http://netmvc.blogspot.com/2012/06/aspnet-mvc-4-webapi-support-areas-in.html 貌似被牆了,這裏有介紹一種方法替換HttpControllerSelector服務。
我直接把個人代碼貼出來,你們能夠直接用,首先建立一個新的HttpControllerSelector類 app
using System; using System.Linq; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using System.Net; namespace Zephyr.Web { public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector { private const string NamespaceRouteVariableName = "namespaceName"; private readonly HttpConfiguration _configuration; private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerCache; public NamespaceHttpControllerSelector(HttpConfiguration configuration) : base(configuration) { _configuration = configuration; _apiControllerCache = new Lazy<ConcurrentDictionary<string, Type>>(
new Func<ConcurrentDictionary<string, Type>>(InitializeApiControllerCache)); } private ConcurrentDictionary<string, Type> InitializeApiControllerCache() { IAssembliesResolver assembliesResolver = this._configuration.Services.GetAssembliesResolver(); var types = this._configuration.Services.GetHttpControllerTypeResolver()
.GetControllerTypes(assembliesResolver).ToDictionary(t => t.FullName, t => t); return new ConcurrentDictionary<string, Type>(types); } public IEnumerable<string> GetControllerFullName(HttpRequestMessage request, string controllerName) { object namespaceName; var data = request.GetRouteData(); IEnumerable<string> keys = _apiControllerCache.Value.ToDictionary<KeyValuePair<string, Type>, string, Type>(t => t.Key, t => t.Value, StringComparer.CurrentCultureIgnoreCase).Keys.ToList(); if (!data.Values.TryGetValue(NamespaceRouteVariableName, out namespaceName)) { return from k in keys where k.EndsWith(string.Format(".{0}{1}", controllerName,
DefaultHttpControllerSelector.ControllerSuffix), StringComparison.CurrentCultureIgnoreCase) select k; } string[] namespaces = (string[])namespaceName; return from n in namespaces join k in keys on string.Format("{0}.{1}{2}", n, controllerName,
DefaultHttpControllerSelector.ControllerSuffix).ToLower() equals k.ToLower() select k; } public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { Type type; if (request == null) { throw new ArgumentNullException("request"); } string controllerName = this.GetControllerName(request); if (string.IsNullOrEmpty(controllerName)) { throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound, string.Format("No route providing a controller name was found to match request URI '{0}'",
new object[] { request.RequestUri }))); } IEnumerable<string> fullNames = GetControllerFullName(request, controllerName); if (fullNames.Count() == 0) { throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound, string.Format("No route providing a controller name was found to match request URI '{0}'",
new object[] { request.RequestUri }))); } if (this._apiControllerCache.Value.TryGetValue(fullNames.First(), out type)) { return new HttpControllerDescriptor(_configuration, controllerName, type); } throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound, string.Format("No route providing a controller name was found to match request URI '{0}'",
new object[] { request.RequestUri }))); } } }
而後在WebApiConfig類的Register中替換服務便可實現框架
config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(config));
好吧,如今看看如何使用,仍是在區域的{AreaName}AreaRegistration類下的RegisterArea方法中註冊Api的路由:
GlobalConfiguration.Configuration.Routes.MapHttpRoute( this.AreaName + "Api", "api/" + this.AreaName + "/{controller}/{action}/{id}", new { action = RouteParameter.Optional, id = RouteParameter.Optional,
namespaceName = new string[] { string.Format("Zephyr.Areas.{0}.Controllers",this.AreaName) } }, new { action = new StartWithConstraint() } );
第三個參數defaults中的namespaceName,上面的服務已實現支持。第四個參數constraints我在第8個問題時會講到,這裏先略過。
5、MVC及Web API添加身份驗證及錯誤處理的過濾器
先說身份驗證的問題。不管是mvc仍是api都有一個安全性的問題,未經過身份驗證的人能不能訪問的問題。咱們新一個空項目時,默認是沒有身份驗證的,除非你在控制器類或者方法上面加上Authorize屬性纔會須要身份驗證。可是個人控制器有那麼多,我都要給它加上屬性,多麻煩,因此咱們就想到過濾器了。過濾器中加上後,控制器都不用加就至關於有這個屬性了。
Mvc的就直接在FilterConfig類的RegisterGlobalFilters方法中添加如下代碼便可
filters.Add(new System.Web.Mvc.AuthorizeAttribute());
Web Api的過濾器沒有單獨一個配置類,能夠寫在WebApiConfig類的Register中
config.Filters.Add(new System.Web.Http.AuthorizeAttribute());
Mvc錯誤處理默認有添加HandleErrorAttribute默認的過濾器,可是咱們有可能要捕捉這個錯誤並記錄系統日誌那麼這個過濾器就不夠用了,因此咱們要自定義Mvc及Web Api各自的錯誤處理類,下面貼出個人錯誤處理,MvcHandleErrorAttribute
using System.Web; using System.Web.Mvc; using log4net; namespace Zephyr.Web { public class MvcHandleErrorAttribute : HandleErrorAttribute { public override void OnException(ExceptionContext filterContext) { ILog log = LogManager.GetLogger(filterContext.RequestContext.HttpContext.Request.Url.LocalPath); log.Error(filterContext.Exception); base.OnException(filterContext); } } }
Web API的錯誤處理
using System.Net; using System.Net.Http; using System.Web; using System.Web.Http.Filters; using log4net; namespace Zephyr.Web { public class WebApiExceptionFilter : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext context) { ILog log = LogManager.GetLogger(HttpContext.Current.Request.Url.LocalPath); log.Error(context.Exception); var message = context.Exception.Message; if (context.Exception.InnerException != null) message = context.Exception.InnerException.Message; context.Response = new HttpResponseMessage() { Content = new StringContent(message) }; base.OnException(context); } } }
而後分別註冊到過濾器中,在FilterConfig類的RegisterGlobalFilters方法中
filters.Add(new MvcHandleErrorAttribute());
在WebApiConfig類的Register中
config.Filters.Add(new WebApiExceptionFilter());
這樣過濾器就定義好了。
6、MVC添加自定義模型綁定ModelBinder
在MVC中,咱們有可能會自定義一些本身想要接收的參數,那麼能夠經過ModelBinder去實現。好比我要在MVC的方法中接收JObject參數
public JsonResult DoAction(dynamic request) { }
直接這樣寫的話接收到的request爲空值,由於JObject這個類型參數Mvc未實現,咱們必須本身實現,先新建一個JObjectModelBinder類,添加以下代碼實現
using System.IO; using System.Web.Mvc; using Newtonsoft.Json; namespace Zephyr.Web { public class JObjectModelBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var stream = controllerContext.RequestContext.HttpContext.Request.InputStream; stream.Seek(0, SeekOrigin.Begin); string json = new StreamReader(stream).ReadToEnd(); return JsonConvert.DeserializeObject<dynamic>(json); } } }
而後在MVC註冊路由後面添加
ModelBinders.Binders.Add(typeof(JObject), new JObjectModelBinder()); //for dynamic model binder
添加以後,在MVC控制器中咱們就能夠接收JObject參數了。
7、Web API添加自定義參數綁定HttpParameterBinding
不知道微軟搞什麼鬼,Web Api的參數綁定機制跟Mvc的參數綁定有很大的不一樣,首先Web Api的綁定機制分兩種,一種叫Model Binding,一種叫Formatters,通常狀況下Model Binding用於讀取query string中的值,而Formatters用於讀取body中的值,這個東西要深究還有不少東西,你們有興趣本身再去研究,我這裏就簡單說一下如何自定義ModelBinding,好比在Web API中我本身定義了一個叫RequestWrapper的類,我要在Api控制器中接收RequestWrapper的參數,以下
public dynamic Get(RequestWrapper query) { //do something }
那麼咱們要新建一個RequestWrapperParameterBinding類
using System.Collections.Specialized; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Controllers; using System.Web.Http.Metadata; using Zephyr.Core; namespace Zephyr.Web { public class RequestWrapperParameterBinding : HttpParameterBinding { private struct AsyncVoid { } public RequestWrapperParameterBinding(HttpParameterDescriptor desc) : base(desc) { } public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext, CancellationToken cancellationToken) { var request = System.Web.HttpUtility.ParseQueryString(actionContext.Request.RequestUri.Query); var requestWrapper = new RequestWrapper(new NameValueCollection(request)); if (!string.IsNullOrEmpty(request["_xml"])) { var xmlType = request["_xml"].Split('.'); var xmlPath = string.Format("~/Views/Shared/Xml/{0}.xml", xmlType[xmlType.Length – 1]); if (xmlType.Length > 1) xmlPath = string.Format("~/Areas/{0}/Views/Shared/Xml/{1}.xml", xmlType); requestWrapper.LoadSettingXml(xmlPath); } SetValue(actionContext, requestWrapper); TaskCompletionSource<AsyncVoid> tcs = new TaskCompletionSource<AsyncVoid>(); tcs.SetResult(default(AsyncVoid)); return tcs.Task; } } }
接下來要把這個綁定註冊到綁定規則當中,仍是在WebApiConfig中添加
config.ParameterBindingRules.Insert(0, param => { if (param.ParameterType == typeof(RequestWrapper)) return new RequestWrapperParameterBinding(param); return null; });
此時RequestWrapper參數綁定已完成,可使用了
8、讓Web API同時支持多個Get方法
先引用微軟官方的東西把存在的問題跟你們說明白,假如Web Api在路由中註冊的爲
routes.MapHttpRoute( name: "API Default", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
而後個人控制器爲
public class ProductsController : ApiController { public void GetAllProducts() { } public IEnumerable<Product> GetProductById(int id) { } public HttpResponseMessage DeleteProduct(int id){ } }
看到上面不知道到你們看到問題了沒,若是我有兩個Get方法(我再加一個GetTop10Products,這種狀況很常見),並且參數也相同那麼路由就沒有辦法區分了。有人就想到了修改路由設置,把routeTemplate:修改成"api/{controller}/{action}/{id}",沒錯,這樣是能解決上述問題,可是你的api/products不管是Get Delete Post Input方式都沒法請求到對應的方法,你必需要api/products/GetAllProducts、api/products/DeleteProduct/4 ,action名你不能省略。如今明白了問題所在了。我就是要解決這個問題。
還記得我在寫第四點的時候有提到這裏,思路就是要定義一個constraints去實現:
咱們先分析下uri path: api/controller/x,問題就在這裏的x,它有可能表明action也有可能表明id,其實咱們就是要區分這個x什麼狀況下表明action什麼狀況下表明id就能夠解決問題了,我是想本身定義一系統的動詞,若是你的actoin的名字是以我定義的這些動詞中的一個開頭,那麼我認爲你是action,不然認爲你是id。
好,思路說明白了,咱們開始實現,先定義一個StartWithConstraint類
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http.Routing; namespace Zephyr.Web { /// <summary> /// 若是請求url如: api/area/controller/x x有多是actioin或id /// 在url中的x位置出現的是以 get put delete post開頭的字符串,則看成action,不然就看成id /// 若是action爲空,則把請求方法賦給action /// </summary> public class StartWithConstraint : IHttpRouteConstraint { public string[] array { get; set; } public bool match { get; set; } private string _id = "id"; public StartWithConstraint(string[] startwithArray = null) { if (startwithArray == null) startwithArray = new string[] { "GET", "PUT", "DELETE", "POST", "EDIT", "UPDATE", "AUDIT", "DOWNLOAD" }; this.array = startwithArray; } public bool Match(System.Net.Http.HttpRequestMessage request, IHttpRoute route, string parameterName,
IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (values == null) // shouldn't ever hit this. return true; if (!values.ContainsKey(parameterName) || !values.ContainsKey(_id)) // make sure the parameter is there. return true; var action = values[parameterName].ToString().ToLower(); if (string.IsNullOrEmpty(action)) // if the param key is empty in this case "action" add the method so it doesn't hit other methods like "GetStatus" { values[parameterName] = request.Method.ToString(); } else if (string.IsNullOrEmpty(values[_id].ToString())) { var isidstr = true; array.ToList().ForEach(x => { if (action.StartsWith(x.ToLower())) isidstr = false; }); if (isidstr) { values[_id] = values[parameterName]; values[parameterName] = request.Method.ToString(); } } return true; } } }
而後在對應的API路由註冊時,添加第四個參數constraints
GlobalConfiguration.Configuration.Routes.MapHttpRoute( this.AreaName + "Api", "api/" + this.AreaName + "/{controller}/{action}/{id}", new { action = RouteParameter.Optional, id = RouteParameter.Optional,
namespaceName = new string[] { string.Format("Zephyr.Areas.{0}.Controllers",this.AreaName) } }, new { action = new StartWithConstraint() } );
這樣就實現了,Api控制器中Action的取名就要注意點就是了,不過還算是一個比較完美的解決方案。