刚开始建立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的取名就要注意点就是了,不过还算是一个比较完美的解决方案。