ASP.NET Core中使用自定义路由

上一篇文章《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, 0, 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

posted @ 2017-02-07 23:07  神八  阅读(16906)  评论(0编辑  收藏  举报