You are viewing a preview of this lesson. Sign in to start learning
Back to ASP.NET with .NET 10

Endpoint Creation & Routing

Define endpoints with lambda expressions and route patterns

Endpoint Creation & Routing in ASP.NET with .NET 10

Master endpoint creation and routing in ASP.NET with .NET 10 through free flashcards and practical coding exercises. This lesson covers minimal APIs, route patterns, HTTP verb attributes, route parameters, and route constraintsโ€”essential concepts for building modern web applications and RESTful services.

Welcome ๐Ÿ’ป

Welcome to the world of endpoint creation and routing in ASP.NET! If you've ever wondered how a web application knows which code to execute when you visit a URL like /products/123 or submit a form to /api/orders, you're about to discover the magic behind it. Routing is the mechanism that maps incoming HTTP requests to specific handlers or endpoints in your application.

With .NET 10, Microsoft has refined and expanded routing capabilities, making it easier than ever to create clean, maintainable APIs using minimal APIs alongside traditional controller-based approaches. Whether you're building a microservice, a RESTful API, or a full-featured web application, understanding routing is fundamental to your success as an ASP.NET developer.

In this lesson, we'll explore how to create endpoints using both minimal APIs and controllers, define sophisticated route patterns, handle different HTTP methods, extract data from URLs, and apply constraints to ensure your routes behave exactly as intended. Let's dive in! ๐Ÿš€

Core Concepts ๐Ÿ“š

What is Routing?

Routing is the process of matching an incoming HTTP request to a specific endpoint in your application. Think of it as a sophisticated postal system: just as mail gets delivered to the right address based on the address label, HTTP requests get "delivered" to the right code based on the URL and HTTP method.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚            REQUEST ROUTING FLOW                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

    ๐Ÿ“ฑ Client Request
    "GET /api/products/123"
           |
           โ†“
    ๐Ÿ” Routing Middleware
    (Pattern Matching)
           |
      โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
      โ†“         โ†“
   โœ… Match   โŒ No Match
      |         |
      |         โ†“
      |    ๐Ÿ”ด 404 Not Found
      |
      โ†“
   ๐Ÿ’ผ Endpoint Handler
   (Your Code Executes)
      |
      โ†“
   ๐Ÿ“ค Response
   "{ id: 123, name: 'Widget' }"
      |
      โ†“
   ๐Ÿ“ฑ Client Receives Response

Minimal APIs vs Controller-Based Routing ๐ŸŽฏ

.NET 6+ introduced Minimal APIs, a streamlined way to create endpoints without the ceremony of controllers. .NET 10 continues to enhance this approach.

Aspect Minimal APIs Controller-Based
Code Style Functional, concise Object-oriented, structured
Boilerplate Minimal More ceremony
Best For Microservices, simple APIs Large applications, complex logic
File Location Program.cs Controllers folder
Dependency Injection Method parameters Constructor injection

Creating Endpoints with Minimal APIs ๐Ÿ”ง

In .NET 10, creating endpoints is remarkably simple. You use the WebApplication object to map HTTP methods to handlers:

Basic endpoint structure:

app.MapGet("/route", (parameters) => { /* handler code */ });

The routing middleware examines each incoming request and attempts to match it against registered routes in the order they were defined.

HTTP Verb Mapping ๐ŸŒ

ASP.NET provides dedicated methods for each HTTP verb:

HTTP Method Minimal API Purpose Example Use
GET MapGet Retrieve data Fetch product list
POST MapPost Create new resource Add new customer
PUT MapPut Update entire resource Update user profile
PATCH MapPatch Partial update Change email address
DELETE MapDelete Remove resource Delete order

Route Parameters ๐Ÿ“

Route parameters allow you to capture values from the URL. They're defined using curly braces {parameter} in the route pattern:

app.MapGet("/products/{id}", (int id) => { /* use id */ });

When a request comes in for /products/42, the value 42 is automatically extracted and passed to your handler as the id parameter.

