理解ASP.Net核心请求和反应管道----第4部分:路线和终点 (中文 (Chinese Simplified))

理解ASP.Net核心请求和反应管道----第4部分:路线和终点

Sunday, 09 November 2025

//

17 minute read

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

我们穿越了托管层 探索了凯斯特勒 掌握了中间软件现在,我们得出一个关键问题:一旦一个请求通过你的中器管道,ASP.NET Core如何知道执行的代码?答案是路由.

终点

路由是匹配收到 HTTP 请求的可执行代码的机制 。

终点是目的地——执行的实际代码。

它们共同形成了一个强大、灵活的系统,支持从简单的路线处理器到复杂的监查系统应用等一切工作。

timeline
    title ASP.NET Core Routing Evolution
    ASP.NET Core 1.0 - 2.2 : Traditional Routing : Routes defined in middleware : MapRoute for MVC
    ASP.NET Core 3.0 - 3.1 : Endpoint Routing : UseRouting + UseEndpoints : Routing metadata aware
    ASP.NET Core 6.0+ : Minimal APIs : Direct MapGet/MapPost : Simplified registration

在此部分, 我们将探索路线如何运作, 端点如何登记和执行, 以及如何利用这个系统来建立复杂的应用程序 。

app.UseMvc(routes =>
{
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

备注:这是我在AI/一种花费1000美元Clude Code Web信用的实验的一部分。

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

我为这篇论文、我的理解、我不得不提出来的问题 提供了大量的资料。

app.MapGet("/users/{id}", (int id) => $"User {id}");

这很有趣,填补了一个空白 我还没有看到填补 其他地方。

区域规则的演变

graph LR
    A[Request] --> B[Routing Middleware]
    B --> C{Match Route?}
    C -->|Yes| D[Set Endpoint]
    C -->|No| E[No Endpoint Set]
    D --> F[Authorization Middleware]
    E --> F
    F --> G[Other Middleware]
    G --> H[Endpoint Middleware]
    H --> I{Endpoint Exists?}
    I -->|Yes| J[Execute Endpoint]
    I -->|No| K[404 Not Found]

    style B fill:#ffe1e1
    style H fill:#e1ffe1
    style J fill:#e1e1ff

ASP.NET核心的路线变化显著:UseRouting())

传统例常(早于3.0):

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Phase 1: Route matching happens here
app.UseRouting();

// Between routing and endpoints, you can use middleware that needs route information
app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();
    if (endpoint != null)
    {
        Console.WriteLine($"Matched endpoint: {endpoint.DisplayName}");

        // Access route values
        var routeValues = context.Request.RouteValues;
        foreach (var (key, value) in routeValues)
        {
            Console.WriteLine($"  {key} = {value}");
        }
    }

    await next(context);
});

app.UseAuthorization(); // Can make decisions based on matched endpoint

// Phase 2: Endpoint execution happens here
app.MapGet("/users/{id}", (int id) => $"User {id}");

app.Run();

终点路由(3.0+):

sequenceDiagram
    participant Request
    participant Routing as UseRouting
    participant Auth as UseAuthorization
    participant Endpoint as Endpoint Middleware

    Request->>Routing: GET /users/123
    Note over Routing: Parse URL<br/>Match against patterns<br/>Extract route values
    Routing->>Routing: Found match: /users/{id}
    Routing->>Routing: Set endpoint metadata<br/>Set route values {id: 123}

    Routing->>Auth: Continue with endpoint set
    Note over Auth: Can check endpoint metadata<br/>for [Authorize] attributes

    Auth->>Endpoint: Continue
    Note over Endpoint: Endpoint is set
    Endpoint->>Endpoint: Execute matched endpoint<br/>with route values

    Endpoint->>Request: Return response

现代最低API(6.0+):

规则运行如何运作:两阶段进程

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseRouting();

// This middleware runs BEFORE endpoint execution
app.Use(async (context, next) =>
{
    Console.WriteLine("Before endpoint execution");
    await next(context);
    Console.WriteLine("After endpoint execution");
});

