理解 ASP.NET 核心请求和响应管道 @-Part {4:Routing and Endpoints
一. 导言导言 导言 导言 导言
我们通过主机层 , 探索了凯斯特雷尔・, 和掌握的中继器械 MS K3 现在我们到了一个关键的问题 路由 和 终点.
Routing 是一个匹配 HTTP 收到的请求与可执行代码“.” 终端点的匹配机制。 “—” 是目的地@MSK1}它们构成一个强大的“MS K3” 灵活系统,支持从简单路由处理器到复杂的 MVC 应用程序等所有内容
在此部分, , @ we'}将探索路由如何运作@,#如何注册和执行端点 @,}以及您如何能够利用这个系统来构建复杂的应用程序_.{
请注意::}这是我在AI &/’的实验的一部分, 这是用来花费“$1000”Calude Code Web credits “.” I''ve fed this BUNCH of pages, 我的理解是‘,’
区域规则的演变
ASP=.NETCore'}路线已发生重大改变 :
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
传统路程 @(_Pre-3.0):}@
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
终点运行中 @(3.0+): @%
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
现代最小APIs @ (6.0+): @
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 stroke:#ef4444,stroke-width:3px
style H stroke:#10b981,stroke-width:3px
style J stroke:#6366f1,stroke-width:3px
阶段=1: 匹配路径(UseRouting())
中继器件路由检查请求并尝试将其匹配到已注册的端点@: @
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();
流程图:
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
2: 终点执行阶段
端点中继器件执行匹配的端点@:
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
路线模板
路由模板定义 URL 模式以匹配 @ : {
基本模板
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();
例如 @:
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();
内建的- 限制
| Q匹配@ | |||
|---|---|---|---|
int |
{id:int} |
123, -456 | |
bool |
{active:bool} {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}真的? |
||
datetime |
{date:datetime} |
2024-01-15 | |
decimal |
{price:decimal} |
19.99 | |
double |
{lat:double} |
51.5074 | |
float |
{temp:float} |
98.6 | |
guid |
{id:guid} |
@550e @MS K2e}29b-41d□4-a{716-446655440000~ | |
long |
{size:long} |
9223372036854775807 | |
minlength(n) |
{name:minlength(3)} @ |
@ abc+,_abcd# | # |
maxlength(n) |
{name:maxlength(5)} @ |
@ abc+,_abcde @ | # |
length(n) |
{code:length(5)} @ |
# abcde # | # |
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} @ |
@ abc+,} XYZ @ MPK2# | |
regex(pattern) |
{code:regex(^[A-Z]{3}$)} |
ABC,XYZMS K2 | |
required |
{id:required} @ |
任何非{-} 空值\ MS K2 |
测试限制 @:
# ✅ 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
自定义路线限制
创建您自己的限制@: @%
// 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();
测试:
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
路线优先事项和排序
当多条路线匹配,ASP}.NET核心使用优先系统时
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核心可以从多个来源捆绑路线参数@:
路线价值
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;
});
HttpContText 文本
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
}
]
路线组
共享配置的分组相关端点@: @%
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})";
});
已版本的 APPs
// 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", ...]
}
]
业绩考量
缓存路线
// 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
链接生成
从路径生成 URL @ :
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 };
});
密钥外出
- Routing 是一个匹配 @(_usesRouting@)}与执行 @MS K4_endpoints)}的双关节进程+:}
- 路由模板支持参数@,限制 @,可选值 @MS K2和 catch-alls
- 校验参数类型和模式
- 自定义限制提供应用程序@-}特定验证
- 优先路由@: @ literals @ >} 限制参数 @ I>_ 未受约束的 @ MPK3可选 @ >} captain -all
- 参数可以绑定于以下路径@,查询字符串 @,}标题{,body_,}及服务
- 端点元数据使授权和文件等特征得以实现
- 相关终点的路线组简化配置
- 链接生成从路径名称创建 URL
- 路由系统在业绩方面高度优化
路由和终点构成 HTTP 请求与您应用程序代码@ . @% 掌握此系统之间连接的桥梁, 您可以精确控制 URL 如何绘制功能=.}