Parameter binding is automatic for:

  • Route parameters (from URL path)
  • Query string parameters (from URL after ?)
  • Headers
  • Request body (for POST/PUT)
  • Services (dependency injection)

๐Ÿ’ก Tip: Parameter names in your handler must match the route template names (case-insensitive).

Route Constraints ๐Ÿšง

Route constraints ensure that route parameters meet specific criteria before the route matches. This prevents invalid data from reaching your handler:

Constraint Syntax Example Matches
int {id:int} /user/42 Integers only
guid {id:guid} /order/{guid} Valid GUIDs
min {age:min(18)} /adult/21 โ‰ฅ 18
max {count:max(100)} /items/50 โ‰ค 100
length {code:length(5)} /zip/12345 Exactly 5 chars
regex {ssn:regex(^\d{{3}}-\d{{2}}-\d{{4}}$)} /ssn/123-45-6789 Pattern match
alpha {name:alpha} /user/john Letters only

Optional and Default Parameters โš™๏ธ

You can make route parameters optional or provide defaults:

Optional parameter (using ?):

app.MapGet("/search/{term?}", (string? term) => { /* term might be null */ });

Default value (using =):

app.MapGet("/page/{pageNumber:int=1}", (int pageNumber) => { /* defaults to 1 */ });

Route Patterns and Catch-All ๐ŸŽฃ

ASP.NET supports several advanced routing patterns:

Catch-all parameter (captures everything):

app.MapGet("/files/{*filepath}", (string filepath) => { /* filepath contains full path */ });
// Matches: /files/documents/2024/report.pdf
// filepath = "documents/2024/report.pdf"

Multiple parameters:

app.MapGet("/blog/{year:int}/{month:int}/{slug}", 
    (int year, int month, string slug) => { /* handle */ });
// Matches: /blog/2024/03/aspnet-routing

Query String Parameters ๐Ÿ”

Query string parameters (after ? in URL) are automatically bound:

app.MapGet("/search", (string? query, int page = 1, int pageSize = 20) => 
{
    // URL: /search?query=laptop&page=2&pageSize=50
    // query = "laptop", page = 2, pageSize = 50
});

๐Ÿ’ก Tip: Make query parameters nullable or provide defaultsโ€”they might not be present in the URL!

Route Groups ๐Ÿ“ฆ

.NET 10 enhances route groups, allowing you to apply common prefixes and configuration to multiple endpoints:

var apiGroup = app.MapGroup("/api");
apiGroup.MapGet("/products", () => { /* GET /api/products */ });
apiGroup.MapPost("/products", () => { /* POST /api/products */ });

Controller-Based Routing ๐ŸŽฎ

For larger applications, controller-based routing provides better organization:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult GetProduct(int id)
    {
        // Handles: GET /api/products/123
    }

    [HttpPost]
    public IActionResult CreateProduct([FromBody] Product product)
    {
        // Handles: POST /api/products
    }
}

The [controller] token in the route template is replaced with the controller name (minus "Controller"), so ProductsController becomes products.

Route Precedence ๐Ÿ†

When multiple routes could match a request, ASP.NET uses these rules:

ROUTE PRECEDENCE (highest to lowest)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ 1. Exact literal match             โ”‚
โ”‚    /products/special               โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 2. Constrained parameters          โ”‚
โ”‚    /products/{id:int}              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 3. Unconstrained parameters        โ”‚
โ”‚    /products/{name}                โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 4. Optional parameters             โ”‚
โ”‚    /products/{category?}           โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 5. Catch-all                       โ”‚
โ”‚    /products/{*rest}               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โš ๏ธ Common Mistake: Defining routes in the wrong order can cause unexpected behavior. More specific routes should come before more general ones.

Endpoint Metadata and Filters ๐Ÿท๏ธ

.NET 10 allows you to add metadata and filters to endpoints:

