Web Api 的 路由机制

ASP.NET Web API 是一种框架,用于轻松构建可以访问多种客户端(包括浏览器和移动设备)的 HTTP 服务。 ASP.NET Web API 是一种用于在 .NET Framework 上构建 RESTful 应用程序的理想平台。

所以我们要想学习web api 就要去了解它的路由机制。MVC和web api的路由机制的主要区别是web api 使用的是http 方法而不是url 的路径。本文就演示一下web api 是怎样在controller 里映射 http 请求的。

web api 路由

在 web api 中 controller 是用来操作http 请求的。它里面的action 方法 对应不同的http 请求的方法。当 web api 收到一个http 请求的时候 ,路由表就会将请求映射到对应的action 方法中。

路由表的定义是在 App_Start文件夹的WebApiConfig.cs类中,路由的定义其实和mvc 的差不多的。

 public static void Register(HttpConfiguration config)
 {
 // Web API configuration and services

 // Web API routes
 config.MapHttpAttributeRoutes();

 config.Routes.MapHttpRoute(
 name: "DefaultApi", //这个可以随意命名
 routeTemplate: "api/{controller}/{id}",//web api 的地址
 defaults: new { id = RouteParameter.Optional }
 );
 }

在 Global.asax 中注册 路由。

 protected void Application_Start()
 {
 AreaRegistration.RegisterAllAreas();

 WebApiConfig.Register(GlobalConfiguration.Configuration);//注册 api 路由 必须写在mvc 路由注册之前否则会找不到。
 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
 RouteConfig.RegisterRoutes(RouteTable.Routes);
 }

当web api 框架收到http 请求的时候,它会尝试的去从路由表中的模板中匹配这个URL,如果不匹配将会显示404的错误。比如我们注册的这个模板就会匹配一下的地址:

api/WebApi
api/WebApi/2222
api/Home/werwrwe

下面的就不会匹配,因为他们少了api/这个参数:

WebApi
WebApi/2222
Home/werwrwe

这个api/的参数是为了避免和mvc 的 路由有所冲突,当然你可以随意的去命名。

一旦请求的地址被匹配,web api 将会选择相对应controller 和 action

1,模板中的{controller}将对应请求地址中的controller。

2,web api 将会根据http 请求的方法来找到以这个方法的名字开头的 action ,比如说如果请求的是GET 方法,web api 就会找 以get 开头 action,getname getdata 等等。仅仅适用于 GET,POST,PUT,DELETE这四种方法。当然还可以设置其他的方法,需要在controller 上添加属性。

3,模板中的占位符比如{id} 用来匹配传进来的参数的。

比如:

 public class TestController : ApiController
  {
  public IEnumerable<TestModel> GetAllData() { }
  public TestModel GetDataById(int id) { }
  public HttpResponseMessage Delete(int id){ }
  public HttpResponseMessage Post(TestModel test){}
  public HttpResponseMessage Put(TestModel test){}
  }

下面是每个http 请求对应的 action 方法:

Http 请求方法 对应的 action url 参数 操作
GET GetAllData/GetDataById api/webapi/1 1 获取
POST Post api/webapi 添加
DELETE Delete api/webapi 删除
PUT Put api/webapi 更新

设置action 特性来接受http 请求

这个是接受get 请求的,action 的名字可以不用get 开头。

 public class TestController : ApiController
  {
  [HttpGet]
  public IEnumerable<TestModel> Find() { }
  }

还可以接受那四个请求之外的http 请求或者多个请求:

public class TestController : ApiController
 {
 [AcceptVerbs("GET", "HEAD")]
 public IEnumerable<TestModel> Find() { }

 [AcceptVerbs("MKCOL")]
 public void Deatil() { }
 }

我们还可以按照mvc 的方式来定义 web api 的模板,比如:

routes.MapHttpRoute(
 name: "api",
 routeTemplate: "api/{controller}/{action}/{id}",
 defaults: new { id = RouteParameter.Optional }
 );

它就可以和api/test/find 相匹配。
我们还可以在action 上定义action 的名字:

public class TestController : ApiController
 {
 [AcceptVerbs("GET", "HEAD")]
 [ActionName("test")]
 public IEnumerable<TestModel> Find() { }
 }

那么api/test/test 就可以进入上面的那个方法。

如果设置NoAction

public class TestController : ApiController
 {

 [NonAction]
 public IEnumerable<TestModel> Find() { }
 }

那么web api 就不会认为它是一个 action 即使与模板匹配也不会被调用。


给Asp.Net MVC及WebApi添加路由优先级

一、为什么需要路由优先级