// Endpoint execution happens here
app.MapGet("/", () =>
{
    Console.WriteLine("Executing endpoint");
    return "Hello World";
});

app.Run();

终点定线分两个阶段进行:

Before endpoint execution
Executing endpoint
After endpoint execution

第1阶段:道路配对(

路由的中间软件检查请求, 并尝试将其匹配到注册的终点 :

流程图 :

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Literal path
app.MapGet("/", () => "Home page");

// Single parameter
app.MapGet("/users/{id}", (int id) => $"User {id}");

// Multiple parameters
app.MapGet("/posts/{year}/{month}/{day}", (int year, int month, int day) =>
    $"Posts from {year}-{month:D2}-{day:D2}");

// Optional parameter
app.MapGet("/products/{id?}", (int? id) =>
    id.HasValue ? $"Product {id}" : "All products");

// Default value
app.MapGet("/search/{query=all}", (string query) =>
    $"Searching for: {query}");

// Catch-all parameter
app.MapGet("/files/{*path}", (string path) =>
    $"File path: {path}");

app.Run();

第2阶段:终点执行

GET /                           → "Home page"
GET /users/123                  → "User 123"
GET /posts/2024/1/15            → "Posts from 2024-01-15"
GET /products                   → "All products"
GET /products/42                → "Product 42"
GET /search                     → "Searching for: all"
GET /search/aspnet              → "Searching for: aspnet"
GET /files/docs/guide.pdf       → "File path: docs/guide.pdf"

端点中继器件执行匹配的端点 :

产出:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Integer constraint
app.MapGet("/users/{id:int}", (int id) =>
    $"User {id}");

// Minimum value
app.MapGet("/products/{id:int:min(1)}", (int id) =>
    $"Product {id}");

// Range
app.MapGet("/items/{id:int:range(1,100)}", (int id) =>
    $"Item {id}");

// String length
app.MapGet("/codes/{code:length(5)}", (string code) =>
    $"Code {code}");

// Min/max length
app.MapGet("/names/{name:minlength(2):maxlength(20)}", (string name) =>
    $"Name {name}");

// Regex
app.MapGet("/posts/{slug:regex(^[a-z0-9-]+$)}", (string slug) =>
    $"Post slug: {slug}");

// GUID
app.MapGet("/orders/{id:guid}", (Guid id) =>
    $"Order {id}");

// DateTime
app.MapGet("/appointments/{date:datetime}", (DateTime date) =>
    $"Appointment on {date:yyyy-MM-dd}");

// Alpha (letters only)
app.MapGet("/tags/{tag:alpha}", (string tag) =>
    $"Tag: {tag}");

// Multiple constraints (AND)
app.MapGet("/archive/{year:int:min(2000):max(2030)}", (int year) =>
    $"Archive for {year}");

app.Run();

路线模板

路径模板定义 URL 模式以匹配 : |------------|---------|---------| | int | {id:int} | 123, -456 | | bool | {active:bool}基本模板 | datetime | {date:datetime} | 2024-01-15 | | decimal | {price:decimal} | 19.99 | | double | {lat:double} | 51.5074 | | float | {temp:float} | 98.6 | | guid | {id:guid}实例: | long | {size:long} | 9223372036854775807 | | minlength(n) | {name:minlength(3)}路线限制 | maxlength(n) | {name:maxlength(5)}制约因素确保参数与具体模式相匹配: | length(n) | {code:length(5)}内在制约因素: | min(n) | {age:min(18)} | 18, 19, 100 | | max(n) | {age:max(120)} | 1, 100, 120 | | range(min,max) | {num:range(1,10)} | 1, 5, 10 | | alpha | {tag:alpha}限制 示例 配對 | regex(pattern) | {code:regex(^[A-Z]{3}$)}真实的,虚假的 | required | {id:required}550e8400-e29b-41d4-a716-4466555440000

*** abc, abcd * * abc * abc * abcd * abc * abcc * abcd * abcc * abcc * abcc * abcc * abcd * abcc * abc, abcd * abcd * abcc, abcd * abc * abcc, abcd * abc, abcd * * abc, abcd * abc, abcd * * abc, abcd * * abc, abcd * * abc, abcd * * aabc, abcd * * abc, abcd * * abc, abc, abcd * * * abc, abc, acd * * abc, abc, acd * a, a, acd * * acd * * * * * a aac, a, ac, acd * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ***

# ✅ Matches {id:int}
GET /users/123          → Success

# ❌ Doesn't match {id:int}
GET /users/abc          → 404

# ✅ Matches {id:int:min(1)}
GET /products/5         → Success

# ❌ Doesn't match {id:int:min(1)}
GET /products/0         → 404
GET /products/-1        → 404

* abc, abcde * * abc, abcde * abc * abc * abc, abcde * abc * abc * abc, abcde * abc * abc * abc, abcde * abcc, abcde * abccc * abc * abc, abcde * abc, abcde * abc * abc * abc, abc, abcde * abc * abc, abcde * a abc, abcde * * a abc * abc, ac, acde * a abccc * a abc, ac * abc * abc, ac, acde * aac * abc, ac, acd * aac * aac * a aac * a a a ac * ac * ac * * a aac * aac * aac * aac * * aabc * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * aaaaaabc, aabc, aac, * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

  • abcde * * * abcde * * * abcde * * abcde * * abcde * * abcde * * abcde * * abcde * * abcde * abcde * * abcde * * abcde * abcde * * abcde * * abcde * * * * abcde * * acde * * * * abcde * * * * * aacde * * * * * aacde * * * * * * aacde * * * * * * * * * a aacde * * * * * * * * * aacde * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Custom constraint: validates slugs
public class SlugConstraint : IRouteConstraint
{
    private readonly Regex _regex = new Regex(@"^[a-z0-9]+(?:-[a-z0-9]+)*$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out var value) && value != null)
        {
            var slug = value.ToString();
            return !string.IsNullOrEmpty(slug) && _regex.IsMatch(slug);
        }

        return false;
    }
}

// Register the constraint
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("slug", typeof(SlugConstraint));
});

