上一篇文章《ASP.NET Core中使用默认MVC路由》提到了如何使用默认的MVC路由配置,通过这个配置,我们就可以把请求路由到Controller和Action,通常情况下我们使用默认的路由器就可以了。
但是有些情况下,我们需要创建自己的路由规则,不是简单的修改MVC路由模板这么简单,比如我们需要针对一些特定的URL做特殊处理,这种情况通常是我们需要兼容一些旧的URL,但是升级之后总不能不管吧,要们做跳转或者给用户一个友好提示等等。如果旧的URL是MVC的那种格式Controller/Action格式还好办,如果是webform格式的URL,比如xxx.aspx或者静态文件URL,比如bbb.html。这时候我们不能使用默认的MVC路由器来处理我们的请求,我们需要提供一个特定的Router,也可以为理解为路由处理器,如果请求的URL匹配得上,那么就交给一个特定的handler处理。
这里我们提到了Handler,实际上可以理解为Httphandler,没错就是它。在Webform里面,之前版本的MVC都一直存在,web请求最终都给交给一个特定的Handler处理。在Webform里面的handler是aspx的code behind文件,在MVC里面是Routehandler,之后各自实现自己的process方法。
好了不深入去看,我们接着上一篇的例子,创建自己的一个Router,这Router可以实现以下功能
1.这个Router的作用是兼容不存在的Url,对于这些不存在的Url,我们给出友好提示
2.我们还可以将某些特定URL的请求转交给MVC框架去处理,这里说的是转交,而不是直接让MVC路由去处理。
实现过程
1.在项目根目录下创建一个类,名字为LegacyRoute,实现IRouter接口,实现代码如下
public class LegacyRoute : IRouter
{
private readonly string[] _urls; public LegacyRoute(params string[] urls)
{
_urls = urls;
} public Task RouteAsync(RouteContext context)
{
var requestedUrl = context.HttpContext.Request.Path.Value.TrimEnd('/');
if (_urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase))
{
context.Handler = async ctx => {
var response = ctx.Response;
byte[] bytes = Encoding.ASCII.GetBytes($"This URL: {requestedUrl} is not available now");
await response.Body.WriteAsync(bytes, , bytes.Length);
};
}
return Task.CompletedTask;
} public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return null;
}
}
上述代码实现了IRouter的两个接口,这两个接口方法的简单介绍一下
RouteAsync:这个处理请求的关键方法,有请求过来的时候,并且URL能匹配的上,系统会调用这个Router进行匹配,看能否处理这个请求,如果能处理则给出响应。
比如这个例子,Router保存了一些需要兼容的旧的Url,如果请求过来的Url包含在里面,那么直接Repsonse输出结果。
处理的方式是创建一个Handler的实例给RouteContext,这个Handler的类型是RequestDelegate,是一个委托,这个实例可以理解为一个中间件实例,参考《ASP.NET Core中Middleware的使用》里面的说明。
GetVirtualPath:这个方法是返回用户能看到的URL路径,比如在cshtml里面调用Url.Action得到的Url,会调用这个方法获取对应的URL,这个方法一会再讲讲如何实现。
2.定义好了Router之后,然后就是应用到特定的Url,路由配置当然也是在Startup里面完成。
由于我们要把自定义的Router加入到当前的Routes集合,所以之前使用的简化版UseMvcWithDefaultRoute需要改成如下方式
app.UseMvc(routes =>
{
routes.Routes.Add(new LegacyRoute(
"/articles/aspwinform.html",
"/old/mvc3"));
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
这里自定义的LegacyRoute要放在MVC的路由之前,否则会被MVC的路由提前拦截。
LegacyRoute的构造方法里提供了两个需要兼容的旧的Url,这时候启动项目,并且输入这两个URL会给出一段文字提示。
如果输入其他不存在的路径,那么还是返回404错误的
3 这时这个Router只是简单的修改Response内容,那如果需要返回更多的信息咋办,不能在这方寸方法里写大堆html代码吧,自己定义一套模板模式,
那几乎又是重写了一套逻辑。MVC的模板已经很强大了,那么我们完全可以把这个路由接收到请求再次转交给MVC去处理,这样就能用到高达上的Razor模板了。
我们先新建一个Controller,名字为LegacyController,增加一个简单的Action
public class LegacyController : Controller
{
public ViewResult GetLegacyUrl(string legacyUrl)
=> View("GetLegacyUrl", legacyUrl);
}
然后在Views文件夹创建Legacy目录,在Legacy目录创建GetLegacyUrl.cshtml文件,然后给一些内容文字什么的,具体不给截图了,都很简。
@model string
@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Routing</title>
</head>
<body class="panel-body">
<h2>GetLegacyURL</h2>
This URL: @Model is not available now
</body>
</html>
创建这两个文件是为了后面操作做铺垫。
紧接着修改LegacyRoute为如下代码
public class LegacyRoute : IRouter
{
private readonly string[] _urls;
private readonly IRouter _mvcRoute; public LegacyRoute(IServiceProvider services, params string[] urls)
{
_urls = urls;
_mvcRoute = services.GetRequiredService<MvcRouteHandler>();
} public async Task RouteAsync(RouteContext context)
{
var requestedUrl = context.HttpContext.Request.Path.Value.TrimEnd('/');
if (_urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase))
{
context.RouteData.Values["controller"] = "Legacy";
context.RouteData.Values["action"] = "GetLegacyUrl";
context.RouteData.Values["legacyUrl"] = requestedUrl;
await _mvcRoute.RouteAsync(context);
}
} public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return null;
}
}
主要改动是注入了IServiceProvider,这是Core里面用于查找服务的Provider,用它实现service locate功能,查找我们需要的服务组件,这是IOC的知识点,先不详述。
引入IServiceProvider主要是为了得到MvcRouteHandler这个服务组件。
设置contexnt的RouteData的数据,然后将整个context实例交给MvcRouteHandler的实例去处理,这样就顺利将某些特定旧Url的请求转交给了Mvc去处理。
LegacyRoute的构造方法改了,那么在Startup里面也要调整,要提供IServiceProvider的实例,还好通过IApplicationBuilder的实例就可以轻松获得
routes.Routes.Add(new LegacyRoute(
app.ApplicationServices,
"/articles/aspwinform.html",
"/old/mvc3"));
4 到这一步的时候,看起来已经可以处理某些特定的URL的请求。但是LegacyRoute里面还有一个方法没有实现,那就是GetVirtualPath,这是干嘛的呢,实际上就是用于生成对外显示的URL路径。
使用如下代码完善GetVirtualPath方法
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context.Values.ContainsKey("legacyUrl"))
{
var url = context.Values["legacyUrl"] as string;
if (_urls.Contains(url))
{
return new VirtualPathData(this, url);
}
}
return null;
}
这个代码是判断是否在路由参数里提供了legacyUrl参数,如果有则进一步处理,并返回一个VirtualPathData实例。
说这么多可能不太好理解,实际上这个用在cshtml里面就明白了
在页面里创建一个a标记,使用如下代码
<a asp-route-legacyurl="/old/mvc3">/old/mvc3</a> (由于这里用到了TagHelper,所以还必须做一些处理,具体参考源代码的配置)
那么生成的的Html代码如下
<a href="/old/mvc3">/old/mvc3</a>
看起来比较的多余,跟直接写死/old/mvc3路径一样效果,但是走的路径不同,如果修改了路由规则,那么通过asp-route-*方式设置的路径也会跟着修改。
到这里基本实现了自定义的路由,基本的思路就是这样,当然能做跟多复杂的事情,甚至定义一个完整的路由规则。
完整代码示例可以从以下路径下载
https://github.com/shenba2014/AspDotNetCoreMvcExamples/tree/master/CustomRouter