Endpoint Metadata, Route Constraints & Templates

15 questions found

What is a route template in ASP.NET Core, and how do route parameters like {id} work within one?

Beginner
A route template defines the URL pattern an endpoint matches, with curly-brace segments ({id}) representing route parameters extracted from the actual request URL and made available to the handler -- static segments (like 'products') must match literally, while parameter segments capture whatever value appears in that position of the URL.
app.MapGet("/products/{id}", (int id) => $"Product {id}");

// GET /products/42 -> id = 42
// GET /products/abc -> fails to bind since 'abc' isn't a valid int
Real-world example An e-commerce API's route template /orders/{orderId}/items/{itemId} extracts both the order and item identifiers directly from the URL path, making them available as strongly-typed parameters to the handler.

Common follow-ups: What happens if a URL segment doesn't match the expected parameter type?;How do you make a route parameter optional?

Controllers vs Minimal APIs;Model Binding & Validation

How do route constraints (like {id:int} or {id:guid}) restrict which requests match a given route, and why are they useful?

Intermediate
Route constraints, appended after a colon in the route parameter syntax, restrict matching to values satisfying a specific type or pattern -- {id:int} only matches numeric segments, {id:guid} only matches valid GUID strings, {slug:alpha} only matches alphabetic characters -- letting the routing system reject non-matching requests (falling through to another route or a 404) before the constraint mismatch ever reaches your handler as a confusing binding failure.
app.MapGet("/products/{id:int}", (int id) => GetById(id));       // only matches numeric IDs
app.MapGet("/products/{slug:alpha}", (string slug) => GetBySlug(slug));  // only matches alphabetic slugs

// GET /products/42 -> matches the first route
// GET /products/wireless-mouse -> matches the second route
Real-world example An API distinguishes between numeric product IDs and human-readable slugs using route constraints, letting /products/42 and /products/wireless-mouse route to two entirely different handlers based purely on the URL's shape.

Common follow-ups: What built-in constraints does ASP.NET Core provide (int, guid, alpha, minlength, etc.)?;How would you write a custom route constraint for a domain-specific pattern?

Model Binding & Validation;Content Negotiation & Output Formatters

How would you implement a custom route constraint (IRouteConstraint) for a domain-specific validation pattern, like a product SKU format?

Advanced
Implement IRouteConstraint's Match method containing the custom matching logic (like a regex check against a specific SKU pattern), register it with a name in the RouteOptions.ConstraintMap, and reference it in route templates via that name -- letting invalid-format URLs fail to match at the routing level rather than reaching the handler and requiring manual validation there.
public class SkuConstraint : IRouteConstraint {
    public bool Match(HttpContext? context, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection direction) {
        return values.TryGetValue(routeKey, out var value) &&
               Regex.IsMatch(value?.ToString() ?? "", @"^[A-Z]{3}-\d{4}$");
    }
}

builder.Services.Configure<RouteOptions>(options => options.ConstraintMap.Add("sku", typeof(SkuConstraint)));

app.MapGet("/products/{sku:sku}", (string sku) => GetBySku(sku));
Real-world example A warehouse inventory API rejects malformed SKU-format URLs (like /products/invalid) at the routing level using a custom constraint, before the request ever reaches handler code that would otherwise need to manually validate and reject the malformed input.

Common follow-ups: How does a custom route constraint's performance compare to validating inside the handler?;What's the risk of overly complex regex logic inside a route constraint?

Model Binding & Validation;Diagnostics & Performance

What is endpoint metadata, and how do attributes like [Authorize] and custom metadata attach to a specific route for later inspection by middleware?

Intermediate
Endpoint metadata is arbitrary data attached to a matched route (via attributes on controller actions, or .WithMetadata()/.RequireAuthorization() fluent calls on minimal API endpoints), stored in the Endpoint object that middleware running after UseRouting can inspect via HttpContext.GetEndpoint() -- this is precisely how UseAuthorization knows an endpoint requires authentication: it reads the [Authorize] attribute's metadata attached to the matched endpoint.
app.MapGet("/admin/reports", GetReports)
   .RequireAuthorization()
   .WithMetadata(new AuditLogAttribute("SensitiveDataAccess"));

