ASP.NET Core 中的几大功能模块(Razor Pages、MVC、SignalR/Blazor、Mini-API 等等)都以终结点(End Point)的方式公开。在HTTP管道上调用时,其扩展方法基本是以 Map 开头,如 MapControllers、MapBlazorHub。

对于 MVC 应用,常用的是静态路由匹配方式,即调用以下方法:

MapControllers
MapControllerRoute
MapDefaultControllerRoute
MapAreaControllerRoute

它们的特点是路由模板是固定的——提供 controller、action 或 area 等关键字段的值,如咱们严重熟悉的 {controller=Home}/{action=Index}/{id?}。在访问控制器时,必须按照路由规的格式提供相应的值。比如,访问 DouFuZha 控制器下的 Boom 操作,则需要URL:/doufuzha/boom。也就是说:控制器名称和操作名称都是直接指定的,没有中途转换

相反地,如果 controller、action 等关键字段不直接提供,或者需要翻译(转换),这就涉及到调用 MapDynamicControllerRoute 扩展方法的事了。这个方法不要翻译为“动态MVC”,这样翻译会被误解为“运行时动态产生控制器”(其实真有高人这么做,综合运用代码生成和动态编译生成控制器,但实际开发中比较少用到,除非有特殊需求的庞大系统)。这里的“动态”修饰的是路由,所以,这个扩展方法是允许开发者使用另一个路由规则来动态映射相应的控制器/操作。

这个有什么用途呢?既然有这个功能,当然是有用的。例如

【情况一】(这是很多教程文章都用的例子)控制器名:Cats,操作:Play。

指定动态路由:{lang}/{controller}/{action}

其中,lang 表示语言,这样就可以不同语言使用不同的 URL 了。请看:

中文:zh/猫/撸猫
英文:en/cats/play

于是,应用程序在运行阶段动态转译,先提取 lang 字段的值,看看是什么语言,如果是 zh ,那么 controller=猫 要转换为 controller = cats;action=撸猫 要转换为 action=play。如果 lang 字段的值是 en,可以不转换。

【情况二】控制器有两个:MembersV1、MembersV2

指定动态路由:{controller}/{action}/{v}

如果 controller=members,v=1,那么,转换为:controller=MembersV1,action 的值不变。

如果 controller=members,v=2,那么,转换为:controller=MembersV2,action 的值不变。

如果 controller != members,不做任何转换。

【情况三】比较奇葩,动态路由中不包含控制器名,只包含操作名称。

{action}

控制器名称通过 HTTP 请求的头部来提供。

GET /cooking
accept: ...
host: ...
controller: dabaicai

于是,经过转换,得到 controller=DaBaiCai,action=Cooking(煮大白菜)。

上面只是列举了一些情况,其实还有很多场景是可以用到动态路由 MVC 的。

下面咱们聊聊怎么去运用。实现 MVC 的动态路由需要知道一个核心类—— DynamicRouteValueTransformer。这是个抽象类,需要实现抽象方法 TransformAsync。该方法是异步等待的,签名如下:

public abstract ValueTask<RouteValueDictionary> TransformAsync (HttpContext httpContext, RouteValueDictionary values);

你会发现一件有意思的事:输入参数 values 和返回值都是路由规则的数据字典。估计你也看出这货的思路了,是的,输入参数的 values 动态路由模板被匹配后产生的字段集,而返回的字典是你根据实际需求转换后的路由字段集。

如前文举例的 {controller}/{action}/{v},如果访问:http://abc.com/members/register/2,那么,values 参数包含的数据为:

controller: members
action: register
v: 2

members 控制器不存在的,所以,根据 v 的值进行转换,最终返回的字典数据为:

controller: MembersV2
action: Register

你也会发现,TransformAsync 方法还有个 HttpContext 参数。对的,这是为了方便你分析 HTTP 请求消息用的。比如,前文的举例中,就有个脑洞大开的,把控制器名藏在 Header 里面。这时候就可以通过这个 HttpContext 参数访问 Headers 集合,把 HTTP 头的值读出来,并作为 controller 路由字段的值。

-----------------------------------------------------------------------------------------------------------------------

接下来,我们新手试玩。

老周这个示例是固定路由和动态路由同时使用的,这样比较实用。好,咱先不说太多。来看看控制器的代码。