var app = builder.Build();

// Use the custom constraint
app.MapGet("/blog/{slug:slug}", (string slug) =>
    $"Blog post: {slug}");

app.Run();

*** abc, XYZ ***

GET /blog/my-first-post          → ✅ "Blog post: my-first-post"
GET /blog/hello-world-2024       → ✅ "Blog post: hello-world-2024"
GET /blog/My_Invalid_Slug        → ❌ 404
GET /blog/has spaces             → ❌ 404

ABC XYZ ABC XYZ ABC ABC XYZ ABC XYZ ABC XYZ ABC ABC XYZ ABC XYZ ABC ABC XYZ ABC XYZ ABC ABC ABC XYZ SAZ ABC ABC XYZ ABC ABC ABC XYZ ABZ ABC ABC XYZ ABZ ABC XYZ ABZ ABC ABC XYZ ABZ ABC ABZ ABZ ABZ ABC ABC XYZ ABZ ABZ ABZ ABZ ABC ABC XYZ ABZ ABZ ABZ ABC ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ XYZ XZ XZ XZ XZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ ABZ AB AB ABZ ABZ ABZ ABZ ABZ AB AB ABZ ABZ AB AB AB ABZ ABZ ABZ ABZ AB AB AB AB AB AB AB AB AB ABZ ABZ ABZ AB AB AB AB AB AB AB AB ABZ ABZ ABZ ABZ AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB AB

  • 任何非空值 * * * 任何非空值 * * * * 任何非空值 * * * 任何非空值 *
graph TD
    A[Incoming Request] --> B{Route Matching}
    B --> C[Order by Priority]
    C --> D[ Literal segments highest]
    D --> E[ Constrained parameters]
    E --> F[ Unconstrained parameters]
    F --> G[ Optional parameters]
    G --> H[ Catch-all parameters lowest]
    H --> I[Select first match]


