ASP.NET Core 基础(九)——路由Routing

此文是在官方文档的基础上做的个人笔记,一些简单的内容就没用再列出来了,参考官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/routing?view=aspnetcore-5.0

定义:路由是负责匹配传入的http请求,然后进行发送到应用的可执行终结点(代码处理单元)。

本文只介绍较低级别的路由信息,有关MVC中的路由和Razor中的路由参考:
https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0
https://docs.microsoft.com/zh-cn/aspnet/core/razor-pages/razor-pages-conventions?view=aspnetcore-5.0

1. 路由基础知识

路由是一对由UseRoutingUseEndPoints注册的中间件:

  • UseRouting向管道添加路由配置。此中间件会查看应用中定义的终结点集合,并根据请求选择最佳配置。
  • UseEndpoints向管道添加终结点执行。它会执行与所选终结点关联的委托。
 app.UseRouting();
 //...
 app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });

上述代码MapGet表示当是get请求时且请求的是根URL时就执行委托。如果请求的方法不是GET或者请求的不是根URL则无路由配置就返回404.

1.1 终结点

MapGet方法用于定义终结点。终结点可以通过匹配URL和HTTP方法来选择运行委托执行请求。类似MapGet的方法还有:

  • MapMapDeleteMapGetMapPutMapPostMapHealthChecks
  • MapRazorPages:针对的RazorPage
  • MapController:针对控制器
  • MapHub<THub>:针对SingalR
  • MapGrpcService<TService>:针对gRpc

1.2 路由模板

下面代码的/hello/{name:alpha}就是一个路由模板,这个模板匹配类似/hello/jim格式的路由。其中alpha叫路由约束,表示name属性应为字母,所以不会匹配/hello/123.

endpoints.MapGet("/hello/{name:alpha}",async c=>
{
    var name=c.Request.RouteValues["name"];
})

常见路由模板介绍:

路由模板示例匹配 URI请求 URI
hello/hello仅匹配单个路径 /hello。
hello/{name:minlength(4)}/hello/jim不匹配
/hello/jimm匹配
name最小长度4
files/{filename}.{ext?}/files/a.txt
/files/a
因为那个点是可选的所以能匹配两个
{Page=Home}/匹配并将 Page 设置为 默认值Home。
{Page=Home}/Contact匹配并将 Page 设置为 Contact。
{controller}/{action}/{id?}/Products/List映射到 Products 控制器和 List 操作。
{controller}/{action}/{id?}/Products/Details/123映射到 Products 控制器和 Details 操作,并将 id 设置为 123。
{controller=Home}/{action=Index}/{id?}/映射到 Home 控制器和 Index 方法。 id 将被忽略。
{controller=Home}/{action=Index}/{id?}/Products映射到 Products 控制器和 Index 方法。 id 将被忽略。

1.3路由约束

路由约束不能等同于输入验证,因为不符合约束的路由直接返回404.

约束示例匹配项示例说明
int{id:int}123456789, -123456789匹配任何整数
bool{active:bool}true, FALSE匹配 true 或 false。 不区分大小写
datetime{dob:datetime}2016-12-31, 2016-12-31 7:32pm在固定区域性中匹配有效的 DateTime 值。 请参阅前面的警告。
decimal{price:decimal}49.99, -1,000.01在固定区域性中匹配有效的 decimal 值。 请参阅前面的警告。
double{weight:double}1.234, -1,001.01e8在固定区域性中匹配有效的 double 值。 请参阅前面的警告。
float{weight:float}1.234, -1,001.01e8在固定区域性中匹配有效的 float 值。 请参阅前面的警告。
guid{id:guid}CD2C1638-1638-72D5-1638-DEADBEEF1638匹配有效的 Guid 值
long{ticks:long}123456789, -123456789匹配有效的 long 值
minlength(value){username:minlength(4)}Rick字符串必须至少为 4 个字符
maxlength(value){filename:maxlength(8)}MyFile字符串不得超过 8 个字符
length(length){filename:length(12)}somefile.txt字符串必须正好为 12 个字符
length(min,max){filename:length(8,16)}somefile.txt字符串必须至少为 8 个字符,且不得超过 16 个字符
min(value){age:min(18)}19整数值必须至少为 18
max(value){age:max(120)}91整数值不得超过 120
range(min,max){age:range(18,120)}91整数值必须至少为 18,且不得超过 120
alpha{name:alpha}Rick字符串必须由一个或多个字母字符组成,a-z,并区分大小写。
regex(expression){ssn:regex(^\d{{3}}-\d{{2}}-\d{{4}}$)}123-45-6789字符串必须与正则表达式匹配。 请参阅有关定义正则表达式的提示。
required{name:required}Rick用于强制在 URL 生成过程中存在非参数值
多个约束的组合{id:int:min(1)}2整数最小为1
路由参数转换{path:slugify}通过slugify类对path进行转换