app.MapGet("/admin/users", () => { /* handler */ })
   .RequireAuthorization()
   .WithName("GetAllUsers")
   .WithTags("admin", "users")
   .Produces<List<User>>(StatusCodes.Status200OK);

This enables:

  • Authorization requirements
  • OpenAPI/Swagger documentation
  • Endpoint naming (for URL generation)
  • Response type documentation

Route Parameter Transformation ๐Ÿ”„

You can transform route parameters automatically:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        return value?.ToString()?.ToLowerInvariant();
    }
}

This ensures URLs are consistently formatted (e.g., /api/products instead of /api/Products).

Testing Routes ๐Ÿงช

๐Ÿ”ง Try this: Test if your routes are registered correctly:

app.MapGet("/debug/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
{
    var endpoints = endpointSources.SelectMany(es => es.Endpoints);
    return endpoints.Select(e => new 
    {
        Name = e.DisplayName,
        Pattern = (e as RouteEndpoint)?.RoutePattern.RawText
    });
});

Visit /debug/routes to see all registered routes in your application!

Examples with Detailed Explanations ๐Ÿ’ก

Example 1: Building a Simple Product API

Let's create a complete product API with multiple endpoints:

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

// In-memory data store for demo
var products = new List<Product>
{
    new Product { Id = 1, Name = "Laptop", Price = 999.99m },
    new Product { Id = 2, Name = "Mouse", Price = 29.99m },
    new Product { Id = 3, Name = "Keyboard", Price = 79.99m }
};

// GET all products
app.MapGet("/api/products", () => Results.Ok(products));

// GET single product by ID with constraint
app.MapGet("/api/products/{id:int}", (int id) =>
{
    var product = products.FirstOrDefault(p => p.Id == id);
    return product is not null 
        ? Results.Ok(product) 
        : Results.NotFound($"Product {id} not found");
});

// GET products with pagination
app.MapGet("/api/products/search", (int page = 1, int pageSize = 10) =>
{
    var pagedProducts = products
        .Skip((page - 1) * pageSize)
        .Take(pageSize);
    return Results.Ok(new { Page = page, PageSize = pageSize, Data = pagedProducts });
});

// POST create new product
app.MapPost("/api/products", (Product product) =>
{
    product.Id = products.Max(p => p.Id) + 1;
    products.Add(product);
    return Results.Created($"/api/products/{product.Id}", product);
});

// PUT update product
app.MapPut("/api/products/{id:int}", (int id, Product updatedProduct) =>
{
    var product = products.FirstOrDefault(p => p.Id == id);
    if (product is null) return Results.NotFound();
    
    product.Name = updatedProduct.Name;
    product.Price = updatedProduct.Price;
    return Results.Ok(product);
});

// DELETE product
app.MapDelete("/api/products/{id:int}", (int id) =>
{
    var product = products.FirstOrDefault(p => p.Id == id);
    if (product is null) return Results.NotFound();
    
    products.Remove(product);
    return Results.NoContent();
});

app.Run();

record Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

Explanation:

  • We use MapGet for retrieval, MapPost for creation, MapPut for updates, and MapDelete for removal
  • The {id:int} constraint ensures only numeric IDs match that route
  • Query parameters (page, pageSize) have defaults, making them optional
  • We return appropriate HTTP status codes using Results helpers
  • The Created result includes the new resource's URI in the Location header

Example 2: Advanced Route Constraints and Patterns

Let's explore sophisticated routing scenarios:

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

// Route with multiple constraints
app.MapGet("/orders/{year:int:min(2020):max(2030)}/{month:int:range(1,12)}/{day:int:range(1,31)}",
    (int year, int month, int day) =>
    {
        var date = new DateTime(year, month, day);
        return $"Orders for {date:yyyy-MM-dd}";
    });
// Matches: /orders/2024/03/15
// Rejects: /orders/2019/03/15 (year too old)
// Rejects: /orders/2024/13/15 (invalid month)