public class HomeController : Controller
{
public ActionResult Main()
{
var context =HttpContext.RequestServices.GetRequiredService<StudentsDbContext>();
return View("MainView", context.Students.ToArray());
} public ActionResult NewStudent() => View(); public ActionResult AddNewItem(Student stu)
{
var dbcontext = HttpContext.RequestServices.GetRequiredService<StudentsDbContext>();
if (ModelState.IsValid)
{
dbcontext.Students.Add(stu); dbcontext.SaveChanges();
}
return RedirectToAction("Main");
} public ActionResult DeleteItem(long sid)
{
var context = HttpContext.RequestServices.GetRequiredService<StudentsDbContext>();
var q = from s in context.Students
where s.Id == sid
select s;
if(q.Count() == 1)
{
Student? _stu = q.FirstOrDefault();
if(_stu != null)
{
context.Students.Remove(_stu);
context.SaveChanges();
}
}
return RedirectToAction("Main");
}
}

这个控制器只做演示,所以比较简单,主要是 Main 浏览学生信息;NewStudent 展示新增学生信息的页面,AddNewItem 在 <form> 元素 POST 时调用,向数据库插入一条学生信息;DeleteItem 根据学生 ID 删除一条学生数据。

下面是 MainView 视图,它主要浏览学生信息。

@model IEnumerable<Student>

<div>
<a asp-controller="Home" asp-action="NewStudent">新增</a>
</div> <div>
@if(Model.Count() == 0)
{
<p>什么鬼都没有</p>
}
else
{
<table style="width:85%;margin-top:15px;" border="1" cellpadding="2" cellspacing="0">
<thead>
<tr>
<th>编号</th>
<th>姓名</th>
<th>电邮</th>
<th>年龄</th>
</tr>
</thead>
<tbody>
@foreach(var student in Model)
{
<tr>
<td>@student.Id</td>
<td>@student.Name</td>
<td>@student.Email</td>
<td>@student.Age</td>
<td><a href="/delone/@student.Id">删除</a></td>
</tr>
}
</tbody>
</table>
}
</div>

这个页面里已经混合了固定路由和动态路由了。

1、asp-controller、asp-action 是标记帮助器(Tag Helpers)实现的,指定要访问的控制器和操作方法,会为 <a> 元素自动生成链接,这是固定路由,生成的URL的路由模板是我们熟悉的:{controller}/{action}。

2、在每一行数据的“删除”链接上,/delone/@student.Id 是动态路由,@student.Id是返回ID的值,即 /delone/1、/delone/2、/delone/6 等。这个用的是动态路由:{op}/{sid:long?},匹配后,op=delone,sid=1,sid=2……

还有,控制器代码中,AddNewItem 和 DeleteItem 操作方法在处理完后跳转回 Main 操作方法时,也是使用了固定路由。

return RedirectToAction("Main");

下面就是核心部分了,实现 DynamicRouteValueTransformer 抽象类。

 public class CustTransform : DynamicRouteValueTransformer
{
public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
// 这个动态路由要有 op 字段
if (!values.ContainsKey("op"))
{
return new ValueTask<RouteValueDictionary>(values);
}
var newValues = new RouteValueDictionary();
string? k = values["op"] as string;
if (k == null || k is { Length: 0 })
{
return new ValueTask<RouteValueDictionary>(values);
}
// 转换路由参数
switch (k.ToLowerInvariant())
{
case "addone":
newValues["controller"] = "Home";
newValues["action"] = "NewStudent";
break;
case "listall":
newValues["controller"] = "Home";
newValues["action"] = "Main";
break;
case "delone":
newValues["controller"] = "Home";
newValues["action"] = "DeleteItem";
// 解析id
if(values.TryGetValue("sid", out object? val) && val != null)
{
newValues["sid"] = val;
}
break;
default:
newValues["controller"] = "Home";
newValues["action"] = "Main";
break;
}
return new ValueTask<RouteValueDictionary>(newValues);
}
}

有许多教程的示例代码是直接修改 values 然后将它返回,这样会增加了代码对 values 实例的引用,听说这样会导致内存泄漏。老周的代码是 new 一个新的 RouteValueDictionary 实例,如果要要修改,就把它返回;如果不需要修改,可以把 values 直接返回。至于这样会不会有问题,不太好说,反正目前来说能正常运行,内存占用没多大变化。