// Custom middleware can inspect this metadata:
app.Use(async (context, next) => {
    var endpoint = context.GetEndpoint();
    var auditAttr = endpoint?.Metadata.GetMetadata<AuditLogAttribute>();
    if (auditAttr != null) { /* log access */ }
    await next();
});
Real-world example A custom auditing middleware inspects each matched endpoint's metadata for a custom AuditLogAttribute, automatically logging access to any endpoint tagged with it, without needing to hardcode a list of sensitive routes anywhere else in the codebase.

Common follow-ups: How does this metadata-based approach avoid tight coupling between middleware and specific routes?;What built-in metadata types does ASP.NET Core itself attach to endpoints?

ASP.NET Core Middleware & Request Pipeline;Filters

How do you write custom middleware or an endpoint filter that reads endpoint metadata to conditionally alter its behavior per-route?

Advanced
Custom middleware calls context.GetEndpoint()?.Metadata.GetMetadata<T>() to check for a specific metadata type attached to the matched endpoint, letting one piece of middleware behave differently for different routes based on declarative metadata rather than hardcoded route-path string comparisons, which is fragile and doesn't survive route template changes.
public record RateLimitOverrideAttribute(int MaxRequests);

app.MapGet("/api/expensive-report", GetReport)
   .WithMetadata(new RateLimitOverrideAttribute(MaxRequests: 5));

// Custom middleware reads this metadata to apply a stricter limit just for this route
app.Use(async (context, next) => {
    var override_ = context.GetEndpoint()?.Metadata.GetMetadata<RateLimitOverrideAttribute>();
    var limit = override_?.MaxRequests ?? DefaultLimit;
    // apply 'limit' for this specific request
    await next();
});
Real-world example A rate-limiting middleware applies a much stricter request limit specifically to an expensive reporting endpoint by reading a custom RateLimitOverrideAttribute attached as endpoint metadata, rather than hardcoding a special case for that route's URL string.

Common follow-ups: Why is metadata-based route identification more maintainable than string-comparing request paths?;How would you combine metadata from multiple attributes attached to the same endpoint?

Rate Limiting;Filters

How does route template precedence work when multiple routes could potentially match the same incoming URL?

Intermediate
ASP.NET Core's routing system ranks candidate routes by specificity (more literal segments and constraints rank higher than wildcard/parameter segments), selecting the most specific match -- a route like /products/featured (fully literal) takes precedence over /products/{id} (parameterized) for a request to /products/featured, ensuring the more specific, intentional route wins rather than an ambiguous match resulting in unpredictable behavior.
app.MapGet("/products/featured", () => GetFeaturedProducts());  // more specific, higher precedence
app.MapGet("/products/{id}", (string id) => GetById(id));         // less specific, lower precedence

// GET /products/featured matches the FIRST route despite also technically matching the second's pattern
Real-world example An API correctly routes /products/featured to a dedicated featured-products handler instead of accidentally matching the more general /products/{id} pattern (which would try to parse 'featured' as an ID), thanks to ASP.NET Core's specificity-based precedence.

Common follow-ups: What happens if two routes have identical specificity and both could match?;How do route constraints affect this precedence calculation?

Controllers vs Minimal APIs;Error Handling

How would you implement a catch-all route parameter to handle a variable-depth URL path, like a file browser or wiki-style page hierarchy?

Advanced
A catch-all route parameter, denoted with an asterisk prefix ({*path}), captures the remainder of the URL path (including any slashes) as a single string value, rather than requiring an exact number of fixed segments -- useful for hierarchical resources like nested file paths or wiki pages where the depth isn't fixed in advance.
app.MapGet("/wiki/{*pagePath}", (string pagePath) => {
    // GET /wiki/programming/csharp/generics -> pagePath = "programming/csharp/generics"
    return GetWikiPage(pagePath);
});
Real-world example A documentation site's wiki feature uses a catch-all route to support arbitrarily nested page hierarchies (/wiki/category/subcategory/page-name) without needing to register a separate route template for every possible nesting depth.