// Route with regex constraint for product codes
app.MapGet("/products/{code:regex(^[A-Z]{{3}}-\\d{{4}}$)}", (string code) =>
{
    return $"Product code: {code}";
});
// Matches: /products/ABC-1234
// Rejects: /products/AB-123 (wrong format)

// Catch-all route for file paths
app.MapGet("/files/{*filepath}", (string filepath) =>
{
    // filepath might be: "documents/2024/report.pdf"
    return $"Requested file: {filepath}";
});
// Matches: /files/documents/2024/report.pdf

// Optional parameter with default
app.MapGet("/catalog/{category?}", (string category = "all") =>
{
    return $"Showing category: {category}";
});
// Matches: /catalog โ†’ category = "all"
// Matches: /catalog/electronics โ†’ category = "electronics"

// Complex route with multiple optional segments
app.MapGet("/blog/{year:int?}/{month:int?}/{slug?}", 
    (int? year, int? month, string? slug) =>
    {
        if (slug is not null)
            return $"Post: {year}/{month}/{slug}";
        if (month.HasValue)
            return $"Posts from {year}/{month}";
        if (year.HasValue)
            return $"Posts from {year}";
        return "All posts";
    });
// Matches: /blog โ†’ all posts
// Matches: /blog/2024 โ†’ posts from 2024
// Matches: /blog/2024/03 โ†’ posts from March 2024
// Matches: /blog/2024/03/routing-guide โ†’ specific post

app.Run();

Explanation:

  • Multiple constraints can be chained with colons: {year:int:min(2020):max(2030)}
  • Regex constraints require escaping backslashes: \\d in C# string becomes \d in regex
  • Catch-all parameters (*) capture all remaining path segments
  • Optional parameters allow routes to match multiple URL patterns
  • The handler logic adapts based on which parameters are present

Example 3: Using Route Groups for API Versioning

Route groups help organize related endpoints:

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

// Version 1 API
var v1 = app.MapGroup("/api/v1")
    .WithTags("v1")
    .WithOpenApi();

v1.MapGet("/users", () => new[] 
{ 
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" }
});

v1.MapGet("/users/{id:int}", (int id) => 
    new { Id = id, Name = "User" + id });

// Version 2 API with enhanced response
var v2 = app.MapGroup("/api/v2")
    .WithTags("v2")
    .WithOpenApi();

v2.MapGet("/users", () => new[] 
{ 
    new { Id = 1, Name = "Alice", Email = "alice@example.com", Active = true },
    new { Id = 2, Name = "Bob", Email = "bob@example.com", Active = true }
});

v2.MapGet("/users/{id:int}", (int id) => 
    new { Id = id, Name = "User" + id, Email = $"user{id}@example.com", Active = true });

// Admin endpoints with authorization
var admin = app.MapGroup("/api/admin")
    .RequireAuthorization("AdminPolicy")
    .WithTags("admin");

admin.MapGet("/stats", () => new 
{ 
    TotalUsers = 150, 
    ActiveSessions = 42,
    ServerUptime = TimeSpan.FromHours(720)
});

admin.MapDelete("/users/{id:int}", (int id) =>
{
    // Delete user logic
    return Results.NoContent();
});

app.Run();