这里的转换思路是这样的:

动态路由 {op}/{sid?},sid 可选,在删除数据时要用。

如果 op=addone ==> controller=Home,action=NewStudent;

如果 op=listall ==> controller=Home,action=Main;

如果 op=delone,sid需要有值 ==> controller=Home,action=DeleteItem,sid=sid(这个sid从动态路由传过来)。

如果 op=其他值,直接 controller=Home,action=Main。

现在,你明白前面视图文件中,“删除”链接的 href 为啥是 /delone/@student.Id 了吧。

CustTransform 类要注册到服务容器中,因为动态路由在执行时是从服务容器获取 DynamicRouteValueTransformer 实例的。

builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<CustTransform>();

这里我就直接注册为单实例模式,反正不需要反复实例化。

在应用程序 Build 了后,需要映射终结点。

var builder = WebApplication.CreateBuilder(args);
…… var app = builder.Build(); app.MapControllerRoute("app", "{controller}/{action}/{sid:long?}");
app.MapDynamicControllerRoute<CustTransform>("{op=main}/{sid:long?}"); app.Run();

以前看到网上有人问:为什么我用了动态路由之后,asp-controller、asp-action 等 Tag Helper 不能用了?就是因为你只调用了 MapDynamicControllerRoute 方法,而忘了调用 MapControllerRoute 方法。

如果你同时用到了固定路由和动态路由,一定要同时调用两个方法。放心,它们不会冲突,除非你指定的路由模板重复。如果你整个项目都用的是动态路由,那可以不调用 MapControllerRoute 方法。

这个示例老周图省事,用的是内存数据库(In-Memory DB)。这是 DB 上下文和模型类。

 // 实体
[PrimaryKey(nameof(Id))]
public class Student
{
public long Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public int? Age { get; set; }
} // 数据库上下文
public class StudentsDbContext : DbContext
{
public StudentsDbContext(DbContextOptions<StudentsDbContext> options) : base(options) { } public DbSet<Student> Students => Set<Student>();
}

Id 属性是主键。

运行之后,咱们看看生成的 HTML。

内存数据库只存在内存中,所以每次运行后,要手动添加一些数据来测试。

这样,固定路由和动态路由的URL都能同时工作了。