Common follow-ups: How does URL encoding of slashes within a catch-all segment get handled?;What are the security implications of accepting arbitrary path-like input this way (path traversal)?

File Uploads & Streaming Large Files;Model Binding & Validation

How do you define optional route parameters, and what's the difference between an optional parameter and one with a default value?

Intermediate
Appending a question mark to a route parameter ({id?}) makes it optional, resulting in a null/default value in the handler if the URL segment is omitted entirely. A default value (using = syntax in older routing conventions, or a C# default parameter value in minimal APIs) instead provides a specific fallback value rather than null when the segment is absent -- both let one route template match URLs with or without that particular segment present.
// Optional: results in null if omitted
app.MapGet("/products/{category?}", (string? category) =>
    category is null ? GetAllProducts() : GetByCategory(category));

// Default value: results in "all" if omitted, via C# default parameter
app.MapGet("/products/{category}", (string category = "all") => GetByCategory(category));
Real-world example A product listing endpoint supports both /products (showing everything) and /products/electronics (filtered) using a single optional route parameter, avoiding the need for two separately registered route templates.

Common follow-ups: How does an optional route parameter interact with route constraints applied to it?;What happens if an optional parameter appears before a required one in the template?

Model Binding & Validation;RESTful Web APIs & Controllers

How would you implement route-based API versioning using route templates and constraints without a dedicated versioning library?

Advanced
A hand-rolled approach embeds the version directly in the route template ({version} segment, potentially constrained to a specific set of allowed values via a custom constraint or regex constraint) and dispatches to different handler logic based on the captured version value -- while workable for simple cases, this reimplements much of what a dedicated library like Asp.Versioning already provides more robustly (deprecation headers, Swagger integration), so it's generally only justified for very simple versioning needs.
app.MapGet("/api/v{version:regex(^(1|2)$)}/products/{id}", (string version, int id) => {
    return version == "2" ? GetProductV2(id) : GetProductV1(id);
});
Real-world example A small internal API implements minimal hand-rolled route-based versioning using a regex constraint restricting the version segment to '1' or '2', appropriate for its narrow, simple versioning needs without pulling in a full versioning library's overhead.

Common follow-ups: At what point does hand-rolled versioning become worth replacing with a dedicated library like Asp.Versioning?;How does this approach handle version-specific Swagger documentation?

API Versioning;API Documentation with Swagger/OpenAPI

What is route grouping via MapGroup, and how does it let you apply shared route prefixes, metadata, and filters to a set of related minimal API endpoints?

Intermediate
MapGroup("/prefix") returns a RouteGroupBuilder that automatically prepends the given prefix to every endpoint mapped within it, and any metadata, authorization requirements, or filters applied to the group (via .RequireAuthorization(), .AddEndpointFilter(), .WithTags()) automatically apply to all endpoints in that group, reducing repetition for a cohesive set of related routes.
var productsGroup = app.MapGroup("/api/products")
    .RequireAuthorization()
    .WithTags("Products");

productsGroup.MapGet("/", GetAll);           // -> /api/products, requires auth
productsGroup.MapGet("/{id}", GetById);      // -> /api/products/{id}, requires auth
productsGroup.MapPost("/", Create);          // -> /api/products, requires auth
Real-world example A minimal API organizes all product-related endpoints under one MapGroup call, applying shared authorization and Swagger tagging once at the group level instead of repeating the same configuration on every individual endpoint registration.

Common follow-ups: Can route groups be nested for even finer-grained shared configuration?;How does grouping interact with API versioning's own grouping mechanisms?

Controllers vs Minimal APIs;API Documentation with Swagger/OpenAPI

Showing 1–10 of 15