【ASP.NET Core】动态映射MVC路由

asp,net,core,动态,映射,mvc,路由 · 浏览次数 : 808

小编点评

这个控制器主要用于展示和操作学生信息,并支持删除操作。它包含以下主要部分: 1. **`MainView`视图**: - 使用 `IEnumerable` 的类型声明接收所有学生数据。 - 在视图中显示所有学生信息,包括编号、姓名和年龄。 - 使用 `@foreach` 循环遍历 `Model` 中的每个学生,并显示其信息。 - 并在删除按钮点击时调用 `DeleteItem` 方法,并使用 `return RedirectToAction` 方法跳转回 `Main` 页面。 2. **`DeleteItem` 方法**: - 使用 `HttpContext.RequestServices.GetRequiredService` 获取数据库上下文。 - 从 `Model` 中获取要删除的学生的 `id`。 - 使用 `string` 类型将 `id` 转化成 `string` 类型。 - 调用 `context.Students.Find(id)` 获取指定 id 的学生。 - 使用 `return RedirectToAction` 方法跳转回 `Main` 页面。 3. **`CustTransform` 类**: - 实现了 `DynamicRouteValueTransformer` 接口。 - 使用 `RouteValueDictionary` 来存储路由参数,并根据路由参数的值进行路由。 - 在 `MapDynamicControllerRoute` 方法中注册 `CustTransform` 类。 4. **注册`CustTransform` 类**: - 在应用程序构建过程中注册 `CustTransform` 类,并将其实例化。 - 这样就确保在动态路由中使用到的 `CustTransform` 实例。 5. **映射终结点**: - 在应用程序启动时,调用 `builder.MapControllerRoute` 和 `builder.MapDynamicControllerRoute` 方法注册固定路由和动态路由。 - 使用 `app.MapControllerRoute` 注册固定路由,使用 `app.MapDynamicControllerRoute` 注册动态路由。 6. **测试**: - 使用浏览器访问应用程序,并观察页面内容和操作按钮。 - 在删除数据时,查看导航到的页面是否正确。 总而言之,这个控制器提供了一个展示和操作学生信息的简单示例,并使用了一些设计技巧,例如使用 `CustTransform` 类来处理动态路由,以及注册 `CustTransform` 类以确保动态路由中使用到的 `CustTransform` 实例。

正文

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路由相似的内容:

【ASP.NET Core】动态映射MVC路由

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

Asp-Net-Core开发笔记:进一步实现非侵入性审计日志功能

前言 上次说了利用 AOP 思想实现了审计日志功能,不过有同学反馈还是无法实现完全无侵入,于是我又重构了一版新的。 回顾一下:Asp-Net-Core开发笔记:实现动态审计日志功能 现在已经可以实现对业务代码完全无侵入的审计日志了,在需要审计的接口上加上 [AuditLog] 特性,就可以记录这个接

基于EF Core存储的国际化服务

前言 .NET 官方有一个用来管理国际化资源的扩展包Microsoft.Extensions.Localization,ASP.NET Core也用这个来实现国际化功能。但是这个包的翻译数据是使用resx资源文件来管理的,这就意味着无法动态管理。虽然官方有在文档中提供了一些第三方管理方案,但是都不太

aspnetcore插件开发dll热加载

该项目比较简单,只是单纯的把业务的dll模块和controller的dll做了一个动态的添加删除处理,目的就是插件开发。由于该项目过于简单,请勿吐槽。复杂的后续可以通过泛型的实体、dto等做业务和接口的动态区分。 项目结构如下: 上面的两个模块是独立通过dll加载道项目中的 repository动态

aspnetcore插件开发dll热加载 二

这一篇文章应该是个总结。 投简历的时候是不是有人问我有没有abp的开发经历,汗颜! 在各位大神的尝试及自己的总结下,还是实现了业务和主机服务分离,通过dll动态的加载卸载,控制器动态的删除添加。 项目如下: 演示效果: 下面就是代码部分: 重点 1.IActionDescriptorChangePr

ASP.NET Core中创建中间件的几种方式

前言 今天我们一起来盘点一下在ASP.NET Core应用程序中添加和创建中间件常见的四种方式。 中间件介绍 ASP.NET Core中间件(Middleware)是用于处理HTTP请求和响应的组件,它们被安排在请求处理管道中,并按顺序执行。中间件的设计是为了使其在请求处理管道中能够以灵活和可扩展的

推荐十个优秀的ASP.NET Core第三方中间件,你用过几个?

ASP.NET Core 作为一个强大的、跨平台的、高性能的开源框架,为开发者提供了丰富的功能和灵活的扩展性。其中,中间件(Middleware)是 ASP.NET Core 架构中的核心组件之一,它负责处理 HTTP 请求和响应的管道,允许开发者在请求和响应之间插入自定义逻辑。随着 ASP.NET

Asp-Net-Core开发笔记:使用原生的接口限流功能

前言 之前介绍过使用 AspNetCoreRateLimit 组件来实现接口限流 从 .Net7 开始,AspNetCore 开始内置限流组件,当时我们的项目还在 .Net6 所以只能用第三方的 现在都升级到 .Net8 了,当然是得来试试这个原生组件 体验后:配置使用都比较简单,不过功能也没有 A

ASP.NET Core如何禁用模型验证(或者从模型状态中移除某些属性)?

这是一篇4年前的文章:【经验分享】在ASP.NET Core中,如果禁用某个请求的模型验证? 事隔多年,又有网友问到这个问题。我就来重新整理一下,顺便扩展一下之前的解决办法。 这是一个来自网友【David】的提问。在 AppBoxCore 项目的新增用户页面,新增一个上传按钮:

Asp .Net Core 系列:国际化多语言配置

目录概述术语本地化器IStringLocalizer在服务类中使用本地化IStringLocalizerFactoryIHtmlLocalizerIViewLocalizer资源文件区域性回退配置 CultureProvider内置的 RequestCultureProvider实现自定义 Requ