1.3.1 自定义路由约束

很少情况下需要这么做,当模型绑定也不能实现你的要求时,可以实现IRouteConstraint接口来创建自定义路由约束

1.4 路由参数转换

使用场景:/subscription-management/get-all这种的url可以匹配到SubScriptionManagementControll.GetAll方法上。
首先,定义路由

routes.MapControllerRoute(
    name: "default",
    template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

然后写一个类实现IOutboundParameterTransformer接口:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString(), 
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}

最后,在startup里进行配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}

当使用Url.Action("GetAll","SubscriptionMangement")生成路径时,也会生成/subscription-management/get-all

1.5 其它中间件与Routing、EndPoint的关系

//因为还没有调用app.UseRouting,所以这里始终为null
app.Use(next => context =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseRouting();

//如果你请求的url匹配到了某个EndPoint,则这里不会返回null
app.Use(next => context =>
{
    var point=context.GetEndpoint();
    //可以获取终结点后面跟的元数据进行处理
    var cls=point?.Metadata.GetMetadata<Class>();
    Console.WriteLine($"2. Endpoint: {point?.DisplayName ?? "(null)"}");
    
    return next(context);
});

app.UseEndpoints(endpoints =>
{
    // 匹配根Url,如果匹配到了,则整个管道到此结束,不会执行后续代码
    endpoints.MapGet("/", context =>
    {
        Console.WriteLine(
            $"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
        return Task.CompletedTask;
    }).WithDisplayName("Hello").WithMetadata(new Class());//带上元数据Class对象
});

//如果你请求的url没有匹配到任何EndPoint,才会执行到这里
//所以这里要么不输出,要么输出null
app.Use(next => context =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});
  • 中间件可以在UseRouting前执行,以修改路由操作的数据,如:UseRewriter, UseHttpMethodOverride, UsePathBase.
  • 中间件可以在UseRoutingUseEndPoints之间运行,以便在执行终结点前处理路由元数据:如:UseAuthorizationUseCors

1.6 路由与主机(Host)的匹配

使用场景:请求过来的某个url,请求头里的Host字段应符合某个域名规则。如:

  1. 请求的根路径的主机只能是 contoso.comadventure-works.com。请求健康检查的主机的端口号只能是8080.
public void Configure(IApplicationBuilder app)
{
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
            .RequireHost("contoso.com");
        endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
            .RequireHost("adventure-works.com");
        endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
    });
}
  1. 请求到某个controller上的主机只能是contoso.comadventure-works.com。但是请求到Privacy这个action上的主机是个例外,只能是example.com:8080
[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
    [Host("example.com:8080")]
    public IActionResult Privacy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

1.7路由的性能

当应用出现性能问题的时候,如果开发人员排除了代码逻辑问题,一般都会认为是路由问题。但最常见的根本原因是性能不佳的自定义中间件。以下演示如何测试一个中间件执行耗时:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();
        logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });
    app.UseRouting();
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();
        logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });
}

用Time1减去Time2就测出了UseRouting中间件的性能。上述代码也可以优化如下:

public sealed class MyStopwatch : IDisposable
{
    ILogger<Startup> _logger;
    string _message;
    Stopwatch _sw;
    public MyStopwatch(ILogger<Startup> logger, string message)
    {
        _logger = logger;
        _message = message;
        _sw = Stopwatch.StartNew();
    }
    private bool disposed = false;
    public void Dispose()
    {
        if (!disposed)
        {
            _logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",_message, _sw.ElapsedMilliseconds);
            disposed = true;
        }
    }
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    int count = 0;
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });
    app.UseRouting();
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JimCarter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值