Explanation:

  • MapGroup creates a route prefix applied to all endpoints in that group
  • Each group can have its own metadata (tags, authorization, etc.)
  • This enables API versioning: /api/v1/users vs /api/v2/users
  • Groups make it easy to apply authorization to entire sections: all /api/admin/* routes require authorization
  • WithTags helps organize endpoints in OpenAPI/Swagger documentation

Example 4: Complex Controller-Based Routing

For larger applications, controllers provide better structure:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    // GET api/orders
    [HttpGet]
    public IActionResult GetOrders([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
    {
        // Return paginated orders
        return Ok(new { Page = page, PageSize = pageSize, Orders = new List<object>() });
    }

    // GET api/orders/5
    [HttpGet("{id:int}")]
    public IActionResult GetOrder(int id)
    {
        return Ok(new { Id = id, Total = 99.99 });
    }

    // GET api/orders/by-customer/john-doe
    [HttpGet("by-customer/{customerSlug}")]
    public IActionResult GetOrdersByCustomer(string customerSlug)
    {
        return Ok(new { Customer = customerSlug, Orders = new List<object>() });
    }

    // GET api/orders/2024/03
    [HttpGet("{year:int:min(2020)}/{month:int:range(1,12)}")]
    public IActionResult GetOrdersByDate(int year, int month)
    {
        return Ok(new { Year = year, Month = month, Orders = new List<object>() });
    }

    // POST api/orders
    [HttpPost]
    public IActionResult CreateOrder([FromBody] CreateOrderDto dto)
    {
        var newOrder = new { Id = 123, Total = dto.Total, Status = "pending" };
        return CreatedAtAction(nameof(GetOrder), new { id = 123 }, newOrder);
    }

    // PUT api/orders/5/status
    [HttpPut("{id:int}/status")]
    public IActionResult UpdateOrderStatus(int id, [FromBody] UpdateStatusDto dto)
    {
        return Ok(new { Id = id, Status = dto.NewStatus });
    }

    // DELETE api/orders/5
    [HttpDelete("{id:int}")]
    public IActionResult DeleteOrder(int id)
    {
        return NoContent();
    }
}

record CreateOrderDto(decimal Total, List<int> ProductIds);
record UpdateStatusDto(string NewStatus);

Explanation:

  • [Route("api/[controller]")] creates the base route /api/orders
  • Each action method uses HTTP verb attributes: [HttpGet], [HttpPost], etc.
  • Route templates in attributes are relative to the controller route
  • [FromQuery] explicitly binds from query string, [FromBody] from request body
  • CreatedAtAction generates a Location header pointing to the new resource
  • Constraints work identically in controller routes: {id:int}, {year:int:min(2020)}

Common Mistakes โš ๏ธ

1. Forgetting Route Constraints

// โŒ WRONG: Accepts any string, even non-numeric
app.MapGet("/products/{id}", (string id) => 
{
    if (!int.TryParse(id, out var productId))
        return Results.BadRequest("Invalid ID");
    // ...
});

// โœ… RIGHT: Only matches numeric IDs
app.MapGet("/products/{id:int}", (int id) => 
{
    // id is guaranteed to be an integer
});

2. Route Order Issues

// โŒ WRONG: Catch-all defined first
app.MapGet("/products/{name}", (string name) => $"Product: {name}");
app.MapGet("/products/featured", () => "Featured products");
// "/products/featured" will match the first route with name="featured"!

// โœ… RIGHT: Most specific routes first
app.MapGet("/products/featured", () => "Featured products");
app.MapGet("/products/{name}", (string name) => $"Product: {name}");

3. Missing Nullable Annotations for Optional Parameters

// โŒ WRONG: Runtime null warning if query param missing
app.MapGet("/search", (string query) => { /* query might be null! */ });

// โœ… RIGHT: Explicitly nullable or with default
app.MapGet("/search", (string? query) => { /* proper null handling */ });
app.MapGet("/search", (string query = "all") => { /* has default */ });

4. Not Using Results Helpers

// โŒ WRONG: Returns wrong status code
app.MapGet("/users/{id:int}", (int id) =>
{
    return null; // Returns 204 No Content instead of 404!
});

// โœ… RIGHT: Use Results helpers
app.MapGet("/users/{id:int}", (int id) =>
{
    return Results.NotFound($"User {id} not found");
});

5. Inconsistent Route Naming

// โŒ WRONG: Inconsistent naming
app.MapGet("/Products", () => { /* ... */ });
app.MapGet("/api/orders", () => { /* ... */ });

// โœ… RIGHT: Consistent lowercase, clear hierarchy
app.MapGet("/api/products", () => { /* ... */ });
app.MapGet("/api/orders", () => { /* ... */ });

6. Not Validating Input

