This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Sunday, 09 November 2025
We hebben gereisd door de hosting laag, onderzocht Kestrel, en gemasterde middleware. Nu komen we op een cruciale vraag: als een verzoek maakt het door uw middleware pijplijn, hoe weet ASP.NET Core welke code uit te voeren? Het antwoord is routering en eindpunten.
Routing is het mechanisme dat overeenkomt met binnenkomende HTTP-verzoeken naar uitvoerbare code. Endpoints zijn de bestemmingen en de eigenlijke code die uitvoert. Samen vormen ze een krachtig, flexibel systeem dat alles ondersteunt van eenvoudige route handlers tot complexe MVC-toepassingen.
In dit deel zullen we onderzoeken hoe routing werkt, hoe endpoints worden geregistreerd en uitgevoerd, en hoe je dit systeem kunt gebruiken om geavanceerde toepassingen te bouwen.
OPMERKING: Dit is onderdeel van mijn experimenten met AI / een manier om $1000 Calude Code Web credits uit te geven. Ik heb dit een BUNCH van papieren, mijn begrip, vragen die ik moest genereren van dit artikel. Het is leuk en vult een gat dat ik niet ergens anders heb gezien gevuld.
De routering van ASP.NET Core is aanzienlijk geëvolueerd:
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
Traditionele Routing (pre-3.0):
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
Eindpunt-routing (3.0+):
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
Moderne minimale API's (6.0+):
app.MapGet("/users/{id}", (int id) => $"User {id}");
Eindpuntrouting werkt in twee fasen:
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
UseRouting())De routering middleware onderzoekt het verzoek en probeert het aan te passen aan een geregistreerd eindpunt:
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();
Stroomdiagram:
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
Het eindpunt middleware voert het overeenkomende eindpunt uit:
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();
Uitvoer:
Before endpoint execution
Executing endpoint
After endpoint execution
Routesjablonen definiëren het URL-patroon dat overeenkomt met:
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();
Bijvoorbeeld:
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"
Beperkingen zorgen ervoor dat parameters overeenkomen met specifieke patronen:
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();
Ingebouwde beperkingen:
Contraint Example Matches
|------------|---------|---------|
| int | {id:int} | 123, -456 |
| bool | {active:bool} Waar, vals.
| datetime | {date:datetime} | 2024-01-15 |
| decimal | {price:decimal} | 19.99 |
| double | {lat:double} | 51.5074 |
| float | {temp:float} | 98.6 |
| guid | {id:guid} 550e8400-e29b-41d4-a716-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
| regex(pattern) | {code:regex(^[A-Z]{3}$)} ABC, XYZ
| required | {id:required} Elke niet-leegte waarde
Testbeperkingen:
# ✅ 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
Creëer uw eigen beperkingen:
// 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();
Testen:
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
Wanneer meerdere routes overeenkomen, gebruikt ASP.NET Core een prioriteitssysteem:
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]
Voorbeeld:
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();
Bijpassend gedrag:
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)
U kunt ook een expliciete volgorde instellen:
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 kan routeparameters binden vanuit meerdere bronnen:
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;
});
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"] }
Eindpunten kunnen metadata hebben die ze beschrijven:
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();
Uitvoer van /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
}
]
Groepsgerelateerde eindpunten met gedeelde configuratie:
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();
Aangemaakte routes:
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)]
Visualisatie:
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"
};
});
Bekijk alle geregistreerde routes:
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();
Uitvoer van /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
Aanmaken van URL's van routes:
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 en eindpunten vormen de brug tussen HTTP-verzoeken en uw toepassingscode. Het beheersen van dit systeem geeft u nauwkeurige controle over hoe URL's kaart naar functionaliteit.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.