大家都知道我们在Asp.Net MVC项目或WebApi项目中注册路由是没有优先级的,当项目比较大、或有多个区域、或多个Web项目、或采用插件式框架开发时,我们的路由注册很可能不是写在一个文件中的,而是分散在很多不同项目的文件中,这样一来,路由的优先级的问题就突显出来了。

比如: App_Start/RouteConfig.cs中

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Areas/Admin/AdminAreaRegistration.cs中

context.MapRoute(
    name: "Login",    
    url: "login",
    defaults: new { area = "Admin", controller = "Account", action = "Login", id = UrlParameter.Optional },
    namespaces: new string[] { "Wenku.Admin.Controllers" }
);

假如是先注册上面那个通用的default路由,再注册这个login的路由,那么无论怎么样,都会先匹配第一个满足条件的路由,也就是第两个路由注册是无效的。
造成这个问题的原因就是这两个路由注册的顺序问题,而Asp.Net MVC及WebApi中注册路由都没有优先级这个概念,所以今天我们就是要自己实现这个想法,在注册路由时加入一个优先级的概念。

二、解决思路

1、先分析路由注册的入口,比如我们新建一个mvc4.0的项目

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

Mvc路由的注册入口有两个:
a. AreaRegistration.RegisterAllAreas(); 注册区域路由
b. RouteConfig.RegisterRoutes(RouteTable.Routes); 注册项目路由

WebApi路由注册入口有一个:
WebApiConfig.Register(GlobalConfiguration.Configuration); 注册WebApi路由

2、注册路由的处理类分析
AreaRegistrationContext
RouteCollection
HttpRouteCollection

注册路由时主要是由这三个类来注册处理路由的。

3、路由优先级方案
a、更改路由的注册入口
b、自定义一个路由的结构类RoutePriority及HttpRoutePriority,这两个类下面都有Priority这个属性
c、自定一个RegistrationContext来注册路由,注册的对象为上述自定义路由。
d、所有的路由注册完成之后再按优先顺序添加到RouteCollection及HttpRouteCollection中实际生效。

三、具体实现

1、路由定义

public class RoutePriority : Route
{
    public string Name { get; set; }
    public int Priority { get; set; }

    public RoutePriority(string url, IRouteHandler routeHandler)
        : base(url,routeHandler)
    {

    }
}

public class HttpRoutePriority
{
    public string Name { get; set; }
    public int Priority { get; set; }
    public string RouteTemplate{get;set;}
    public object Defaults{get;set;}
    public object Constraints{get;set;} 
    public HttpMessageHandler Handler{get;set;}
}

2、定义路由注册的接口

public interface IRouteRegister
{
    void Register(RegistrationContext context);
}

3、定义路由注册上下文类

public class RegistrationContext
{
    #region mvc
    public List<RoutePriority> Routes = new List<RoutePriority>();

    public RoutePriority MapRoute(string name, string url,int priority=0)
    {
        return MapRoute(name, url, (object)null /* defaults */, priority);
    }

    public RoutePriority MapRoute(string name, string url, object defaults, int priority = 0)
    {
        return MapRoute(name, url, defaults, (object)null /* constraints */, priority);
    }

    public RoutePriority MapRoute(string name, string url, object defaults, object constraints, int priority = 0)
    {
        return MapRoute(name, url, defaults, constraints, null /* namespaces */, priority);
    }

    public RoutePriority MapRoute(string name, string url, string[] namespaces, int priority = 0)
    {
        return MapRoute(name, url, (object)null /* defaults */, namespaces, priority);
    }

    public RoutePriority MapRoute(string name, string url, object defaults, string[] namespaces,int priority=0)
    {
        return MapRoute(name, url, defaults, null /* constraints */, namespaces, priority);
    }

    public RoutePriority MapRoute(string name, string url, object defaults, object constraints, string[] namespaces, int priority = 0)
    {
        var route = MapPriorityRoute(name, url, defaults, constraints, namespaces, priority);
        var areaName = GetAreaName(defaults);
        route.DataTokens["area"] = areaName;

        // disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up
        // controllers belonging to other areas
        bool useNamespaceFallback = (namespaces == null || namespaces.Length == 0);
        route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback;

        return route;
    }

    private static string GetAreaName(object defaults)
    {
        if (defaults != null)
        {
            var property = defaults.GetType().GetProperty("area");
            if (property != null)
                return (string)property.GetValue(defaults, null);
        }

        return null;
    }