// โŒ WRONG: No validation
app.MapPost("/api/products", (Product product) =>
{
    // What if product.Name is null or empty?
    return Results.Created($"/api/products/{product.Id}", product);
});

// โœ… RIGHT: Validate input
app.MapPost("/api/products", (Product product) =>
{
    if (string.IsNullOrWhiteSpace(product.Name))
        return Results.BadRequest("Product name is required");
    if (product.Price <= 0)
        return Results.BadRequest("Price must be positive");
    
    return Results.Created($"/api/products/{product.Id}", product);
});

7. Overlapping Routes

// โŒ WRONG: Ambiguous routes
app.MapGet("/api/{resource}/{id:int}", (string resource, int id) => { });
app.MapGet("/api/products/{id:int}", (int id) => { });
// Second route will never match!

// โœ… RIGHT: Specific routes or clear differentiation
app.MapGet("/api/products/{id:int}", (int id) => { });
app.MapGet("/api/orders/{id:int}", (int id) => { });

Key Takeaways ๐ŸŽฏ

๐Ÿ”น Routing maps HTTP requests to endpoints based on URL patterns and HTTP methods

๐Ÿ”น Minimal APIs (MapGet, MapPost, etc.) provide a concise way to define endpoints directly in Program.cs

๐Ÿ”น Route parameters {id} capture values from URLs and bind them to handler parameters

๐Ÿ”น Route constraints {id:int} ensure parameters meet specific criteria before the route matches

๐Ÿ”น Route order matters: more specific routes should be defined before general ones

๐Ÿ”น Results helpers (Results.Ok, Results.NotFound, etc.) return appropriate HTTP status codes

๐Ÿ”น Route groups organize related endpoints and apply common configuration

๐Ÿ”น Controller-based routing with attributes provides better structure for large applications

๐Ÿ”น Query parameters are automatically bound from the URL query string

๐Ÿ”น Always validate input and use nullable types for optional parameters

Further Study ๐Ÿ“š

  1. Official Microsoft Documentation - Routing in ASP.NET Core
    https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing

  2. Minimal APIs Overview
    https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis

  3. Route Constraint Reference
    https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#route-constraints

๐Ÿ“‹ Quick Reference Card: ASP.NET Routing

ConceptSyntaxExample
Basic GET endpointapp.MapGet(route, handler)app.MapGet("/api/users", () => users)
Route parameter{paramName}"/users/{id}"
Integer constraint{param:int}"/products/{id:int}"
Range constraint{param:range(min,max)}"{age:range(18,100)}"
Optional parameter{param?}"/search/{term?}"
Default value{param=value}"{page:int=1}"
Catch-all{*param}"/files/{*path}"
POST endpointapp.MapPost(route, handler)app.MapPost("/api/users", CreateUser)
Route groupapp.MapGroup(prefix)app.MapGroup("/api/v1")
Success responseResults.Ok(data)Results.Ok(users)
Created responseResults.Created(uri, data)Results.Created($"/api/users/{id}", user)
Not found responseResults.NotFound()Results.NotFound("User not found")

Practice Questions

Test your understanding with these questions:

Q1: What method creates a GET endpoint in minimal APIs? ```csharp app.{{1}}("/api/users", () => users); ```
A: MapGet
Q2: Complete the route constraint for integer IDs: ```csharp app.MapGet("/products/{{1}}", (int id) => GetProduct(id)); ```
A: ["id:int"]
Q3: What HTTP status code does Results.NotFound() return? A. 200 B. 201 C. 400 D. 404 E. 500
A: D
Q4: Complete the POST endpoint: ```csharp app.{{1}}("/api/products", (Product p) => CreateProduct(p)); ```
A: ["MapPost"]
Q5: What does this route pattern match? ```csharp app.MapGet("/files/{*filepath}", (string filepath) => { }); ``` A. Only /files/ B. /files/ with one segment C. /files/ with any number of segments D. Only files with extensions E. Nothing - invalid syntax
A: C