测试制约因素:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Priority 1: Literal segments (highest priority)
app.MapGet("/users/admin", () => "Admin user endpoint");

// Priority 2: Constrained parameter
app.MapGet("/users/{id:int}", (int id) => $"User {id}");

// Priority 3: Unconstrained parameter
app.MapGet("/users/{username}", (string username) => $"User @{username}");

// Priority 4: Optional parameter
app.MapGet("/users/{id:int?}", (int? id) =>
    id.HasValue ? $"User {id}" : "All users");

// Priority 5: Catch-all (lowest priority)
app.MapGet("/users/{*path}", (string path) => $"Catch-all: {path}");

app.Run();

自定义路线限制

GET /users/admin        → "Admin user endpoint" (literal match)
GET /users/123          → "User 123" (constrained parameter)
GET /users/john         → "User @john" (unconstrained parameter)
GET /users/admin/test   → "Catch-all: admin/test" (catch-all)

创建您自己的限制 :

app.MapGet("/products/{id}", (int id) => $"Product {id}")
    .WithOrder(1);

app.MapGet("/products/featured", () => "Featured products")
    .WithOrder(0); // Lower number = higher priority

测试 :

路线优先事项和排序

当多条线路匹配时, ASP.NET Core使用优先系统:

app.MapGet("/users/{id}/posts/{postId}", (int id, int postId) =>
    $"User {id}, Post {postId}");

示例:

app.MapGet("/search", (string? q, int page = 1, int pageSize = 10) =>
    $"Query: {q}, Page: {page}, Size: {pageSize}");

// GET /search?q=aspnet&page=2&pageSize=20

匹配行为 :

app.MapGet("/api/data", ([FromHeader(Name = "X-API-Key")] string apiKey) =>
    $"API Key: {apiKey}");

您也可设定明确的命令:

app.MapPost("/users", ([FromBody] User user) =>
{
    return Results.Created($"/users/{user.Id}", user);
});

public record User(int Id, string Name, string Email);

装订参数

app.MapGet("/data", (IMyService service) =>
{
    var data = service.GetData();
    return data;
});

ASP.NET核心可从多种来源将路线参数捆绑在一起:

app.MapGet("/info", (HttpContext context) =>
{
    var userAgent = context.Request.Headers["User-Agent"];
    var ip = context.Connection.RemoteIpAddress;

    return new
    {
        UserAgent = userAgent.ToString(),
        IpAddress = ip?.ToString()
    };
});

路线价值

public record CreatePostRequest(string Title, string Content, List<string> Tags);

app.MapPost("/api/posts",
    async (
        [FromRoute] int userId,
        [FromQuery] bool publish,
        [FromBody] CreatePostRequest request,
        [FromHeader(Name = "X-Request-ID")] string requestId,
        [FromServices] IPostService postService,
        HttpContext context) =>
    {
        var post = await postService.CreatePostAsync(
            userId,
            request.Title,
            request.Content,
            request.Tags,
            publish
        );

        context.Response.Headers["X-Request-ID"] = requestId;

        return Results.Created($"/api/posts/{post.Id}", post);
    });

// POST /api/posts?publish=true
// Header: X-Request-ID: abc123
// Body: { "title": "Hello", "content": "World", "tags": ["aspnet", "routing"] }

查询字符串

页眉

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthorization();

// Add metadata with extension methods
app.MapGet("/public", () => "Public endpoint")
    .WithName("GetPublic")
    .WithDisplayName("Public Endpoint")
    .WithDescription("A public endpoint that anyone can access")
    .WithTags("Public");

app.MapGet("/private", () => "Private endpoint")
    .RequireAuthorization() // Adds [Authorize] metadata
    .WithName("GetPrivate");

app.MapGet("/admin", () => "Admin only")
    .RequireAuthorization("AdminPolicy");