【ASP.NET Core】动态映射MVC路由的更多相关文章

  1. (8)ASP.NET Core 中的MVC路由一

    1.前言 ASP.NET Core MVC使用路由中间件来匹配传入请求的URL并将它们映射到操作(Action方法).路由在启动代码(Startup.Configure方法)或属性(Controlle ...

  2. (9)ASP.NET Core 中的MVC路由二

    1.URL生成 MVC应用程序可以使用路由的URL生成功能,生成指向操作(Action)的URL链接. IUrlHelper 接口用于生成URL,是MVC与路由之间的基础部分.在控制器.视图和视图组件 ...

  3. asp.net core 系列 5 MVC框架路由(上)

    一. 概述 介绍asp.net core路由时,我初步想了下,分几篇来说明.  路由的知识点很多,参考了官方文档提取出一些重要的知识点来说.    在ASP.NET Core中是使用路由中间件来匹配传 ...

  4. ASP.Net Core 2.2 MVC入门到基本使用系列 (三)

    本教程会对基本的.Net Core 进行一个大概的且不会太深入的讲解, 在您看完本系列之后, 能基本甚至熟练的使用.Net Core进行Web开发, 感受到.Net Core的魅力. 本教程知识点大体 ...

  5. 在 Asp.Net Core 中安装 MVC

    在 ASP.NET Core 中安装 MVC 到目前为止,我们在本系列视频中使用的 ASP.NET Core 项目是使用“空”项目模板生成的.目前这个项目没有设置和安装 MVC. 两个步骤学会在 AS ...

  6. ASP.NET CORE 1.0 MVC API 文档用 SWASHBUCKLE SWAGGER实现

    from:https://damienbod.com/2015/12/13/asp-net-5-mvc-6-api-documentation-using-swagger/ 代码生成工具: https ...

  7. ASP.NET Core 2.0 MVC项目实战

    一.前言 毕业后入职现在的公司快有一个月了,公司主要的产品用的是C/S架构,再加上自己现在还在学习维护很老的delphi项目,还是有很多不情愿的.之前实习时主要是做.NET的B/S架构的项目,主要还是 ...

  8. ASP.Net Core 2.2 MVC入门到基本使用系列 (二)

    本教程会对基本的.Net Core 进行一个大概的且不会太深入的讲解, 在您看完本系列之后, 能基本甚至熟练的使用.Net Core进行Web开发, 感受到.Net Core的魅力. 本教程知识点大体 ...

  9. ASP.Net Core 2.2 MVC入门到基本使用系列 (一)

    本教程会对基本的.Net Core 进行一个大概的且不会太深入的讲解, 在您看完本系列之后, 能基本甚至熟练的使用.Net Core进行Web开发, 感受到.Net Core的魅力. 本教程知识点大体 ...

  10. ASP.Net Core 2.2 MVC入门到基本使用系列 (四)

    本教程会对基本的.Net Core 进行一个大概的且不会太深入的讲解, 在您看完本系列之后, 能基本甚至熟练的使用.Net Core进行Web开发, 感受到.Net Core的魅力. 本教程知识点大体 ...

随机推荐

  1. 逆向使用 execjs时遇到 UnicodeDecodeError: 'gbk' codec can't decode byte 0x80 in position 28: illegal multibyte sequence

    问题: 如下图所示 今天在维护以前的爬虫代码 发现有个网站一直爬取失败,我原以为是网站逆向的部分改了,搞了好久才发现是GBK的问题 接下来告诉大家解决方案 解决方案 如下图 在下图这个subbsubp ...

  2. react的组件通信

    react的组件通信 1.父组件传子组件 import React, {Component} from 'react'class Father extends Component{ render() ...

  3. 小程序利用canvas 绘制图案 (生成海报, 生成有特色的头像)

    小程序利用canvas 绘制图案 (生成海报, 生成有特色的头像) 微信小程序生成特色头像,海报等是比较常见的.下面我来介绍下实现该类小程序的过程. 首先选择前端来通过 canvas 绘制.这样比较节 ...

  4. 【题解】CF631B Print Check

    题面传送门 解决思路: 首先考虑到,一个点最终的情况只有三种可能:不被染色,被行染色,被列染色. 若一个点同时被行.列染色多次,显示出的是最后一次被染色的结果.所以我们可以使用结构体,对每一行.每一列 ...

  5. C#11之原始字符串

    最近.NET7.0和C#11相继发布,笔者也是第一时间就用上了C#11,其中C#11的有一个更新能解决困扰我多年的问题,也就是文章的标题原始字符串. 在使用C#11的原始字符串时,发现的一些有意思的东 ...

  6. 前端面试HTML和CSS总结,这一篇就够了!

    一,面试基础 HTML和CSS ps:这俩面试答不上来的,基本就可以回去了,以下是HTML题,一般来说这地方不会出太多题,面试官也不愿意花太多时间在这上面. 1,HTML语义化,如何理解语义化? 让人 ...

  7. go工具pprof部署

    在做内存分析时,用到了pprof,这里做一下部署介绍和入门级别的使用. pprof是golang的性能工具,有两种交互方式:命令行交互和web交互,同时还支持性能分析数据的图形化展示. 部署pprof ...

  8. 关于model,modelsmanager,db以及phql之间关系的梳理

    摘要: model在前,db在model后面作为驱动支持.phql是phalcon自创的查询语言,主要特性是在sql语句中用模型名替代表名,其聪明地解析模型类,包括关联关系等,还支持参数绑定.mode ...

  9. Vue使用axios请求接口返回成功200但是进入到catch中

    发生这个问题时查阅了许多资料,没有一个是对得上的.最后发现原来是在请求拦截器中的错误 错误代码如下 // 添加响应拦截器 axios.interceptors.response.use(functio ...

  10. MySQL DATE_SUB查询工龄大于35的员工信息

    #(11) 查询工龄大于或等于35年的员工信息.SELECT * FROM emp e WHERE e.HIREDATE<=DATE_SUB(SYSDATE(),INTERVAL 35 YEAR ...