一起谈.NET技术,ASP.NET MVC:自定义 Route 以生成小写的 Url

简介:   先给出本文中测试用的 controller:public class PersonsController : Controller{public ActionResult Query(string name) {return View(); }}  ASP.

  先给出本文中测试用的 controller:

public class PersonsController : Controller
{
public ActionResult Query(string name)
{
return View();
}
}

  ASP.NET 中 Url 大小写

  不严格来讲,ASP.NET MVC 对 Url 是不敏感的,以下 Url 都是相同的,都可以访问到 PersonController 的 Query 方法:

  1. ~/Persons/Query
  2. ~/PERSONS/QUERY
  3. ~/persons/query

  但对 MVC 的数据绑定来说,大小写似乎还是有区别的:

  1. ~/Persons/Query?Name=Bob
  2. ~/Persons/Query?Name=bob

  对以上两个 Url,Query 中 name 参数会接收到两个不同值:Bobbob。Action 中的参数只是原样接收,并没有作任何处理。至于name 字符串的大小写是否敏感要看具体的应用了。

  再回头看前面的三个 Url:

  1. ~/Persons/Query: 是 MVC 中默认生成的,因为在 .Net 中方法命名通常采用 PascalCase;
  2. ~/PERSONS/QUERY: 全部大写,这种写法很不友好,很难读,应该杜绝采用这种方式;
  3. ~/persons/query:这种方式比较好,易读,也是大多数人选择的方式。

  本文探讨如何在 MVC 中使用第三种方式,也就是小写(但不完全小写),目标如下:

  在不影响程序正常运行的前提下,将所有能小写的都小写,如:

  ~/persons/query?name=Bob&age=18

  ~/persons/query/name/Bob/age/18

  MVC 中 Url 的生成

  在 View 中生成超级链接有多种方式:

<%: Html.ActionLink("人员查询", "Query", "Persons", new { name = "Bob" }, null) %>
<%: Html.RouteLink("人员查询", new { controller = "Persons", action = "Query", name = "Bob" })%>
<a href="<%:Url.Action("Query", "Persons", new { name="Bob" }) %>">人员查询</a>

  在 Action 中,可以使用 RedirectTo 来调转至新的页面:

return RedirectToAction("Query", "Persons", new { name = "Bob" });
return RedirectToRoute(new { controller = "Persons", action = "Query", name = "Bob" });

  ActionLink、RouteLink、RedirectToAction 和 RedirectToRouter 都会生成 Url,并最终显示在浏览器的地址栏中。

  这四个方法都有很多重载,想从这里下手控制 Url 小写实在是太麻烦了。当然也不可行,因为可能还有其它方式来生成 Url。

  MVC 是一个非常优秀的框架,但凡优秀的框架都会遵循 DRY(Don't repeat yourself) 原则,MVC 也不例外。MVC 中 RouteBase 负责 Url 的解析和生成:

public abstract class RouteBase
{
public abstract RouteData GetRouteData(HttpContextBase httpContext);
public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
}

  GetRouteData 用来解析 Url,GetVirtualPath 用来生成 Url。ActionLink、RouteLink、RedirectToAction 和 RedirectToRouter 内部都会调用 GetVirtualPath 方法来生成 Url。

  因此我们的入手点就是 GetVirtualPath 方法。

  自定义 Route 以生成小写的 Url

  MVC 中 RouteBase 的具体实现类是 Route,我们经常在 Global.asax 中经常使用:

public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
//...
}

  MapRoute 返回 Route,MapRoute 有很多重载,用来简化我们构建 Route 的过程。

  Route 类没有给我们提供可直接扩展的地方,因此我们只能自定义一个新的 Route 来实现我们的小写 Url。但处理路由的细节也是相当麻烦的,因此我们最简单的方式就是写一个继承自 Route 的类,然后重写它的 GetVirtualPath 方法:

public class LowerCaseUrlRoute : Route
{
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//在此处进行小写处理
return base.GetVirtualPath(requestContext, values);
}
}

  再来看下我们的目标:

  ~/persons/query?name=Bob&age=18

  ~/persons/query/name/Bob/age/18

  其实我们只需要进行两步操作:

  1. 将路由中的 area、controller、action 的值都变成小写;
  2. 将路由中其它键值对的键变成小写,如:Name=Bob 中的 Name。

  那我们先来完成这个功能吧:

private static readonly string[] requiredKeys = new [] { "area", "controller", "action" };