    private RoutePriority MapPriorityRoute(string name, string url, object defaults, object constraints, string[] namespaces,int priority)
    {
        if (url == null)
        {
            throw new ArgumentNullException("url");
        }

        var route = new RoutePriority(url, new MvcRouteHandler())
        {
            Name = name,
            Priority = priority,
            Defaults = CreateRouteValueDictionary(defaults),
            Constraints = CreateRouteValueDictionary(constraints),
            DataTokens = new RouteValueDictionary()
        };

        if ((namespaces != null) && (namespaces.Length > 0))
        {
            route.DataTokens["Namespaces"] = namespaces;
        }

        Routes.Add(route);
        return route;
    }

    private static RouteValueDictionary CreateRouteValueDictionary(object values)
    {
        var dictionary = values as IDictionary<string, object>;
        if (dictionary != null)
        {
            return new RouteValueDictionary(dictionary);
        }

        return new RouteValueDictionary(values);
    }
    #endregion

    #region http
    public List<HttpRoutePriority> HttpRoutes = new List<HttpRoutePriority>();

    public HttpRoutePriority MapHttpRoute(string name, string routeTemplate, int priority = 0)
    {
        return MapHttpRoute(name, routeTemplate, defaults: null, constraints: null, handler: null, priority: priority);
    }

    public HttpRoutePriority MapHttpRoute(string name, string routeTemplate, object defaults, int priority = 0)
    {
        return MapHttpRoute(name, routeTemplate, defaults, constraints: null, handler: null, priority: priority);
    }

    public HttpRoutePriority MapHttpRoute(string name, string routeTemplate, object defaults, object constraints, int priority = 0)
    {
        return MapHttpRoute(name, routeTemplate, defaults, constraints, handler: null, priority: priority);
    }

    public HttpRoutePriority MapHttpRoute(string name, string routeTemplate, object defaults, object constraints, HttpMessageHandler handler, int priority = 0)
    {
        var httpRoute = new HttpRoutePriority();
        httpRoute.Name = name;
        httpRoute.RouteTemplate = routeTemplate;
        httpRoute.Defaults = defaults;
        httpRoute.Constraints = constraints;
        httpRoute.Handler = handler;
        httpRoute.Priority = priority;
        HttpRoutes.Add(httpRoute);

        return httpRoute;
    }
    #endregion
}

4、把路由注册处理方法添加到Configuration类中

public static Configuration RegisterRoutePriority(this Configuration config)
{
    var typesSoFar = new List<Type>();
    var assemblies = GetReferencedAssemblies();
    foreach (Assembly assembly in assemblies)
    {
        var types = assembly.GetTypes().Where(t => typeof(IRouteRegister).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface);
        typesSoFar.AddRange(types);
    }

    var context = new RegistrationContext();
    foreach (var type in typesSoFar)
    {
        var obj = (IRouteRegister)Activator.CreateInstance(type);
        obj.Register(context);
    }

    foreach (var route in context.HttpRoutes.OrderByDescending(x => x.Priority))
        GlobalConfiguration.Configuration.Routes.MapHttpRoute(route.Name, route.RouteTemplate, route.Defaults, route.Constraints, route.Handler);

    foreach (var route in context.Routes.OrderByDescending(x => x.Priority))
        RouteTable.Routes.Add(route.Name, route);

    return config;
}

private static IEnumerable<Assembly> GetReferencedAssemblies()
{
    var assemblies = BuildManager.GetReferencedAssemblies();
    foreach (Assembly assembly in assemblies)
        yield return assembly;
}

这样一来就大功告成,使用时只需要在Global.asax.cs文件中修改原注册入口为

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);

        Configuration.Instance()
            .RegisterComponents()
            .RegisterRoutePriority(); //注册自定义路由
    }
}

在每个项目中使用只需要要继承自定义路由注册接口IRouteRegister,例如:

public class Registration : IRouteRegister
{
    public void Register(RegistrationContext context)
    {
       //注册后端管理登录路由
        context.MapRoute(
          name: "Admin_Login",
          url: "Admin/login",
          defaults: new { area = "Admin", controller = "Account", action = "Login", id = UrlParameter.Optional },
          namespaces: new string[] { "Wenku.Admin.Controllers" },
          priority: 11
      );

       //注册后端管理页面默认路由
        context.MapRoute(
            name: "Admin_default",
            url: "Admin/{controller}/{action}/{id}",
            defaults: new { area = "Admin", controller = "Home", action = "Index", id = UrlParameter.Optional },
            namespaces: new string[] { "Wenku.Admin.Controllers" },
            priority: 10
        );

       //注册手机访问WebApi路由
        context.MapHttpRoute(
            name: "Mobile_Api",
            routeTemplate: "api/mobile/{controller}/{action}/{id}",
            defaults: new
            {
                area = "mobile",
                action = RouteParameter.Optional,
                id = RouteParameter.Optional,
                namespaceName = new string[] { "Wenku.Mobile.Http" }
            },
            constraints: new { action = new StartWithConstraint() },
            priority: 0
        );
    }
}

