新版routing功能介绍
在asp.net 5和mvc6中,routing功能被全部重写了,虽然用法有些类似,但和之前的routing原理完全不太一样了,该routing框架不仅可以支持mvc和web api,还支持一般的asp.net5程序。新版的改变有如下几个部分。
首先,routing系统是基于asp.net 5的,是一个独立于mvc的路由框架,而不是基于mvc的。mvc只是在上面扩展了一个快捷方式而已。
其次,在asp.net 5中,mvc和web api控制器没有区别了,即合二为一了。两者派生于同一个controller基类。也就是说该routing框架是适用于两者的,适用于mvc则意味着也适用于web api。
最后,不管在基于约定的route声明还是基于attribute的route声明,都可以使用内联约束和参数选项。例如,你可以约定路由中某个参数的数据类型,也可以让一个参数标记为可选类型,再或者给其提供一个默认值。
routing框架的主要流程
基本的routing框架是基于middleware来实现的,这样就可以将其添加到http的请求pipeline中了,它可以喝其它任意middleware一起进行组合使用,如静态文件处理程序、错误页、或者signalr服务器。
在使用routing框架之前,首要要了解routing的作用,作用很简单:
对于http请求,routing系统负责找出与之匹配的route,创建route数据,并将该请求派送到该route对于的处理程序(handler)上。controller和action的选择,只是mvc的handler的一个具体实现,该实现使用route数据和http请求中的其它信息来选择要执行的controller和action。在新版的mvc6中,该处理程序的名称为mvcroutehandler。
路由系统的执行流程如下:
asp.net 5监听到一个http请求。然后routing middleware就会尝试将route集合中的route匹配该请求。一旦成功匹配一个请求,就找出该route对应的handler。调用该handler上的routeasync方法(因为所有的handler都要实现该接口方法)。routingcontext有一个ishandled标记,如果该标记设置为true,则意味着该请求已经被这个handler成功处理了;如果设置为false,则意味着该handler无法处理该请求,系统会再为此匹配一个route。
和之前的routing系统有点不同的是,老版的routing系统一旦成功匹配一个路由,就将其交由其对应的handler,不管对应的handler能不能处理该请求,所以就会出现route匹配成功了,但是找不到对应的action,此时就会出现404错误,而新版对此作出了上述第4步骤的改进(重新将控制权交回给routing系统,进行重新匹配),看起来还是非常不错的。
route参数和约束条件的改进
在之前的route设置中,要约束一个参数的数据类型的话,我们需要使用类型如下代码:
routes.maproute(
"product",
"product/{productid}",
defaults: new { controller = "product", action = "details" },
constraints: new { productid = @"\d+" });
而在新版route中,就可以直接设置product/{productid:int}了,约束条件遵守如下约定:
{parameter:constraint}
目前支持的约束如下:
| 约束 | 示例 | 说明 |
|---|---|---|
| required | "product/{productname:required}" | 参数必选 |
| alpha | "product/{productname:alpha}" | 匹配字母,大小写不限 |
| int | "product/{productid:int}" | 匹配int类型 |
| long | "product/{productid:long}" | 匹配long类型 |
| bool | "product/{productid:bool}" | 匹配bool类型 |
| double | "product/{productid:double}" | 匹配double类型 |
| float | "product/{productid:float}" | 匹配float类型 |
| guid | "product/{productid:guid}" | 匹配guid类型 |
| decimal | "product/{productid:decimal}" | 匹配decimal类型 |
| datetime | "search/{datetime:datetime}" | 匹配datetime类型 |
| composite | "product/{productid:composite}" | 匹配composite类型 |
| length | "product/{productname:length(5)}" | 长度必须是5个字符 |
| length | "product/{productname:length(5, 10)}" | 长度在5-10个之间 |
| maxlength | "product/{productid:maxlength(10)}" | 最大长度为10 |
| minlength | "product/{productid:minlength(3)}" | 最小长度为3 |
| min | "product/{productid:min(3)}" | 大于等于3 |
| max | "product/{productid:max(10)}" | 小于等于10 |
| range | "product/{productid:range(5, 10)}" | 对应的数组在5-10之间 |
| regex | "product/{productid:regex(^\d{4}$)}" | 符合指定的正则表达式 |
而对于可选参数,则值需要在约束类型后面加一个问号即可,示例如下:
routes.maproute(
"product",
"product/{productid:long?}",
new { controller = "product", action = "details" });
如果参数是必填的,需要保留一个默认值的话,则可以按照如下示例进行设置:
routes.maproute(
"product",
"product/{productid:long=1000}",
new { controller = "product", action = "details" });
通用routing
关于示例使用,我们先不从mvc开始,而是先从普通的routing使用方式开始,新版route添加的时候默认添加的是templateroute实例,并且在该实例实例化的时候要设置一个handler。
举例来说,我们先创建一个空的asp.net 5项目,并在project.json文件的dependencies节点中添加程序集"microsoft.aspnet.routing": "1.0.0-beta3",,在startup.cs的configure方法里添加如下代码:
public void configure(iapplicationbuilder app)
{
routecollection routes = new routecollection();
routes.add(new templateroute(new debuggerroutehandler("routehandlera"), "", null));
routes.add(new templateroute(new debuggerroutehandler("routehandlerb"), "test/{a}/{b:int}", null));
routes.add(new templateroute(new debuggerroutehandler("routehandlerc"), "test2", null));
app.userouter(routes); // 开启routing功能
}
在这里,我们设置http请求处理的的handler为debuggerroutehandler,该类继承于irouter,实例代码如下:
public class debuggerroutehandler : irouter
{
private string _name;
public debuggerroutehandler(string name)
{
_name = name;
}
public string getvirtualpath(virtualpathcontext context)
{
throw new notimplementedexception();
}
public async task routeasync(routecontext context)
{
var routevalues = string.join("", context.routedata.values);
var message = string.format("{0} values={1} ", _name, routevalues);
await context.httpcontext.response.writeasync(message);
context.ishandled = true;
}
}
上述类,继承irouter以后,必须实现一个routeasync的方法,并且如果处理成功,则将ishandled设置为true。
访问如下网址即可查看相应的结果:
正常:`http://localhost:5000/` 正常:`http://localhost:5000/test/yyy/12` 404 :`http://localhost:5000/test/yyy/s` 正常:`http://localhost:5000/test2` 404 :`http://localhost:5000/test3`
注意:templateroute和debuggerroutehandler都继承于irouter,是实现前面所述的不出现404错误(继续匹配下一个路由)的核心。
mvc中的routing
在mvc示例程序中,我们只需要配置在调用app.usemvc方法的时候,使用委托中的maproute方法来定义各种route就可以了。在这里我们以空白项目为例,来看看mvc的route如何使用。
第一步:在project.json文件的dependencies节点中引用程序集"microsoft.aspnet.mvc": "6.0.0-beta3",
第二部:添加mvc的middleware,并使用mvc,然后添加一条默认的路由,代码如下:
public void configureservices(iservicecollection services)
{
services.addmvc();
}
public void configure(iapplicationbuilder app)
{
app.usemvc(routebuilder =>
{
routebuilder.maproute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "home", action = "index" });
});
}
第三步:分别创建如下如下三种controller,其中productscontroller继承于microsoft.aspnet.mvc下的controller。
public class productscontroller : controller
{
public iactionresult index()
{
return content("it works with controller base class!");
}
}
public class democontroller
{
public iactionresult index()
{
return new objectresult("it works without controller base class!");
}
}
public class apicontroller
{
public object index()
{
return new { code = 100000, data = "ok" };
}
}
访问http://localhost:5000/products和http://localhost:5000/demo,均能显示正常的输出结果;而访问http://localhost:5000/api的时候返回的则是json数据。
这就是我们在前面asp.net5新特性中所讲的mvc和api合二为一了,并且也可以不继承于controller基类(但类名要以controller结尾)。这种技术的核心是controller的查找机制,关于如何在一个项目中查找合适的程序集,请参考《controller与action》章节。
新版mvc在判定controller的时候,有2个条件:要么继承于controller,要么是引用mvc程序集并且类名以controller结尾。
所以,在创建mvc controller和web api controller的时候,如果你不需要相关的上下文(如httpcontext、actioncontext等)的话,则可以不必继承于controller基类;但推荐都继承于controller,因为可以多多利用基类的方法和属性,因为不管继承不继承,你定义的所有controller类都要走mvc的各个生命周期,我们通过actionfilter来验证一下:
第一步:在project.json文件的dependencies节点中引用程序集"microsoft.aspnet.server.weblistener": "1.0.0-beta3"。
第二步:创建一个aciton filter,分别在action执行前和执行后输出一行文字,代码如下:
public class actionfiltertest : iactionfilter
{
public void onactionexecuting(actionexecutingcontext context)
{
var typename = context.controller.gettype().fullname;
console.writeline(typename + "." + context.actiondescriptor.name + ":start");
}
public void onactionexecuted(actionexecutedcontext context)
{
var typename = context.controller.gettype().fullname;
console.writeline(typename + "." + context.actiondescriptor.name + ":end");
}
}
第三步:在configureservices方法里注册该action filter。
services.configure<mvcoptions>(options =>
{
options.filters.add(typeof(actionfiltertest));
});
运行程序,并访问响应的路径,三种类型的代码均会按计划输出内容,输出内容如下:
routertest.productscontroller.index:start routertest.productscontroller.index:end routertest.democontroller.index:start routertest.democontroller.index:end routertest.apicontroller.index:start routertest.apicontroller.index:end
普通的asp.net5程序和mvc程序是可以在一起混合使用routing功能的。
自定义route
asp.net 5和mvc6都提供了丰富的route自定义功能,关于普通route的自定义,可以参考前面小节的debuggerroutehandler,这种方式需要实现自己的http输出,相当于原来轻量级的ihttphandler一样。本节,我们将这种在基于mvc的route自定义功能,即定义的route的handler处理程序都是mvcroutehandler。
在之前版本的mvc中,要自定义route,一般都是继承于routebase基类或route类;而在新版的mvc6中,要实现自定义route,有三种方式,分别如下:
继承于templateroute实现irouter实现inamedrouter(注:inamedrouter和irouter的唯一区别是多了一个名称)
本例中,我们以继承继承于templateroute为例,首先创建一个继承于该类的子类promotemplateroute,该类只匹配/promo目录下的路径。
public class promotemplateroute : templateroute
{
public promotemplateroute(irouter target, string routetemplate, iinlineconstraintresolver inlineconstraintresolver)
: base(target, routetemplate, inlineconstraintresolver: inlineconstraintresolver)
{
}
public promotemplateroute(irouter target,
string routetemplate,
idictionary<string, object> defaults,
idictionary<string, object> constraints,
idictionary<string, object> datatokens,
iinlineconstraintresolver inlineconstraintresolver)
: base(target, routetemplate, defaults, constraints, datatokens, inlineconstraintresolver)
{
}
public promotemplateroute(irouter target,
string routename,
string routetemplate,
idictionary<string, object> defaults,
idictionary<string, object> constraints,
idictionary<string, object> datatokens,
iinlineconstraintresolver inlineconstraintresolver)
: base(target, routename, routetemplate, defaults, constraints, datatokens, inlineconstraintresolver)
{ }
public async override task routeasync(routecontext context)
{
var requestpath = context.httpcontext.request.path.value ?? string.empty;
if (!requestpath.startswith("/promo", stringcomparison.ordinalignorecase))
{
return;
}
await base.routeasync(context);
}
}
为了方便使用,我们也比葫芦画瓢,创建一些扩展方法,示例如下:
public static class routebuilderextensions
{
public static iroutebuilder mappromoroute(this iroutebuilder routecollectionbuilder, string name, string template)
{
mappromoroute(routecollectionbuilder, name, template, defaults: null);
return routecollectionbuilder;
}
public static iroutebuilder mappromoroute(this iroutebuilder routecollectionbuilder, string name, string template, object defaults)
{
return mappromoroute(routecollectionbuilder, name, template, defaults, constraints: null, datatokens: null);
}
public static iroutebuilder mappromoroute(this iroutebuilder routecollectionbuilder, string name, string template, object defaults, object constraints, object datatokens)
{
var inlineconstraintresolver = routecollectionbuilder.serviceprovider.getservice<iinlineconstraintresolver>();
routecollectionbuilder.routes.add(
new promotemplateroute(
routecollectionbuilder.defaulthandler,
name,
template,
objecttodictionary(defaults),
objecttodictionary(constraints),
objecttodictionary(datatokens),
inlineconstraintresolver));
return routecollectionbuilder;
}
private static idictionary<string, object> objecttodictionary(object value)
{
var dictionary = value as idictionary<string, object>;
if (dictionary != null)
{
return dictionary;
}
return new routevaluedictionary(value);
}
}
使用的时候,则很简单,和之前的方式非常类似,示例如下:
routes.mappromoroute(
name: "default2",
template: "promo/{controller}/{action}/{id?}",
defaults: new { controller = "home", action = "index" });
通过这种方式,我们可以在符合路由匹配条件的时候,使用promotemplateroute类来处理一些自定义逻辑,比如添加一些额外的文件头信息等等。
基于attribute的routing
基于attribute的routing功能一直是mvc所期待的功能,在web api已经通过routeprefix(controller上使用)和route(action上使用)来实现了。该特性在mvc 6中进行了重写和增强,并且由于mvc和web api合二而一了,所以在这两种controller上都可以使用该特性。
举例来说:
[route("bookhome")]
public class homecontroller : controller
{
public iactionresult index()
{
return view();
}
[route("about")]
public iactionresult about()
{
viewbag.message = "your application description page.";
return view();
}
[route("contactus")]
public iactionresult contact()
{
viewbag.message = "your contact page.";
return view();
}
}
在上述controller上定义一个bookhome前缀,并且在about和contact上又分别定义了action名称,所以上述3个action的访问地址则是如下这种形式:
/bookhome /bookhome/about /bookhome/contactus
在这里,我们需要注意,controller和action使用的attribute都是route,同时,在这些路由模板字符串中,依然可以使用内联参数,比如,我们可以定义类似这样的路由:
[route("products/{productid:int}")]
controller和action标记位
另外,针对route的模板字符串,不仅支持内联参数,还支持controller和action的标记位,即不用写死该controller或action的名称,使用一个[controller]或[action]的字符即可表示该controller或action的名称。比如,我们可以在controller上定义这样的一个路由(action上什么都不定义):
[route("book/[controller]/[action]")]
这样访问首页的地址就变成了:/book/home/index。
web api的等价route定义
在web api中,我们一般还要定义get、post这样的请求方式,为了方便,新版的httpget等一系列方法都集成了route功能,直接在构造函数传入route模板即可,示例如下:
[httpget("products/{productid:int}")]
上述route的定义,即表明,既要符合products/{productid:int}的路由规则,又要是get请求。
其实httpget这一系列attribute也可以在普通的mvc controller上使用,因为在mvc6中,mvc controller和web api controller本身就是同一个东西,只不过mvc的返回类型都是iactionresult而已。route定义,不仅仅支持get请求,还支持post等其它类型的请求,即不限制请求方式。在httpxxx系列特性中,也是支持内联参数和[controller]、[action]标记位的,大可放心使用。目前可用的特性类有:httpget、httppost、httpput、httpdelete、httppatch。
非要重要route定义规则
基于attribute的route定义很方便,但也很危险,具体规则和危险性如下。
规则1:controller上定义了route特性很危险
一旦在controller上定义了route特性,该controller下的所有路由规则都不受其它规则控制了,比如,如果你定义了类似这样的
[route("book")]
public class homecontroller : controller
{
public iactionresult index()
{
return view();
}
public iactionresult about()
{
viewbag.message = "your application description page.";
return view();
}
}
那么,上述2个action你都再也没办法访问了,因为默认的action的名称根本就不会起作用,即/book/index和/book/about这两个路径无法路由到对应的action方法上。而且/book也访问不了,因为有两个以上的action,系统无法定位到其中一个action上。
所以要让上述action能访问,必须要在其中一个action上定义再route,例如:
[route("book")]
public class homecontroller : controller
{
public iactionresult index()
{
return view();
}
[route("about")]
public iactionresult about()
{
viewbag.message = "your application description page.";
return view();
}
}
这样,就可以通过/book/about来访问about方法了,而访问/book则可以访问默认的index方法了,因为该index方法是默认唯一一个没有定义路由的方法,所以他就是/book路由规则的默认action。如果,有3个action的话,则必须要至少给两个action定义route,示例如下:
[route("book")]
public class homecontroller : controller
{
[route("index")]
public iactionresult index()
{
return view();
}
[route("about")]
public iactionresult about()
{
viewbag.message = "your application description page.";
return view();
}
public iactionresult contact()
{
viewbag.message = "your contact page.";
return view();
}
}
此时,contact方法就是默认/book路由的action了,访问/book路径的话,就会显示contact对应的页面。
规则2:route和httpget可以一起使用,但也很危险
我们前面提到,在action上即可以使用route特性,也可以使用httpget特性,两者之间的不同,就是多了一个http method。很多同学可以要问两个特性在一起使用的时候会有问题么?
其实,这两个特性是可以在一起使用的,示例如下:
[route("book")]
public class homecontroller : controller
{
[route("contact")]
[httpget("home/contact2")]
public iactionresult contact()
{
viewbag.message = "your contact page.";
return view();
}
}
这样/book/contact和/book/home/contact2这两个网址,都可以访问了。但如果这里定义httpget,情况就不一样了,示例如下:
[route("contact")]
[httppost("home/contact2")]
此时,访问该action的方式,要么是以get的方式访问/book/contact地址,要么是以post的方式访问/book/home/contact2。所以为了避免出错,建议使用的时候不要讲两者混用,即便是要同时支持get和post,那也是建议用同类型的httpxxx来定义这些路由,例如:
[httpget("contact")]
[httppost("home/contact2")]
这样,看起来就清晰多了。
规则3:多个route和多个httpxxx也可以一起使用,但也很危险
在如下示例中,我们为homecontroller定义了2个route特性,而contact定义了2个route特性和1个httppost特性。
[route("book")]
[route("tom")]
public class homecontroller : controller
{
[route("contact")]
[route("contactus")]
[httppost("home/contact2")]
public iactionresult contact()
{
viewbag.message = "your contact page.";
return view();
}
}
那么,在上述代码生效后,我们将有六种访问来访问该action,这六种方式分布如下:
get:/book/contact get:/book/contactus get:/tom/contact get:/tom/contactus post:/book/home/contact2 post:/tom/home/contact2
但是,在视图文件中,通过@html.actionlink("contact", "contact", "home")生成链接地址的话,则默认会使用第一个定义的route,如果要强制指定顺序,则可以使用order属性来定义排序值,默认会优先使用最小的值。示例如下:
[route("book", order = 1)]
[route("tom", order = 0)]
public class homecontroller : controller
{
[route("contact", order = 1)]
[route("contactus", order = 0)]
[httppost("home/contact2", order = 2)]
public iactionresult contact()
{
viewbag.message = "your contact page.";
return view();
}
}
自定义内联参数约束
在前面的介绍中,我们知道任意类型的路由在定义的时候都支持不同的内联参数约束,因为这些约束是基于asp.net 5的,而不是基于mvc6的,并且这些约束还是可以扩展的,本节我们就来看看如何自定义一些扩展。
无参数约束
首先,我们来看一个比较简单的约束,即无参数约束,类似于{productid:int}这样的类型约束,假设我们要实现一个aabbcc字符串限定的约束,示例如下:
[route("index/{productid:aabbcc}")]
为了确保/index/112233和/index/aabbcc是符合约束的,而/index/aabbccdd是不符合约束的,我们首先要自定义一个约束类aabbccrouteconstraint,并实现irouteconstraint接口,示例如下:
public class aabbccrouteconstraint : irouteconstraint
{
public bool match(httpcontext httpcontext, irouter route, string routekey, idictionary<string, object> values, routedirection routedirection)
{
bool b = false;
object value;
if (values.trygetvalue(routekey, out value) && value != null)
{
if (value is string) // 获取传入的值,比如aabbcc或112233
{
string aabbcc = value.tostring();
b = !string.isnullorwhitespace(aabbcc) && aabbcc.length == 6 && aabbcc[0] == aabbcc[1] && aabbcc[2] == aabbcc[3] && aabbcc[4] == aabbcc[5];
}
}
return b;
}
}
在该实现类中,要实现match方法,根据传入的各种参数,判断是否符合定义的约束,并返回true或false,match方法的参数中,其中routekey是约束{productid:aabbcc}对应的参数名称(本例中是productid),values集合中会有该productid所对应的数字(如112233),在该方法通过响应的判断返回true和false。
下一步,就是要将该约束类注册到routing系统的约束集合中,在startup.cs的configureservices方法中,执行如下语句:
services.configure<routeoptions>(opt =>
{
opt.constraintmap.add("aabbcc", typeof(aabbccrouteconstraint));
});
注意,这里注册的aabbcc就是前面我们所指定约束名称,完成上述步骤以后,即可实现类似{productid:int}的功能了。
有参数约束
一般情况下,有些时候可能需要定义一些约束的值,比如length(1,10)来表示1-10之间的字符串长度,举例来说,加入我们要定义一个4个参数的约束规则,如abcd(1,10,20,30)来表示一个特殊的验证项,则需要声明有4个参数的构造函数,示例如下:
public class abcdrouteconstraint : irouteconstraint
{
public int a { get; private set; }
public int b { get; private set; }
public int c { get; private set; }
public int d { get; private set; }
public abcdrouteconstraint(int a, int b, int c, int d)
{
a = a;b = b;c = c;d = d;
}
public bool match(httpcontext httpcontext, irouter route, string routekey, idictionary<string, object> values, routedirection routedirection)
{
bool b = false;
object value;
if (values.trygetvalue(routekey, out value) && value != null)
{
var valuestring = value.tostring();//这里需要进行进一步的验证工作
return true;
}
return b;
}
}
假如你在action上了定义了如下约束:
[route("index/{productid:abcd(1,20,30,40)}")]
那么,在注册该约束类型以后,系统启动厚扫描所有的route进行注册的时候,会分析你定义的这4个值,然后会将这4个值赋值给该路由对应的约束实例上的a、b、c、d四个属性上,以便在http请求过来的时候,分析url上的值,看是否符合match里定义的规则(在验证的时候就可以使用这4个属性值)。
默认约束的所有代码可以参考: https://github.com/aspnet/routing/tree/dev/src/microsoft.aspnet.routing/constraints
另外,如果定义了4个参数的约束,那么在action上定义路由的时候则必须符合参数的数据类型,如果不符合,系统启动的时候就会出错,示例错误如下:
[route("index/{productid:abcd}")] //没有为该对象定义无参数的构造函数
[route("index/{productid:abcd(a)}")]
[route("index/{productid:abcd('a')}")] //输入字符串的格式不正确
[route("index/{productid:abcd(1,2,3)}")] //构造函数的参数个数和定义的参数个数不一致。
如果你定义的参数类型是字符串类型,则下面2种形式的定义都是合法的:
[route("index/{productid:abcd(a,b,c,d)}")]
[route("index/{productid:abcd('a','b','c','d')}")]
虽然asp.net 5 和mvc6的路由使用方式很简单,但是相关的使用规则却很复杂,大家使用的时候需要多加注意。
发表评论