private void LowerRouteValues(RouteValueDictionary values)
{
foreach (var key in requiredKeys)
{
if (values.ContainsKey(key) == false) continue;

var value = values[key];
if (value == null) continue;

var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
if (valueString == null) continue;

values[key] = valueString.ToLower();
}

var otherKyes = values.Keys
.Except(requiredKeys, StringComparer.InvariantCultureIgnoreCase)
.ToArray();

foreach (var key in otherKyes)
{
var value = values[key];
values.Remove(key);
values.Add(key.ToLower(), value);
}
}

  GetVirtualPath 生成 Url 时,会将 requestContext.RouteData.Values、values(第二个参数) 以及 Defaults(当前 Router 的默认值)三个 RouteValueDictionary 进行合并,如在 View 写了如下的一个 ActionLinks:

<%: Html.ActionLink("查看") %>

  生成的 Html 代码可能是:

<a href="/Home/Details">查看</a>

  因为没有指定 Controller,MVC 会自动使用当前的,即从 requestContext.RouteData.Values 中获取 Controller,得到 ”Home“;”Details“来自 values;如果连 ActionLink 中 Action 也不指定,那将会从 Defaults 中取值。

  因此我们必须将这三个 RouteValueDictionary 都进行处理才能达到我们的目标:

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
LowerRouteValues(requestContext.RouteData.Values);
LowerRouteValues(values);
LowerRouteValues(Defaults);
return base.GetVirtualPath(requestContext, values);
}

  再加上几个构造函数,完整的 LowerCaseUrlRoute 如下:

public class LowerCaseUrlRoute : Route
{
private static readonly string[] requiredKeys = new [] { "area", "controller", "action" };

public LowerCaseUrlRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler) { }

public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler){ }

public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
: base(url, defaults, constraints, routeHandler) { }
public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
: base(url, defaults, constraints, dataTokens, routeHandler) { }

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
LowerRouteValues(requestContext.RouteData.Values);
LowerRouteValues(values);
LowerRouteValues(Defaults);
return base.GetVirtualPath(requestContext, values);
}

private void LowerRouteValues(RouteValueDictionary values)
{
foreach (var key in requiredKeys)
{
if (values.ContainsKey(key) == false) continue;

var value = values[key];
if (value == null) continue;

var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
if (valueString == null) continue;

values[key] = valueString.ToLower();
}

var otherKyes = values.Keys
.Except(requiredKeys, StringComparer.InvariantCultureIgnoreCase)
.ToArray();

foreach (var key in otherKyes)
{
var value = values[key];
values.Remove(key);
values.Add(key.ToLower(), value);
}
}
}

  有了 LowerCaseUrlRoute,我们就可以修改 Global.asax 文件中的路由了。

  创建 LowerCaseUrlRouteMapHelper

  这一步不是必须的,但有了这个 MapHelper 我们在修改 Global.asax 文件中的路由时可以非常方便:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapLowerCaseUrlRoute( //routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}

  尤其是已经配置了很多路由的情况下,其代码如下:

public static class LowerCaseUrlRouteMapHelper
{
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url){
return routes.MapLowerCaseUrlRoute(name, url, null, null);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults){
return routes.MapLowerCaseUrlRoute(name, url, defaults, null);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, string[] namespaces){
return routes.MapLowerCaseUrlRoute(name, url, null, null, namespaces);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, object constraints){
return routes.MapLowerCaseUrlRoute(name, url, defaults, constraints, null);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces){
return routes.MapLowerCaseUrlRoute(name, url, defaults, null, namespaces);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces){
if (routes == null) throw new ArgumentNullException("routes");
if (url == null) throw new ArgumentNullException("url");
LowerCaseUrlRoute route2 = new LowerCaseUrlRoute(url, new MvcRouteHandler());
route2.Defaults = new RouteValueDictionary(defaults);
route2.Constraints = new RouteValueDictionary(constraints);
route2.DataTokens = new RouteValueDictionary();
LowerCaseUrlRoute item = route2;
if ((namespaces != null) && (namespaces.Length > 0))
item.DataTokens["Namespaces"] = namespaces;
routes.Add(name, item);
return item;
}

public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url){
return context.MapLowerCaseUrlRoute(name, url, null);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults){
return context.MapLowerCaseUrlRoute(name, url, defaults, null);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, string[] namespaces){
return context.MapLowerCaseUrlRoute(name, url, null, namespaces);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, object constraints) {
return context.MapLowerCaseUrlRoute(name, url, defaults, constraints, null);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, string[] namespaces){
return context.MapLowerCaseUrlRoute(name, url, defaults, null, namespaces);
}
public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, object constraints, string[] namespaces)
{
if ((namespaces == null) && (context.Namespaces != null))
namespaces = context.Namespaces.ToArray<string>();
LowerCaseUrlRoute route = context.Routes.MapLowerCaseUrlRoute(name, url, defaults, constraints, namespaces);
route.DataTokens["area"] = context.AreaName;
bool flag = (namespaces == null) || (namespaces.Length == 0);
route.DataTokens["UseNamespaceFallback"] = flag;
return route;
}
}

  总结

  大功告成,如果你感兴趣,不妨尝试下!写到这里吧,如果需要,请下载本文中的示例代码:MvcLowerCaseUrlRouteDemo.rar(209KB)如果你有其它办法,欢迎交流!