四、总结

这是一个对Asp.Net Mvc的一个很小的功能拓展,小项目可能不太需要这个功能,但有时候项目大了注册的路由不生效时你应该要想到有可能是因为路由顺序的原因,这时这个路由优先级的功能有可能就会给你带来便利。总之共享给有需要的朋友们参考。

Asp.Net MVC及Web API框架配置会碰到的几个问题及解决方案

前言

刚开始创建MVC与Web API的混合项目时,碰到好多问题,今天拿出来跟大家一起分享下。有朋友私信我问项目的分层及文件夹结构在我的第一篇博客中没说清楚,那么接下来我就准备从这些文件怎么分文件夹说起。问题大概有以下几点:
1、项目层的文件夹结构
2、解决MVC的Controller和Web API的Controller类名不能相同的问题
3、给MVC不同命名空间的Area的注册不同的路由
4、让Web API路由配置也支持命名空间参数
5、MVC及Web API添加身份验证及错误处理的过滤器
6、MVC添加自定义参数模型绑定ModelBinder
7、Web API添加自定义参数绑定HttpParameterBinding
8、让Web API同时支持多个Get方法
正文

一、项目层的文件夹结构

这里的结构谈我自己的项目仅供大家参考,不合理的地方欢迎大家指出。第一篇博客中我已经跟大家说了下框架的分层及简单说了下项目层,现在我们再仔细说下。新建MVC或Web API时微软已经给我们创建好了许多的文件夹,如App_Start放全局设置,Content放样式等、Controller放控制器类、Model数据模型、Scripts脚本、Views视图。有些人习惯了传统的三层架构(有些是N层),喜欢把Model文件夹、Controller文件夹等单独一个项目出来,我感觉是没必要,因为在不同文件夹下也算是一种分层了,单独出来最多也就是编译出来的dll是独立的,基本没有太多的区别。所以我还是从简,沿用微软分好的文件夹。先看我的截图
这里写图片描述

我添加了区域Areas,我的思路是最外层的Model(已删除)、Controllers、Views都只放一些共通的东西,真正的项目放在Areas中,比如上图中Mms代表我的材料管理系统,Psi是另外一个系统,Sys是我的系统管理模块。这样就可以做到多个系统在一个项目中,框架的重用性不言而喻。再具体看区域中一个项目
这里写图片描述
这当中微软生成的文件夹只有Controllers、Models、Views。其它都是我建的,比如Common放项目共通的一些类,Reports准备放报表文件、ViewModels放Knouckoutjs的ViewModel脚本文件。
接下来再看看UI库脚本库引入的一些控件要放置在哪里。如下图
这里写图片描述
我把框架的css images js themes等都放置在Content下,css中放置项目样式及960gs框架,js下面core是自已定义的一些共通的js包括utils.js、common.js及easyui的knouckout绑定实现knouckout.bindings.js,其它一看就懂基本不用介绍了。

二、解决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的后缀,如图
这里写图片描述
我们只要把ApiController的后缀改成和MVC不一样,就可以解决问题了。这个字段是个静态只读的Field,我们只要把它改成ApiContrller就解决问题了。我们首先想到的肯定是反射。好吧,就这么做,在注册Api路由前添加以下代码即可完成

var suffix = typeof(DefaultHttpControllerSelector).GetField("ControllerSuffix", BindingFlags.Static | BindingFlags.Public);
 if (suffix != null) suffix.SetValue(null, "ApiController");

三、给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" }
);

其中第四个参数是命名空间参数,表示这个路由设置只在此命名空间下有效。

四、让Web API路由配置也支持命名空间参数

让人很头疼的是Web Api路由配置竟然不支持命名空间参数,这间接让我感觉它不支持Area,微软真会开玩笑。好吧我们还是自己动手。在google上找到一篇文章http://netmvc.blogspot.com/2012/06/aspnet-mvc-4-webapi-support-areas-in.html 貌似被墙了,这里有介绍一种方法替换HttpControllerSelector服务。
我直接把我的代码贴出来,大家可以直接用,首先创建一个新的HttpControllerSelector类

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个问题时会讲到,这里先略过。

五、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());

这样过滤器就定义好了。

六、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参数了。

七、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参数绑定已完成,可以使用了

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

展开阅读全文

没有更多推荐了,返回首页