// Access metadata
app.Map("/metadata", (IEndpointRouteBuilder endpoints) =>
{
    var dataSources = endpoints.DataSources;

    var endpointList = dataSources
        .SelectMany(ds => ds.Endpoints)
        .OfType<RouteEndpoint>()
        .Select(e => new
        {
            Name = e.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName,
            DisplayName = e.DisplayName,
            Route = e.RoutePattern.RawText,
            RequiresAuth = e.Metadata.GetMetadata<IAuthorizeData>() != null
        });

    return endpointList;
});

app.Run();

机构机构 机构 机构 机构 机构 机构 机构 机构 机构 机构 机构/metadata:

[
  {
    "name": "GetPublic",
    "displayName": "Public Endpoint",
    "route": "/public",
    "requiresAuth": false
  },
  {
    "name": "GetPrivate",
    "displayName": null,
    "route": "/private",
    "requiresAuth": true
  },
  {
    "name": null,
    "displayName": null,
    "route": "/admin",
    "requiresAuth": true
  }
]

服务(依赖性注射)

HttpConttext 文本

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Create a group with common prefix
var api = app.MapGroup("/api");

// All these routes are prefixed with /api
api.MapGet("/users", () => "Get all users");
api.MapGet("/users/{id}", (int id) => $"Get user {id}");
api.MapPost("/users", () => "Create user");

// Nested groups
var v1 = api.MapGroup("/v1");
v1.MapGet("/products", () => "V1 products");

var v2 = api.MapGroup("/v2");
v2.MapGet("/products", () => "V2 products");

// Groups with shared metadata
var adminApi = app.MapGroup("/admin")
    .RequireAuthorization("AdminPolicy")
    .WithTags("Admin");

adminApi.MapGet("/users", () => "Admin: Get users");
adminApi.MapDelete("/users/{id}", (int id) => $"Admin: Delete user {id}");

app.Run();

复杂装订示例

GET    /api/users
GET    /api/users/{id}
POST   /api/users
GET    /api/v1/products
GET    /api/v2/products
GET    /admin/users          [Authorize(AdminPolicy)]
DELETE /admin/users/{id}     [Authorize(AdminPolicy)]

端点元元数据

flowchart TD
    Root[Root] --> Api[api]
    Root --> Admin[admin auth]

    Api --> Users1[GET users]
    Api --> Users2[GET users id]
    Api --> Users3[POST users]
    Api --> V1[v1]
    Api --> V2[v2]

    V1 --> V1Products[GET products]
    V2 --> V2Products[GET products]

    Admin --> AdminUsers[GET users]
    Admin --> AdminDelete[DELETE users id]

端点可以有元数据来描述这些端点:

产出产出来自

app.MapGet("/data.json", () => Results.Json(new { data = "JSON" }));
app.MapGet("/data.xml", () => Results.Text("<data>XML</data>", "application/xml"));
app.MapGet("/data", (HttpContext context) =>
{
    var accept = context.Request.Headers["Accept"].ToString();

    if (accept.Contains("application/json"))
        return Results.Json(new { data = "JSON" });

    if (accept.Contains("application/xml"))
        return Results.Text("<data>XML</data>", "application/xml");

    return Results.Json(new { data = "Default JSON" });
});

路线组

// English routes
app.MapGet("/en/products", () => "Products");
app.MapGet("/en/about", () => "About Us");

// Spanish routes
app.MapGet("/es/productos", () => "Productos");
app.MapGet("/es/acerca", () => "Acerca de Nosotros");

// Dynamic culture from route
app.MapGet("/{culture}/home", (string culture) =>
{
    CultureInfo.CurrentCulture = new CultureInfo(culture);
    return $"Home (Culture: {culture})";
});

共享配置的分组相关端点 :

// Version in route
var v1 = app.MapGroup("/api/v1");
v1.MapGet("/users", () => new { version = 1, users = new[] { "Alice", "Bob" } });

var v2 = app.MapGroup("/api/v2");
v2.MapGet("/users", () => new { version = 2, users = new[]
{
    new { id = 1, name = "Alice" },
    new { id = 2, name = "Bob" }
}});