目录
相关文章
|
5月前
|
监控 Cloud Native 测试技术
.NET技术深度解析:现代企业级开发指南
每日激励:“不要一直责怪过去的自己,他曾经站在雾里也很迷茫”。我是蒋星熠Jaxonic,一名在代码宇宙中探索的极客旅人。从.NET Framework到.NET 8,我深耕跨平台、高性能、云原生开发,践行领域驱动设计与微服务架构,用代码书写技术诗篇。分享架构演进、性能优化与AI融合前沿,助力开发者在二进制星河中逐光前行。关注我,共探技术无限可能!
.NET技术深度解析:现代企业级开发指南
|
11月前
|
SQL 小程序 API
如何运用C#.NET技术快速开发一套掌上医院系统?
本方案基于C#.NET技术快速构建掌上医院系统,结合模块化开发理念与医院信息化需求。核心功能涵盖用户端的预约挂号、在线问诊、报告查询等,以及管理端的排班管理和数据统计。采用.NET Core Web API与uni-app实现前后端分离,支持跨平台小程序开发。数据库选用SQL Server 2012,并通过读写分离与索引优化提升性能。部署方案包括Windows Server与负载均衡设计,确保高可用性。同时针对API差异、数据库老化及高并发等问题制定应对措施,保障系统稳定运行。推荐使用Postman、Redgate等工具辅助开发,提升效率与质量。
461 0
|
开发框架 算法 .NET
C#/.NET/.NET Core技术前沿周刊 | 第 15 期(2024年11.25-11.30)
C#/.NET/.NET Core技术前沿周刊 | 第 15 期(2024年11.25-11.30)
255 6
|
开发框架 Cloud Native .NET
C#/.NET/.NET Core技术前沿周刊 | 第 16 期(2024年12.01-12.08)
C#/.NET/.NET Core技术前沿周刊 | 第 16 期(2024年12.01-12.08)
275 6
|
机器学习/深度学习 人工智能 Cloud Native
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台。本文深入解析 .NET 的核心优势,探讨其在企业级应用、Web 开发及移动应用等领域的应用案例,并展望未来在人工智能、云原生等方面的发展趋势。
344 3
|
敏捷开发 缓存 中间件
.NET技术的高效开发模式,涵盖面向对象编程、良好架构设计及高效代码编写与管理三大关键要素
本文深入探讨了.NET技术的高效开发模式,涵盖面向对象编程、良好架构设计及高效代码编写与管理三大关键要素,并通过企业级应用和Web应用开发的实践案例,展示了如何在实际项目中应用这些模式,旨在为开发者提供有益的参考和指导。
167 3
|
开发框架 安全 Java
.NET技术的独特魅力与优势,涵盖高效的开发体验、强大的性能表现、高度的可扩展性及丰富的生态系统等方面,展示了其在软件开发领域的核心竞争力
本文深入探讨了.NET技术的独特魅力与优势,涵盖高效的开发体验、强大的性能表现、高度的可扩展性及丰富的生态系统等方面,展示了其在软件开发领域的核心竞争力。.NET不仅支持跨平台开发,具备出色的安全性和稳定性,还能与多种技术无缝集成,为企业级应用提供全面支持。
496 3
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
575 0
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
327 7
|
存储 开发框架 前端开发
[回馈]ASP.NET Core MVC开发实战之商城系统(五)
经过一段时间的准备,新的一期【ASP.NET Core MVC开发实战之商城系统】已经开始,在之前的文章中,讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销,新品爆款】,商品列表页面,商品详情等功能的开发,今天继续讲解购物车功能开发,仅供学习分享使用,如有不足之处,还请指正。
409 0