Back to "Comprendre le pipeline de base de demande et de réponse ASP.NET - Partie 4 : Routage et points d'arrivée"

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

AI-Article ASP.NET ASP.NET Lifecycle

Comprendre le pipeline de base de demande et de réponse ASP.NET - Partie 4 : Routage et points d'arrivée

Sunday, 09 November 2025

Présentation

Nous avons parcouru la couche d'hébergement, exploré Kestrel et maîtrisé le middleware. Maintenant, nous arrivons à une question cruciale: une fois qu'une requête le fait à travers votre pipeline de middleware, comment ASP.NET Core sait quel code à exécuter? La réponse est routage et les critères d' évaluation.

Le routage est le mécanisme qui correspond aux requêtes HTTP entrantes au code exécutable. Les points d'extrémité sont les destinations – le code réel qui s'exécute. Ensemble, ils forment un système puissant et flexible qui prend en charge tout, des simples gestionnaires de route aux applications MVC complexes.

Dans cette partie, nous allons explorer comment le routage fonctionne, comment les paramètres sont enregistrés et exécutés, et comment vous pouvez utiliser ce système pour construire des applications sophistiquées.

REMARQUE: Ceci fait partie de mes expériences avec l'IA / un moyen de dépenser 1000 $ Code Calude crédits Web. J'ai alimenté ceci un BUNCH de papiers, ma compréhension, questions que j'ai dû générer cet article. C'est amusant et comble un vide que je n'ai pas vu comblé nulle part ailleurs.

L'évolution du routage

Le routage de ASP.NET Core a considérablement évolué :

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

Routage traditionnel (pré-3.0):

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

Routage du point d'extrémité (3.0+):

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

API Minimales Modernes (6.0+):

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

Comment fonctionne le routage : le processus à deux phases

Le routage d'extrémité fonctionne en deux phases :

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

Phase 1: Correspondance des routes (UseRouting())

L'intergiciel de routage examine la requête et essaie de la faire correspondre à un paramètre enregistré :

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();

Diagramme de flux:

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

Phase 2: Exécution finale

L'intergiciel du paramètre exécute le paramètre correspondant:

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();

Produit :

Before endpoint execution
Executing endpoint
After endpoint execution

Modèles d'itinéraire

Les modèles de route définissent le modèle d'URL à correspondre :

Modèles de base

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();

Exemples:

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"

Contraintes routières

Les contraintes permettent de s'assurer que les paramètres correspondent à des modèles spécifiques :

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();

Contraintes intégrées:

L'exemple des correspondances |------------|---------|---------| | int | {id:int} | 123, -456 | | bool | {active:bool} C'est vrai, faux | 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)} A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C., A.C. | maxlength(n) | {name:maxlength(5)} Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé, Abrégé | length(n) | {code:length(5)} 'bbcde' | 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} Abrégé, XYZ | regex(pattern) | {code:regex(^[A-Z]{3}$)} ABC, XYZ | required | {id:required} Toute valeur non vide

Contraintes d'essai:

# ✅ 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

Contraintes d'itinéraire personnalisées

Créez vos propres contraintes :

// 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();

Essai:

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

Priorités et ordre des routes

Lorsque plusieurs routes correspondent, ASP.NET Core utilise un système prioritaire:

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]


Exemple :

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();

Comportement correspondant & #160;:

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)

Vous pouvez également définir l'ordre explicite:

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

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

Reliure des paramètres

ASP.NET Core peut lier les paramètres de route de plusieurs sources :

Valeurs de la route

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

Chaîne de requête

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

En-têtes

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

Corps

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

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

Services (Injection d ' urgence)

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

HttpContext

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()
    };
});

Exemple de liaison complexe

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"] }

Métadonnées d'extrémité

Les points d'extrémité peuvent avoir des métadonnées qui les décrivent :

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();

Produit /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
  }
]

Groupes d'itinéraires

Terminaux liés au groupe avec configuration partagée:

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();

Routes créées :

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)]

Visualisation :

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]

Scénarios avancés d'acheminement

Négociation du contenu par itinéraire

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" });
});

Routes localisées

// 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})";
});

APIs en version

// 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" })
    };
});

Sous-domaine Routage

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"
    };
});

Déboguage de la route

Voir tous les itinéraires enregistrés:

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();

Produit /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", ...]
  }
]

Considérations de performance

Cachement de la route

// 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

Génération de liens

Générer des URLs à partir des routes & #160;:

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 };
});

Prises en charge des clés

  • Le routage est un processus en deux phases : l'appariement (UseRouting) et l'exécution (endpoints)
  • Les modèles d'itinéraire prennent en charge les paramètres, les contraintes, les valeurs optionnelles et les captures-touts
  • Les contraintes intégrées valident les types de paramètres et les modèles
  • Les contraintes personnalisées fournissent une validation spécifique à l'application
  • Priorité de la route: littérales > Paramètres limités > Non restreints > Facultatifs > Captures-tout
  • Les paramètres peuvent se lier à partir d'itinéraires, de chaînes de requêtes, d'en-têtes, de corps et de services
  • Les métadonnées d'extrémité permettent des fonctionnalités comme l'autorisation et la documentation
  • Les groupes d'itinéraires simplifient la configuration pour les paramètres connexes
  • Génération de liens crée des URLs à partir des noms de route
  • Le système de routage est hautement optimisé pour les performances

Le routage et les paramètres forment le pont entre les requêtes HTTP et votre code d'application. La maîtrise de ce système vous donne un contrôle précis sur la façon dont les URLs map to functionality.

logo

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