// Version in query string
app.MapGet("/api/users", (int version = 1) =>
{
    if (version == 2)
        return Results.Json(new { version = 2, users = new[]
        {
            new { id = 1, name = "Alice" },
            new { id = 2, name = "Bob" }
        }});

    return Results.Json(new { version = 1, users = new[] { "Alice", "Bob" } });
});

// Version in header
app.MapGet("/api/data", (HttpContext context) =>
{
    var version = context.Request.Headers["X-API-Version"].FirstOrDefault() ?? "1";
~~~~
    return version switch
    {
        "2" => Results.Json(new { version = 2, data = "New format" }),
        _ => Results.Json(new { version = 1, data = "Old format" })
    };
});

创建路径 :

app.Use(async (context, next) =>
{
    var host = context.Request.Host.Host;

    if (host.StartsWith("api."))
    {
        context.Request.RouteValues["area"] = "api";
    }
    else if (host.StartsWith("admin."))
    {
        context.Request.RouteValues["area"] = "admin";
    }

    await next(context);
});

app.MapGet("/", (HttpContext context) =>
{
    var area = context.Request.RouteValues["area"]?.ToString() ?? "main";

    return area switch
    {
        "api" => "API area",
        "admin" => "Admin area",
        _ => "Main area"
    };
});

可视化:

高级分流方案

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users", () => "Users");
app.MapPost("/users", () => "Create user");
app.MapGet("/products/{id:int}", (int id) => $"Product {id}");

// Endpoint to inspect all routes
app.MapGet("/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
{
    var endpoints = endpointSources
        .SelectMany(es => es.Endpoints)
        .OfType<RouteEndpoint>();

    var routes = endpoints.Select(e => new
    {
        Name = e.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName,
        Pattern = e.RoutePattern.RawText,
        Order = e.Order,
        HttpMethods = e.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods,
        Metadata = e.Metadata.Select(m => m.GetType().Name)
    });

    return Results.Json(routes);
});

app.Run();

按路线谈判的内容/routes:

[
  {
    "name": null,
    "pattern": "/users",
    "order": 0,
    "httpMethods": ["GET"],
    "metadata": ["HttpMethodMetadata", ...]
  },
  {
    "name": null,
    "pattern": "/users",
    "order": 0,
    "httpMethods": ["POST"],
    "metadata": ["HttpMethodMetadata", ...]
  },
  {
    "name": null,
    "pattern": "/products/{id:int}",
    "order": 0,
    "httpMethods": ["GET"],
    "metadata": ["HttpMethodMetadata", "RoutePatternMetadata", ...]
  }
]

地方化路线

已版本的 APPs

// Routes are compiled and cached at startup
// This is fast:
for (int i = 0; i < 1000; i++)
{
    app.MapGet($"/endpoint{i}", () => $"Endpoint {i}");
}

// At runtime, route matching is O(1) for most cases

子域路由

路径调试

app.MapGet("/users/{id}", (int id) => $"User {id}")
    .WithName("GetUser");

app.MapGet("/generate-link", (LinkGenerator linkGenerator, HttpContext context) =>
{
    var url = linkGenerator.GetPathByName("GetUser", new { id = 123 });
    // url = "/users/123"

    var absoluteUrl = linkGenerator.GetUriByName(context, "GetUser", new { id = 123 });
    // absoluteUrl = "https://localhost:5001/users/123"

    return new { url, absoluteUrl };
});

查看所有注册路线 :

  • 产出产出来自
  • 绩效考量
  • 缓缓路线
  • 链接生成
  • 从路径生成 URL :
  • 密钥外出
  • 运行是一个两阶段过程:匹配(使用运行)和执行(终点)
  • 路线模板模板 支持参数、制约因素、可选值和总可捕量
  • 内置制约因素验证参数类型和模式
  • 海关约束提供针对具体应用程序的验证

路线优先事项:字面上 > 受限制参数 > > 不受限制 > > 任选 > 全面可选 >

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.