Informations de référence rapides sur les API minimales

Ce document :

Les API minimales sont les suivantes :

WebApplication

Le code suivant est généré par un modèle ASP.NET Core :

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

app.MapGet("/", () => "Hello World!");

app.Run();

Le code précédent peut être créé via dotnet new web sur la ligne de commande ou en sélectionnant le modèle web Vide dans Visual Studio.

Le code suivant crée un WebApplication (app) sans créer explicitement de WebApplicationBuilder :

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create initialise une nouvelle instance de la classe WebApplication avec les valeurs par défaut préconfigurées.

WebApplication ajoute automatiquement l’intergiciel suivant à Minimal API applications en fonction de certaines conditions :

  • UseDeveloperExceptionPage est ajouté en premier lorsque HostingEnvironment est "Development".
  • UseRouting est ajouté ensuite si le code utilisateur n’a pas déjà appelé UseRouting et s’il existe des points de terminaison configurés, par exemple app.MapGet.
  • UseEndpoints est ajouté à la fin du pipeline d’intergiciel si des points de terminaison sont configurés.
  • UseAuthentication est ajouté immédiatement après UseRouting, si le code utilisateur n’a pas déjà appelé UseAuthentication et si IAuthenticationSchemeProvider peut être détecté dans le fournisseur de services. IAuthenticationSchemeProvider est ajouté par défaut lors de l’utilisation de AddAuthentication, et les services sont détectés à l’aide de IServiceProviderIsService.
  • UseAuthorization est ajouté après, si le code utilisateur n’a pas déjà appelé UseAuthorization et si IAuthorizationHandlerProvider peut être détecté dans le fournisseur de services. IAuthorizationHandlerProvider est ajouté par défaut lors de l’utilisation de AddAuthorization, et les services sont détectés à l’aide de IServiceProviderIsService.
  • Les intergiciels et les points de terminaison configurés par l’utilisateur sont ajoutés entre UseRouting et UseEndpoints.

Le code suivant est effectivement ce qu’un intergiciel automatique ajouté à l’application produit :

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

Dans certains cas, la configuration de l’intergiciel par défaut n’est pas correcte pour l’application et exige une modification. Par exemple, UseCors doit être appelé avant UseAuthentication et UseAuthorization. L’application doit appeler UseAuthentication et UseAuthorization, si UseCors est appelé :

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

Si l’intergiciel doit être exécuté avant l’exécution de la correspondance d’itinéraire, appeler UseRouting et placer l’intergiciel avant l’appel à UseRouting. UseEndpoints n’est pas obligatoire dans ce cas, car il est automatiquement ajouté comme décrit précédemment :

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

Lors de l’ajout d’un intergiciel de terminal :

  • L’intergiciel doit être ajouté après UseEndpoints.
  • L’application doit appeler UseRouting et UseEndpoints pour que l’intergiciel de terminal puisse être placé à l’emplacement approprié.
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

Un intergiciel de terminal est un intergiciel qui s’exécute si aucun point de terminaison ne gère la requête.

Utilisation des ports

Lorsqu’une application web est créée avec Visual Studio ou dotnet new, un fichier Properties/launchSettings.json est créé et spécifie les ports auxquels l’application répond. Dans les exemples de paramètres de port qui suivent, l’exécution de l’application à partir de Visual Studio renvoie une boîte de dialogue d’erreur Unable to connect to web server 'AppName'. Visual Studio retourne une erreur, car il attend le port spécifié dans Properties/launchSettings.json, mais l’application utilise le port spécifié par app.Run("http://localhost:3000"). Exécutez les exemples de modification de port suivants à partir de la ligne de commande.

Les sections suivantes définissent le port auquel l’application répond.

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");

Dans le code précédent, l’application répond au port 3000.

Plusieurs ports

Dans le code suivant, l’application répond aux ports 3000 et 4000.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

Définir le port à partir de la ligne de commande

La commande suivante permet à l’application de répondre au port 7777 :

dotnet run --urls="https://localhost:7777"

Si le point de terminaison Kestrel est également configuré dans le fichier appsettings.json, l’URL spécifiée par le fichier appsettings.json est utilisée. Pour plus d’informations, consultez Configuration du point de terminaison Kestrel

Lire le port à partir de l’environnement

Le code suivant lit le port à partir de l’environnement :

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

La méthode recommandée pour définir le port à partir de l’environnement consiste à utiliser la variable d’environnement ASPNETCORE_URLS, comme indiqué dans la section suivante.

Définir les ports via la variable d’environnement ASPNETCORE_URLS

La variable d’environnement ASPNETCORE_URLS est disponible pour définir le port :

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS prend en charge plusieurs URL :

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Écouter sur toutes les interfaces

Les exemples suivants illustrent l’écoute sur toutes les interfaces

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Écoutez toutes les interfaces à l’aide d’ASPNETCORE_URLS

Les exemples précédents peuvent utiliser ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Écoutez toutes les interfaces à l’aide d’ASPNETCORE_HTTPS_PORTS

Les exemples précédents peuvent utiliser ASPNETCORE_HTTPS_PORTS et ASPNETCORE_HTTP_PORTS.

ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000

Pour plus d’informations, consultez Configurer des points de terminaison pour le serveur web ASP.NET Core Kestrel

Spécifier HTTPS avec un certificat de développement

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Pour plus d’informations sur le certificat de développement, consultez Approuver le certificat de développement HTTPS ASP.NET Core sur Windows et macOS.

Spécifier HTTPS à l’aide d’un certificat personnalisé

Les sections suivantes montrent comment spécifier le certificat personnalisé à l’aide du fichier appsettings.json et via la configuration.

Spécifier le certificat personnalisé avec appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

Spécifier le certificat personnalisé via la configuration

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Utiliser les API de certificat

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Lire l’environnement

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

Pour plus d’informations sur l’utilisation de l’environnement, consultez Utiliser plusieurs environnements dans ASP.NET Core

Configuration

Le code suivant est lu à partir du système de configuration :

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

Pour plus d’informations, consultez Configuration dans ASP.NET Core

Journalisation

Le code suivant écrit un message dans le journal au démarrage de l’application :

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

Pour plus d’informations, consultez Journalisation dans .NET Core et ASP.NET Core

Accéder au conteneur d’injection de dépendances (DI)

Le code suivant montre comment obtenir des services à partir du conteneur d’authentification unique au démarrage de l’application :


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

Le code suivant montre comment accéder aux clés d’accès à partir du conteneur d’injection de dépendances (DI) en utilisant l’attribut [FromKeyedServices] :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

Pour obtenir plus d’informations sur le DI, consultez Injection de dépendances dans ASP.NET Core.

WebApplicationBuilder

Cette section contient un exemple de code utilisant WebApplicationBuilder.

Modifier la racine du contenu, le nom de l’application et l’environnement

Le code suivant définit la racine du contenu, le nom de l’application et l’environnement :

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder initialise une nouvelle instance de la classe WebApplicationBuilder avec les valeurs par défaut préconfigurées.

Pour plus d’informations, consultez Vue d’ensemble des principes de base d’ASP.NET Core

Modifier la racine du contenu, le nom de l’application et l’environnement avec des variables d’environnement ou la ligne de commande

Le tableau suivant montre la variable d’environnement et l’argument de ligne de commande utilisés pour modifier la racine du contenu, le nom de l’application et l’environnement :

fonctionnalité Variable d’environnement Argument de ligne de commande
Nom de l'application ASPNETCORE_APPLICATIONNAME --applicationName
Nom de l’environnement ASPNETCORE_ENVIRONMENT --environment
Racine de contenu ASPNETCORE_CONTENTROOT --contentRoot

Ajouter des fournisseurs de configuration

L’exemple suivant ajoute le fournisseur de configuration INI :

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

Pour plus d’informations, consultez Fournisseurs de configuration de fichiers dans Configuration dans ASP.NET Core.

Configuration de lecture

Par défaut, WebApplicationBuilder lit la configuration à partir de plusieurs sources, notamment :

  • appSettings.json et appSettings.{environment}.json
  • Variables d’environnement
  • Ligne de commande

Pour obtenir la liste complète des sources de configuration lues, consultez Configuration par défaut dans Configuration dans ASP.NET Core.

Le code suivant lit HelloKey à partir de la configuration et affiche la valeur au niveau du point de terminaison /. Si la valeur de configuration est null, « Hello » est affecté à message :

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Lire l’environnement

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Ajouter des fournisseurs de journalisation

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

Ajouter des services

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Personnaliser IHostBuilder

Les méthodes d’extension existantes sur IHostBuilder sont accessibles à l’aide de la propriété Host :

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Personnaliser IWebHostBuilder

Les méthodes d’extension sur IWebHostBuilder sont accessibles à l’aide de la propriété WebApplicationBuilder.WebHost.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Modifier la racine web

Par défaut, la racine web est relative à la racine de contenu dans le dossier wwwroot. La racine web est l’endroit où l’intergiciel de fichiers statiques recherche les fichiers statiques. La racine web peut être modifiée avec WebHostOptions, la ligne de commande ou avec la méthode UseWebRoot :

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Conteneur d’injection de dépendances (DI) personnalisé

L’exemple suivant utilise Autofac :

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Ajouter un intergiciel

Tout intergiciel ASP.NET Core existant peut être configuré sur WebApplication :

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

Pour plus d’informations, consultez Intergiciel (middleware) ASP.NET Core

Page d’exceptions du développeur

WebApplication.CreateBuilder initialise une nouvelle instance de la classe WebApplicationBuilder avec les valeurs par défaut préconfigurées. La page d’exception du développeur est activée dans les valeurs par défaut préconfigurées. Lorsque le code suivant est exécuté dans l’environnement de développement, la navigation vers / présente une page conviviale qui affiche l’exception.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

Intergiciel (middleware) ASP.NET Core

Le tableau suivant répertorie certains des intergiciels fréquemment utilisés avec les API minimales.

Intergiciel (middleware) Description API
Authentification Prend en charge l’authentification. UseAuthentication
Autorisation Fournit la prise en charge des autorisations. UseAuthorization
CORS Configure le partage des ressources cross-origin (CORS). UseCors
Gestionnaire d’exceptions Gère globalement les exceptions levées par le pipeline d’intergiciels. UseExceptionHandler
En-têtes transférés Transfère les en-têtes en proxy vers la requête actuelle. UseForwardedHeaders
Redirection HTTPS Redirige toutes les requêtes HTTP vers HTTPS. UseHttpsRedirection
HSTS (HTTP Strict Transport Security) Middleware d’amélioration de la sécurité qui ajoute un en-tête de réponse spécial. UseHsts
Journalisation des requêtes Prend en charge la journalisation des requêtes et des réponses HTTP. UseHttpLogging
Délais d'attente des tests Permet de configurer les délais d'attente des requêtes, par défaut global et par point d'extrémité. UseRequestTimeouts
Journalisation des requêtes W3C Prend en charge la journalisation des requêtes et des réponses HTTP au format W3C. UseW3CLogging
Mise en cache des réponses Prend en charge la mise en cache des réponses. UseResponseCaching
Compression des réponses Prend en charge la compression des réponses. UseResponseCompression
Session Prend en charge la gestion des sessions utilisateur. UseSession
Fichiers statiques Prend en charge le traitement des fichiers statiques et l’exploration des répertoires. UseStaticFiles, UseFileServer
WebSockets Autorise le protocole WebSockets. UseWebSockets

Les sections suivantes couvrent la gestion des requêtes : routage, liaison de paramètres et réponses.

Routage

Un WebApplication configuré prend en charge Map{Verb} et MapMethods, où {Verb} est une méthode HTTP en casse mixte, comme Get, Post, Put ou Delete :

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

Les arguments Delegate passés à ces méthodes sont appelés « gestionnaires de routes ».

Gestionnaires de routes

Les gestionnaires de routes sont des méthodes qui s’exécutent lorsque la route correspond. Les gestionnaires de routes peuvent être une expression lambda, une fonction locale, une méthode d’instance ou une méthode statique. Les gestionnaires de routes peuvent être synchrones ou asynchrones.

Expression lambda

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Fonction locale

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Méthode d'instance

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

Méthode statique

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

Point de terminaison défini à l’extérieur de Program.cs

Les API minimales n’ont pas besoin d’être situées à l’emplacement Program.cs.

Program.cs

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}

Consultez également Groupes d’itinéraires plus loin dans cet article.

Les points de terminaison peuvent recevoir des noms afin de générer des URL vers le point de terminaison. L’utilisation d’un point de terminaison nommé évite d’avoir à coder des chemins d’accès en dur dans une application :

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

Le code précédent affiche The link to the hello endpoint is /hello à partir du point de terminaison /.

REMARQUE : Les noms des points de terminaison respectent la casse.

Les noms des points de terminaison :

  • Il doit être globalement unique.
  • Sont utilisés comme ID d’opération OpenAPI lorsque la prise en charge d’OpenAPI est activée. Pour plus d’informations, consultez OpenAPI.

Paramètres de routage

Les paramètres de routage peuvent être capturés dans le cadre de la définition du modèle de route :

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

Le code précédent retourne The user id is 3 and book id is 7 à partir de l’URI /users/3/books/7.

Le gestionnaire de routes peut déclarer les paramètres à capturer. Lorsqu’une requête est effectuée sur une route avec des paramètres déclarés pour la capture, les paramètres sont analysés et transmis au gestionnaire. Cela permet de capturer facilement les valeurs avec un type sécurisé. Dans le code précédent, userId et bookId sont tous deux de type int.

Dans le code précédent, si l’une ou l’autre valeur de route ne peut pas être convertie en int, une exception est levée. La requête GET /users/hello/books/3 lève l’exception suivante :

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Caractères génériques et routes catch all

Les éléments suivants interceptent tous les retours de route Routing to hello à partir du point de terminaison « /posts/hello » :

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Contraintes d'itinéraire

Les contraintes de routage limitent le comportement de correspondance d’une route.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

Le tableau suivant montre les modèles de route précédents et leur comportement :

Modèle de routage Exemple d’URI en correspondance
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

Pour plus d’informations, consultez Référence sur la contrainte d’itinéraire dans Routage dans ASP.NET Core.

Groupes de routes

La méthode d’extension MapGroup permet d’organiser des groupes de points de terminaison avec un préfixe commun. Cela réduit le code répétitif et permet de personnaliser des groupes entiers de points de terminaison avec un seul appel à des méthodes comme RequireAuthorization et WithMetadata, qui ajoutent des métadonnées de point de terminaison.

Par exemple, le code suivant crée deux groupes de points de terminaison similaires :

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

Dans ce scénario, vous pouvez utiliser une adresse relative pour l’en-tête Location dans le résultat 201 Created :

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

Le premier groupe de points de terminaison correspond uniquement aux requêtes précédées de /public/todos, accessibles sans authentification. Le second groupe de points de terminaison correspond uniquement aux requêtes préfixées par /private/todos, qui nécessitent une authentification.

La QueryPrivateTodosfabrique de filtres de point de terminaison est une fonction locale qui modifie les paramètres TodoDb du gestionnaire de routes pour autoriser l’accès aux données de tâche privées et les stocker.

Les groupes de routage prennent également en charge les groupes imbriqués et les modèles de préfixe complexes avec des contraintes et des paramètres de routage. Dans l’exemple suivant, un gestionnaire de routage mappé au groupe user peut capturer les paramètres de routage {org} et {group} définis dans les préfixes de groupe externe.

Le préfixe peut également être vide. Cela peut être utile pour ajouter des métadonnées ou des filtres de point de terminaison à un groupe de points de terminaison sans modifier le modèle de routage.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

L’ajout de filtres ou de métadonnées à un groupe se comporte de la même façon que si vous les ajoutiez individuellement à chaque point de terminaison avant d’ajouter des filtres ou des métadonnées supplémentaires qui ont pu être ajoutés à un groupe interne ou à un point de terminaison spécifique.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

Dans l’exemple ci-dessus, le filtre externe enregistre la requête entrante avant le filtre interne, même si elle a été ajoutée en deuxième. Étant donné que les filtres ont été appliqués à différents groupes, l’ordre dans lequel ils ont été ajoutés les uns par rapport aux autres n’a pas d’importance. Les filtres d’ordre ajoutés sont importants s’ils sont appliqués au même groupe ou au même point de terminaison spécifique.

Une requête sur /outer/inner/ journalisera les éléments suivants :

/outer group filter
/inner group filter
MapGet filter

Liaison de paramètres

La liaison de paramètres est le processus de conversion des données de requête en paramètres fortement typés qui sont exprimés par les gestionnaires de routes. Une source de liaison détermine à partir d’où les paramètres sont liés. Les sources de liaison peuvent être explicites ou déduites en fonction de la méthode HTTP et du type de paramètre.

Sources de liaison prises en charge :

  • Valeurs d’itinéraire
  • Chaîne de requête
  • Header
  • Corps (JSON)
  • Valeurs du formulaire
  • Services fournis par l’injection de dépendances
  • Personnalisé

Le gestionnaire de routage GET suivant utilise certaines de ces sources de liaison de paramètres :

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

Le tableau suivant montre la relation entre les paramètres utilisés dans l’exemple précédent et les sources de liaison associées.

Paramètre Source de liaison
id valeur de route
page chaîne de requête
customHeader en-tête
service Fourni par l’injection de dépendances

Les méthodes HTTP GET, HEAD, OPTIONS et DELETE ne sont pas implicitement liées à partir du corps. Pour lier à partir du corps (JSON) pour ces méthodes HTTP, liez explicitement avec [FromBody] ou lisez à partir de HttpRequest.

L’exemple de gestionnaire de routage POST suivant utilise une source de liaison de corps (JSON) pour le paramètre person :

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

Les paramètres des exemples précédents sont tous liés automatiquement à partir des données de requête. Pour illustrer la commodité de la liaison de paramètres, les gestionnaires de routage suivants montrent comment lire les données de requête directement à partir de la requête :

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

Liaison de paramètre explicite

Les attributs peuvent être utilisés pour déclarer explicitement à partir d’où les paramètres sont liés.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
Paramètre Source de liaison
id valeur de routage avec le nom id
page chaîne de requête avec le nom "p"
service Fourni par l’injection de dépendances
contentType en-tête avec le nom "Content-Type"

Liaison explicite à partir des valeurs de formulaire

L’attribut [FromForm] lie les valeurs de formulaire :

app.MapPost("/todos", async ([FromForm] string name,
    [FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = name,
        Visibility = visibility
    };

    if (attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await attachment.CopyToAsync(stream);
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.

Une alternative consiste à utiliser l’attribut [AsParameters] avec un type personnalisé qui a des propriétés annotées avec [FromForm]. Par exemple, le code suivant lie des valeurs de formulaire aux propriétés du struct d’enregistrement NewTodoRequest :

app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = request.Name,
        Visibility = request.Visibility
    };

    if (request.Attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await request.Attachment.CopyToAsync(stream);

        todo.Attachment = attachmentName;
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
    [FromForm] Visibility Visibility, IFormFile? Attachment);

Pour plus d'informations, voir la section AsParameters plus loin dans cet article.

L’exemple de code complet dans le référentiel AspNetCore.Docs.Samples.

Sécurisez la liaison à partir d’IFormFile et d’IFormFileCollection

La liaison de formulaire complexe est prise en charge à l’aide de IFormFile et IFormFileCollection à l’aide de [FromForm] :

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
    ([FromForm] FileUploadForm fileUploadForm, HttpContext context,
                                                IAntiforgery antiforgery) =>
{
    await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
              fileUploadForm.Name!, app.Environment.ContentRootPath);
    return TypedResults.Ok($"Your file with the description:" +
        $" {fileUploadForm.Description} has been uploaded successfully");
});

app.Run();

Les paramètres liés à la requête avec [FromForm] incluent un jeton anti-falsification. Le jeton anti-falsification est validé lors du traitement de la requête. Pour plus d’informations, consultez Anti-falsification avec des API minimales.

Pour plus d’informations, consultez Liaison de formulaire dans les API minimales.

L’exemple de code complet dans le référentiel AspNetCore.Docs.Samples.

Liaison de paramètre avec injection de dépendances

La liaison de paramètre pour les API minimales lie des paramètres via l’injection de dépendance quand le type est configuré en tant que service. Il n’est pas nécessaire d’appliquer explicitement l’attribut [FromServices] à un paramètre. Dans le code suivant, les deux actions retournent l’heure :

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

Paramètres optionnels

Les paramètres déclarés dans les gestionnaires d’itinéraire sont traités comme requis :

  • Si une requête correspond à l’itinéraire, le gestionnaire d’itinéraire s’exécute uniquement si tous les paramètres requis sont fournis dans la requête.
  • Le fait de ne pas fournir tous les paramètres requis entraîne une erreur.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 3 retournés
/products BadHttpRequestException : le paramètre obligatoire « int pageNumber » n’a pas été fourni à partir de la chaîne de requête.
/products/1 Erreur HTTP 404, aucune route correspondante

Pour rendre pageNumber facultatif, définissez le type comme facultatif ou fournissez une valeur par défaut :

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 3 retournés
/products 1 retourné
/products2 1 retourné

La valeur nullable et la valeur par défaut précédentes s’appliquent à toutes les sources :

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

app.MapPost("/products", (Product? product) => { });

app.Run();

Le code précédent appelle la méthode avec un produit null si aucun corps de requête n’est envoyé.

REMARQUE : Si des données non valides sont fournies et que le paramètre est nullable, le gestionnaire de routage n’est pas exécuté.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 3 retourné
/products 1 retourné
/products?pageNumber=two BadHttpRequestException : Échec de la liaison du paramètre "Nullable<int> pageNumber" à partir de « two ».
/products/two Erreur HTTP 404, aucune route correspondante

Pour plus d’informations, consultez la section Échecs de liaison.

Types spéciaux

Les types suivants sont liés sans attributs explicites :

  • HttpContext : contexte qui contient toutes les informations sur la requête ou la réponse HTTP actuelle :

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest et HttpResponse : requête HTTP et réponse HTTP :

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken : jeton d’annulation associé à la requête HTTP actuelle :

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal : utilisateur associé à la requête, lié à partir de HttpContext.User :

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

Lier le corps de la requête en tant que Stream ou PipeReader

Le corps de la requête peut être lié en tant que Stream ou PipeReader pour prendre en charge efficacement les scénarios où l’utilisateur doit traiter des données et :

  • Stockez les données dans le stockage d’objets blob ou placez les données en file d’attente dans un fournisseur de file d’attente.
  • Traitez les données stockées avec un processus Worker ou une fonction cloud.

Par exemple, les données peuvent être mises en file d’attente pour le Stockage File d’attente Azure ou stockées dans le Stockage Blob Azure.

Le code suivant implémente une file d’attente en arrière-plan :

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

Le code suivant lie le corps de la requête à un Stream :

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

Le code suivant montre l’intégralité du fichier Program.cs :

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • Lors de la lecture de données, le Stream est le même objet que HttpRequest.Body.
  • Le corps de la requête n’est pas mis en mémoire tampon par défaut. Une fois le corps lu, il n’est pas rembobinable. Le flux ne peut pas être lu plusieurs fois.
  • Les Stream et PipeReader ne sont pas utilisables en dehors du gestionnaire d’actions minimal, car les mémoires tampons sous-jacentes seront supprimées ou réutilisées.

Chargements de fichiers à l’aide d’IFormFile et IFormFileCollection

Le code suivant utilise IFormFile et IFormFileCollection pour charger le fichier :

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

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

Les requêtes de chargement de fichiers authentifiés sont prises en charge à l’aide d’un en-tête d’autorisation, d’un certificat client ou d’un en-tête cookie.

Liaison aux formulaires avec IFormCollection, IFormFile et IFormFileCollection

La liaison à partir de paramètres basés sur un formulaire à l’aide de IFormCollection, de IFormFileet de IFormFileCollection est prise en charge. Les métadonnées OpenAPI sont inférées pour les paramètres de formulaire afin de prendre en charge l’intégration à l’interface utilisateur Swagger.

Le code suivant télécharge des fichiers en utilisant la liaison déduite du type IFormFile :

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

Avertissement : lors de l’implémentation de formulaires, l’application doit empêcher lesattaques par falsification de requête intersites (XSRF/CSRF). Dans le code précédent, le service IAntiforgery est utilisé pour prévenir les attaques XSRF en générant et en validant un jeton anti-falsification :

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

Pour plus d'informations sur les attaques XSRF, consultez Antiforgerie avec des API minimales

Pour plus d’informations, consultez l’article Liaison de formulaire dans des API minimales;

Liaison à des collections et à des types complexes à partir de formulaires

La liaison est prise en charge pour :

  • Les collections, par exemple Liste et Dictionnaire
  • Les types complexes, par exemple, Todo ou Project

L'exemple de code suivant montre :

  • Un point de terminaison minimal qui lie une entrée de formulaire à plusieurs parties à un objet complexe.
  • Comment utiliser les services anti-falsification pour prendre en charge la génération et la validation des jetons anti-falsification.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
        <html><body>
           <form action="/todo" method="POST" enctype="multipart/form-data">
               <input name="{token.FormFieldName}" 
                                type="hidden" value="{token.RequestToken}" />
               <input type="text" name="name" />
               <input type="date" name="dueDate" />
               <input type="checkbox" name="isCompleted" value="true" />
               <input type="submit" />
               <input name="isCompleted" type="hidden" value="false" /> 
           </form>
        </body></html>
    """;
    return Results.Content(html, "text/html");
});

app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>> 
               ([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(context);
        return TypedResults.Ok(todo);
    }
    catch (AntiforgeryValidationException e)
    {
        return TypedResults.BadRequest("Invalid anti-forgery token");
    }
});

app.Run();

class Todo
{
    public string Name { get; set; } = string.Empty;
    public bool IsCompleted { get; set; } = false;
    public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}

Dans le code précédent :

  • Le paramètre cible doit être annoté avec l’attribut [FromForm] pour lever l’ambiguïté des paramètres qui doivent être lus à partir du corps JSON.
  • La liaison à partir de types complexes ou de collections n'est pas prise en charge pour les API minimales compilées avec le générateur de délégués de requête.
  • Le balisage présente une entrée masquée supplémentaire nommée isCompleted dont la valeur est false. Si la case isCompleted est cochée lorsque le formulaire est envoyé, les deux valeurs true et false sont envoyées en tant que valeurs. Si la case à cocher est décochée, seule la valeur d’entrée masquée false est envoyée. Le processus de liaison de modèle ASP.NET Core lit uniquement la première valeur lors de la liaison à une valeur bool, ce qui donne true pour les cases à cocher cochées et false pour les cases à cocher non cochées.

Voici un exemple des données de formulaire soumises au point de terminaison précédent :

__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false

Lier des tableaux et des valeurs de chaîne à partir d’en-têtes et de chaînes de requête

Le code suivant illustre la liaison de chaînes de requête à un tableau de types primitifs, de tableaux de chaînes et de StringValues :

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

La liaison de chaînes de requête ou de valeurs d’en-tête à un tableau de types complexes est prise en charge lorsque le type implémente TryParse. Le code suivant est lié à un tableau de chaînes et retourne tous les éléments avec les balises spécifiées :

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

Le code suivant montre le modèle et l’implémentation TryParse requise :

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

Le code suivant lie un tableau int :

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Pour tester le code précédent, ajoutez le point de terminaison suivant pour remplir la base de données avec des éléments Todo :

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

Utilisez un outil comme HttpRepl pour passer les données suivantes au point de terminaison précédent :

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

Le code suivant est lié à la clé d’en-tête X-Todo-Id et retourne les éléments Todo avec des valeurs Id correspondantes :

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Remarque

Lors de la liaison d’un string[] à partir d’une chaîne de requête, l’absence d’une valeur de chaîne de requête correspondante entraîne un tableau vide au lieu d’une valeur Null.

Liaison de paramètres pour les listes d’arguments avec [AsParameters]

AsParametersAttribute active la liaison de paramètres simples aux types et non la liaison de modèle complexe ou récursive.

Examinons le code ci-dessous.

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

Considérez le point de terminaison GET suivant :

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

Le struct suivant peut être utilisé pour remplacer les paramètres mis en évidence précédents :

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

Le point de terminaison GET refactorisé utilise le struct précédent avec l’attribut AsParameters :

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

Le code suivant montre des points de terminaison supplémentaires dans l’application :

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

Les classes suivantes sont utilisées pour refactoriser les listes de paramètres :

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

Le code suivant montre les points de terminaison refactorisés avec AsParameters et le struct et les classes qui précèdent :

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

Les types record suivants peuvent être utilisés pour remplacer les paramètres précédents :

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

L’utilisation de struct avec AsParameters peut être plus performante que l’utilisation d’un type record.

L’exemple de code complet dans le référentiel AspNetCore.Docs.Samples.

Liaison personnalisée

Il existe deux façons de personnaliser la liaison de paramètres :

  1. Pour les sources de liaison de routage, de requête et d’en-tête, liez des types personnalisés en ajoutant une méthode statique TryParse pour le type.
  2. Contrôlez le processus de liaison en implémentant une méthode BindAsync sur un type.

TryParse

TryParse a deux API :

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

Le code suivant affiche Point: 12.3, 10.1 avec l’URI /map?Point=12.3,10.1 :

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync possède les API suivantes :

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

Le code suivant affiche SortBy:xyz, SortDirection:Desc, CurrentPage:99 avec l’URI /products?SortBy=xyz&SortDir=Desc&Page=99 :

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

Échecs de liaison

En cas d’échec de la liaison, le framework enregistre un message de débogage et retourne différents codes d’état au client en fonction du mode d’échec.

Mode d’échec Type de paramètre nullable Source de liaison Code d’état
{ParameterType}.TryParse retourne « false » oui itinéraire/requête/en-tête 400
{ParameterType}.BindAsync retourne « null » oui personnalisé 400
Exceptions {ParameterType}.BindAsync n’a pas d’importance custom 500
Échec de la désérialisation du corps JSON n’a pas d’importance corps 400
Type de contenu incorrect (pas application/json) n’a pas d’importance corps 415

Priorité de liaison

Règles permettant de déterminer une source de liaison à partir d’un paramètre :

  1. Attribut explicite défini sur le paramètre (attributs From*) dans l’ordre suivant :
    1. Valeurs d’itinéraire : [FromRoute]
    2. Chaîne de requête : [FromQuery]
    3. En-tête : [FromHeader]
    4. Corps : [FromBody]
    5. Formulaire : [FromForm]
    6. Service : [FromServices]
    7. Valeurs du paramètre : [AsParameters]
  2. Types spéciaux
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormCollection (HttpContext.Request.Form)
    7. IFormFileCollection (HttpContext.Request.Form.Files)
    8. IFormFile (HttpContext.Request.Form.Files[paramName])
    9. Stream (HttpContext.Request.Body)
    10. PipeReader (HttpContext.Request.BodyReader)
  3. Le type de paramètre a une méthode BindAsync statique valide.
  4. Le type de paramètre est une chaîne ou a une méthode TryParse statique valide.
    1. Si le nom du paramètre existe dans le modèle d’itinéraire, par exemple, app.Map("/todo/{id}", (int id) => {});, il est lié à partir de l’itinéraire.
    2. Lié à partir de la chaîne de requête.
  5. Si le type de paramètre est un service fourni par l’injection de dépendances, il utilise ce service comme source.
  6. Le paramètre provient du corps.

Configurer les options de désérialisation JSON pour la liaison de corps

La source de liaison de corps utilise System.Text.Json pour la désérialisation. Il n’est pas possible de modifier cette valeur par défaut, mais les options de sérialisation et de désérialisation JSON peuvent être configurées.

Configurer globalement les options de désérialisation JSON

Les options qui s’appliquent globalement à une application peuvent être configurées en appelant ConfigureHttpJsonOptions. L’exemple suivant inclut des champs publics et des formats de sortie JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Étant donné que l’exemple de code configure à la fois la sérialisation et la désérialisation, il peut lire NameField et inclure NameField dans la sortie JSON.

Configurer les options de désérialisation JSON pour un point de terminaison

ReadFromJsonAsync a des surcharges qui acceptent un objet JsonSerializerOptions. L’exemple suivant inclut des champs publics et des formats de sortie JSON.

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

Étant donné que le code précédent applique les options personnalisées uniquement à la désérialisation, la sortie JSON exclut NameField.

Lire le corps de la requête

Lisez le corps de la requête directement à l’aide d’un paramètre HttpContext ou HttpRequest :

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

Le code précédent :

  • Accède au corps de la requête à l’aide de HttpRequest.BodyReader.
  • Copie le corps de la requête dans un fichier local.

Réponses

Les gestionnaires de routes prennent en charge les types de valeurs de retour suivants :

  1. Basé sur IResult : cela inclut Task<IResult> et ValueTask<IResult>
  2. string - Cela comprend Task<string> et ValueTask<string>
  3. T (Tout autre type) : cela inclut Task<T> et ValueTask<T>
Valeur de retour Comportement Type de contenu
IResult Le framework appelle IResult.ExecuteAsync Décidé par l’implémentation IResult
string Le framework écrit la chaîne directement dans la réponse text/plain
T (Tout autre type) Le JSON du framework sérialise la réponse application/json

Pour obtenir un guide plus détaillé sur les valeurs de retour du gestionnaire de routes, consultez Créer des réponses dans les applications API minimales.

Exemples de valeurs de retour

Valeurs de retour string

app.MapGet("/hello", () => "Hello World");

Valeurs de retour JSON

app.MapGet("/hello", () => new { Message = "Hello World" });

Retour TypedResults

Le code suivant retourne TypedResults :

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

Le retour de TypedResults est préférable au retour Results. Pour plus d'informations, consultez TypedResults vs Results.

Valeurs de retour IResult

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

L’exemple suivant utilise les types de résultats intégrés pour personnaliser la réponse :

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

Code d’état personnalisé

app.MapGet("/405", () => Results.StatusCode(405));

Texte

app.MapGet("/text", () => Results.Text("This is some text"));

Flux

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

Pour plus d’exemples, consultez Créer des réponses dans les applications API minimales.

Redirection

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Fichier

app.MapGet("/download", () => Results.File("myfile.text"));

Résultats intégrés

Des assistants de résultats communs existent dans les classes Results et TypedResults statiques. Le retour de TypedResults est préférable au retour Results. Pour plus d'informations, consultez TypedResults vs Results.

Personnalisation des résultats

Les applications peuvent contrôler les réponses en implémentant un type IResult personnalisé. Le code suivant est un exemple de type de résultat HTML :

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Nous vous recommandons d’ajouter une méthode d’extension à Microsoft.AspNetCore.Http.IResultExtensions pour rendre ces résultats personnalisés plus détectables.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Résultats typés

L’interface IResult peut représenter des valeurs retournées par les API minimales qui n’utilisent pas la prise en charge implicite de la sérialisation JSON de l’objet retourné dans la réponse HTTP. La classe statique Results est utilisée pour créer différents objets IResult qui représentent différents types de réponses. Par exemple, la définition du code d’état de la réponse ou la redirection vers une autre URL.

Les types implémentant IResult sont publics, ce qui permet les assertions de type lors des tests. Exemple :

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

Vous pouvez examiner les types de retour des méthodes correspondantes sur la classe statique TypedResults pour trouver le type IResult public approprié vers lequel effectuer la conversion.

Pour plus d’exemples, consultez Créer des réponses dans les applications API minimales.

Filtres

Consultez l'article :

Autorisation

Les itinéraires peuvent être protégés à l’aide de stratégies d’autorisation. Celles-ci peuvent être déclarées via l’attribut [Authorize] ou à l’aide de la méthode RequireAuthorization :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Le code précédent peut être écrit avec RequireAuthorization :

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

L’exemple suivant utilise l’autorisation basée sur la stratégie :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Autoriser les utilisateurs non authentifiés à accéder à un point de terminaison

[AllowAnonymous] permet aux utilisateurs non authentifiés d’accéder aux points de terminaison :

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

Les itinéraires peuvent avoir CORS activé à l’aide de stratégies CORS. CORS peut être déclaré via l’attribut [EnableCors] ou à l’aide de la méthode RequireCors. Les exemples suivants activent CORS :

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

Pour plus d’informations, consultez Activation les requêtes cross-origin (CORS) dans ASP.NET Core

ValidateScopes et ValidateOnBuild

ValidateScopes et ValidateOnBuild sont activés par défaut dans l’environnement de Développement, mais désactivés dans d’autres environnements.

Quand ValidateOnBuild est true, le conteneur d’injection de dépendances valide la configuration du service au moment de la génération. Si la configuration du service n’est pas valide, la génération échoue au démarrage de l’application, plutôt qu’au moment de l’exécution (runtime) lorsque le service est demandé.

Quand ValidateScopes est true, le conteneur d’injection de dépendances valide qu’un service délimité n’est pas résolu à partir de l’étendue racine. La résolution d’un service délimité à partir de l’étendue racine peut entraîner une fuite de mémoire, car le service est conservé en mémoire plus longtemps que l’étendue de la requête.

ValidateScopes et ValidateOnBuild ont la valeur false par défaut dans les modes autres que Développement pour des raisons de performances.

Le code suivant montre que ValidateScopes est activé par défaut en mode développement, mais désactivé en mode mise en production :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }

Le code suivant montre que ValidateOnBuild est activé par défaut en mode développement, mais désactivé en mode mise en production :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }

Le code suivant désactive ValidateScopes et ValidateOnBuild dans Development :

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}

Voir aussi

Ce document :

Les API minimales sont les suivantes :

WebApplication

Le code suivant est généré par un modèle ASP.NET Core :

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

app.MapGet("/", () => "Hello World!");

app.Run();

Le code précédent peut être créé via dotnet new web sur la ligne de commande ou en sélectionnant le modèle web Vide dans Visual Studio.

Le code suivant crée un WebApplication (app) sans créer explicitement de WebApplicationBuilder :

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create initialise une nouvelle instance de la classe WebApplication avec les valeurs par défaut préconfigurées.

WebApplication ajoute automatiquement l’intergiciel suivant à Minimal API applications en fonction de certaines conditions :

  • UseDeveloperExceptionPage est ajouté en premier lorsque HostingEnvironment est "Development".
  • UseRouting est ajouté ensuite si le code utilisateur n’a pas déjà appelé UseRouting et s’il existe des points de terminaison configurés, par exemple app.MapGet.
  • UseEndpoints est ajouté à la fin du pipeline d’intergiciel si des points de terminaison sont configurés.
  • UseAuthentication est ajouté immédiatement après UseRouting, si le code utilisateur n’a pas déjà appelé UseAuthentication et si IAuthenticationSchemeProvider peut être détecté dans le fournisseur de services. IAuthenticationSchemeProvider est ajouté par défaut lors de l’utilisation de AddAuthentication, et les services sont détectés à l’aide de IServiceProviderIsService.
  • UseAuthorization est ajouté après, si le code utilisateur n’a pas déjà appelé UseAuthorization et si IAuthorizationHandlerProvider peut être détecté dans le fournisseur de services. IAuthorizationHandlerProvider est ajouté par défaut lors de l’utilisation de AddAuthorization, et les services sont détectés à l’aide de IServiceProviderIsService.
  • Les intergiciels et les points de terminaison configurés par l’utilisateur sont ajoutés entre UseRouting et UseEndpoints.

Le code suivant est effectivement ce qu’un intergiciel automatique ajouté à l’application produit :

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

Dans certains cas, la configuration de l’intergiciel par défaut n’est pas correcte pour l’application et exige une modification. Par exemple, UseCors doit être appelé avant UseAuthentication et UseAuthorization. L’application doit appeler UseAuthentication et UseAuthorization, si UseCors est appelé :

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

Si l’intergiciel doit être exécuté avant l’exécution de la correspondance d’itinéraire, appeler UseRouting et placer l’intergiciel avant l’appel à UseRouting. UseEndpoints n’est pas obligatoire dans ce cas, car il est automatiquement ajouté comme décrit précédemment :

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

Lors de l’ajout d’un intergiciel de terminal :

  • L’intergiciel doit être ajouté après UseEndpoints.
  • L’application doit appeler UseRouting et UseEndpoints pour que l’intergiciel de terminal puisse être placé à l’emplacement approprié.
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

Un intergiciel de terminal est un intergiciel qui s’exécute si aucun point de terminaison ne gère la requête.

Utilisation des ports

Lorsqu’une application web est créée avec Visual Studio ou dotnet new, un fichier Properties/launchSettings.json est créé et spécifie les ports auxquels l’application répond. Dans les exemples de paramètres de port qui suivent, l’exécution de l’application à partir de Visual Studio renvoie une boîte de dialogue d’erreur Unable to connect to web server 'AppName'. Visual Studio retourne une erreur, car il attend le port spécifié dans Properties/launchSettings.json, mais l’application utilise le port spécifié par app.Run("http://localhost:3000"). Exécutez les exemples de modification de port suivants à partir de la ligne de commande.

Les sections suivantes définissent le port auquel l’application répond.

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");

Dans le code précédent, l’application répond au port 3000.

Plusieurs ports

Dans le code suivant, l’application répond aux ports 3000 et 4000.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

Définir le port à partir de la ligne de commande

La commande suivante permet à l’application de répondre au port 7777 :

dotnet run --urls="https://localhost:7777"

Si le point de terminaison Kestrel est également configuré dans le fichier appsettings.json, l’URL spécifiée par le fichier appsettings.json est utilisée. Pour plus d’informations, consultez Configuration du point de terminaison Kestrel

Lire le port à partir de l’environnement

Le code suivant lit le port à partir de l’environnement :

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

La méthode recommandée pour définir le port à partir de l’environnement consiste à utiliser la variable d’environnement ASPNETCORE_URLS, comme indiqué dans la section suivante.

Définir les ports via la variable d’environnement ASPNETCORE_URLS

La variable d’environnement ASPNETCORE_URLS est disponible pour définir le port :

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS prend en charge plusieurs URL :

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Pour plus d’informations sur l’utilisation de l’environnement, consultez Utiliser plusieurs environnements dans ASP.NET Core

Écouter sur toutes les interfaces

Les exemples suivants illustrent l’écoute sur toutes les interfaces

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Écoutez toutes les interfaces à l’aide d’ASPNETCORE_URLS

Les exemples précédents peuvent utiliser ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Spécifier HTTPS avec un certificat de développement

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Pour plus d’informations sur le certificat de développement, consultez Approuver le certificat de développement HTTPS ASP.NET Core sur Windows et macOS.

Spécifier HTTPS à l’aide d’un certificat personnalisé

Les sections suivantes montrent comment spécifier le certificat personnalisé à l’aide du fichier appsettings.json et via la configuration.

Spécifier le certificat personnalisé avec appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

Spécifier le certificat personnalisé via la configuration

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Utiliser les API de certificat

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Configuration

Le code suivant est lu à partir du système de configuration :

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

Pour plus d’informations, consultez Configuration dans ASP.NET Core

Journalisation

Le code suivant écrit un message dans le journal au démarrage de l’application :

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

Pour plus d’informations, consultez Journalisation dans .NET Core et ASP.NET Core

Accéder au conteneur d’injection de dépendances (DI)

Le code suivant montre comment obtenir des services à partir du conteneur d’authentification unique au démarrage de l’application :


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

Pour plus d’informations, consultez Injection de dépendances dans ASP.NET Core.

WebApplicationBuilder

Cette section contient un exemple de code utilisant WebApplicationBuilder.

Modifier la racine du contenu, le nom de l’application et l’environnement

Le code suivant définit la racine du contenu, le nom de l’application et l’environnement :

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder initialise une nouvelle instance de la classe WebApplicationBuilder avec les valeurs par défaut préconfigurées.

Pour plus d’informations, consultez Vue d’ensemble des principes de base d’ASP.NET Core

Modifier la racine du contenu, le nom de l’application et l’environnement à l’aide de variables d’environnement ou de la ligne de commande

Le tableau suivant montre la variable d’environnement et l’argument de ligne de commande utilisés pour modifier la racine du contenu, le nom de l’application et l’environnement :

fonctionnalité Variable d’environnement Argument de ligne de commande
Nom de l'application ASPNETCORE_APPLICATIONNAME --applicationName
Nom de l’environnement ASPNETCORE_ENVIRONMENT --environment
Racine de contenu ASPNETCORE_CONTENTROOT --contentRoot

Ajouter des fournisseurs de configuration

L’exemple suivant ajoute le fournisseur de configuration INI :

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

Pour plus d’informations, consultez Fournisseurs de configuration de fichiers dans Configuration dans ASP.NET Core.

Configuration de lecture

Par défaut, WebApplicationBuilder lit la configuration à partir de plusieurs sources, notamment :

  • appSettings.json et appSettings.{environment}.json
  • Variables d’environnement
  • Ligne de commande

Le code suivant lit HelloKey à partir de la configuration et affiche la valeur au niveau du point de terminaison /. Si la valeur de configuration est null, « Hello » est affecté à message :

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Pour obtenir la liste complète des sources de configuration lues, consultez Configuration par défaut dans Configuration dans ASP.NET Core

Ajouter des fournisseurs de journalisation

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

Ajouter des services

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Personnaliser IHostBuilder

Les méthodes d’extension existantes sur IHostBuilder sont accessibles à l’aide de la propriété Host :

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Personnaliser IWebHostBuilder

Les méthodes d’extension sur IWebHostBuilder sont accessibles à l’aide de la propriété WebApplicationBuilder.WebHost.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Modifier la racine web

Par défaut, la racine web est relative à la racine de contenu dans le dossier wwwroot. La racine web est l’endroit où l’intergiciel de fichiers statiques recherche les fichiers statiques. La racine web peut être modifiée avec WebHostOptions, la ligne de commande ou avec la méthode UseWebRoot :

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Conteneur d’injection de dépendances (DI) personnalisé

L’exemple suivant utilise Autofac :

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Ajouter un intergiciel

Tout intergiciel ASP.NET Core existant peut être configuré sur WebApplication :

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

Pour plus d’informations, consultez Intergiciel (middleware) ASP.NET Core

Page d’exceptions du développeur

WebApplication.CreateBuilder initialise une nouvelle instance de la classe WebApplicationBuilder avec les valeurs par défaut préconfigurées. La page d’exception du développeur est activée dans les valeurs par défaut préconfigurées. Lorsque le code suivant est exécuté dans l’environnement de développement, la navigation vers / présente une page conviviale qui affiche l’exception.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

Intergiciel (middleware) ASP.NET Core

Le tableau suivant répertorie certains des intergiciels fréquemment utilisés avec les API minimales.

Intergiciel (middleware) Description API
Authentification Prend en charge l’authentification. UseAuthentication
Autorisation Fournit la prise en charge des autorisations. UseAuthorization
CORS Configure le partage des ressources cross-origin (CORS). UseCors
Gestionnaire d’exceptions Gère globalement les exceptions levées par le pipeline d’intergiciels. UseExceptionHandler
En-têtes transférés Transfère les en-têtes en proxy vers la requête actuelle. UseForwardedHeaders
Redirection HTTPS Redirige toutes les requêtes HTTP vers HTTPS. UseHttpsRedirection
HSTS (HTTP Strict Transport Security) Middleware d’amélioration de la sécurité qui ajoute un en-tête de réponse spécial. UseHsts
Journalisation des requêtes Prend en charge la journalisation des requêtes et des réponses HTTP. UseHttpLogging
Délais d'attente des tests Permet de configurer les délais d'attente des requêtes, par défaut global et par point d'extrémité. UseRequestTimeouts
Journalisation des requêtes W3C Prend en charge la journalisation des requêtes et des réponses HTTP au format W3C. UseW3CLogging
Mise en cache des réponses Prend en charge la mise en cache des réponses. UseResponseCaching
Compression des réponses Prend en charge la compression des réponses. UseResponseCompression
Session Prend en charge la gestion des sessions utilisateur. UseSession
Fichiers statiques Prend en charge le traitement des fichiers statiques et l’exploration des répertoires. UseStaticFiles, UseFileServer
WebSockets Autorise le protocole WebSockets. UseWebSockets

Les sections suivantes couvrent la gestion des requêtes : routage, liaison de paramètres et réponses.

Routage

Un WebApplication configuré prend en charge Map{Verb} et MapMethods, où {Verb} est une méthode HTTP en casse mixte, comme Get, Post, Put ou Delete :

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

Les arguments Delegate passés à ces méthodes sont appelés « gestionnaires de routes ».

Gestionnaires de routes

Les gestionnaires de routes sont des méthodes qui s’exécutent lorsque la route correspond. Les gestionnaires de routes peuvent être une expression lambda, une fonction locale, une méthode d’instance ou une méthode statique. Les gestionnaires de routes peuvent être synchrones ou asynchrones.

Expression lambda

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Fonction locale

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Méthode d'instance

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

Méthode statique

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

Point de terminaison défini à l’extérieur de Program.cs

Les API minimales n’ont pas besoin d’être situées à l’emplacement Program.cs.

Program.cs

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}

Consultez également Groupes d’itinéraires plus loin dans cet article.

Les points de terminaison peuvent recevoir des noms afin de générer des URL vers le point de terminaison. L’utilisation d’un point de terminaison nommé évite d’avoir à coder des chemins d’accès en dur dans une application :

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

Le code précédent affiche The link to the hello endpoint is /hello à partir du point de terminaison /.

REMARQUE : Les noms des points de terminaison respectent la casse.

Les noms des points de terminaison :

  • Il doit être globalement unique.
  • Sont utilisés comme ID d’opération OpenAPI lorsque la prise en charge d’OpenAPI est activée. Pour plus d’informations, consultez OpenAPI.

Paramètres de routage

Les paramètres de routage peuvent être capturés dans le cadre de la définition du modèle de route :

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

Le code précédent retourne The user id is 3 and book id is 7 à partir de l’URI /users/3/books/7.

Le gestionnaire de routes peut déclarer les paramètres à capturer. Lorsqu’une requête est effectuée sur une route avec des paramètres déclarés pour la capture, les paramètres sont analysés et transmis au gestionnaire. Cela permet de capturer facilement les valeurs avec un type sécurisé. Dans le code précédent, userId et bookId sont tous deux de type int.

Dans le code précédent, si l’une ou l’autre valeur de route ne peut pas être convertie en int, une exception est levée. La requête GET /users/hello/books/3 lève l’exception suivante :

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Caractères génériques et routes catch all

Les éléments suivants interceptent tous les retours de route Routing to hello à partir du point de terminaison « /posts/hello » :

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Contraintes d'itinéraire

Les contraintes de routage limitent le comportement de correspondance d’une route.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

Le tableau suivant montre les modèles de route précédents et leur comportement :

Modèle de routage Exemple d’URI en correspondance
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

Pour plus d’informations, consultez Référence sur la contrainte d’itinéraire dans Routage dans ASP.NET Core.

Groupes de routes

La méthode d’extension MapGroup permet d’organiser des groupes de points de terminaison avec un préfixe commun. Cela réduit le code répétitif et permet de personnaliser des groupes entiers de points de terminaison avec un seul appel à des méthodes comme RequireAuthorization et WithMetadata, qui ajoutent des métadonnées de point de terminaison.

Par exemple, le code suivant crée deux groupes de points de terminaison similaires :

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

Dans ce scénario, vous pouvez utiliser une adresse relative pour l’en-tête Location dans le résultat 201 Created :

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

Le premier groupe de points de terminaison correspond uniquement aux requêtes précédées de /public/todos, accessibles sans authentification. Le second groupe de points de terminaison correspond uniquement aux requêtes préfixées par /private/todos, qui nécessitent une authentification.

La QueryPrivateTodosfabrique de filtres de point de terminaison est une fonction locale qui modifie les paramètres TodoDb du gestionnaire de routes pour autoriser l’accès aux données de tâche privées et les stocker.

Les groupes de routage prennent également en charge les groupes imbriqués et les modèles de préfixe complexes avec des contraintes et des paramètres de routage. Dans l’exemple suivant, un gestionnaire de routage mappé au groupe user peut capturer les paramètres de routage {org} et {group} définis dans les préfixes de groupe externe.

Le préfixe peut également être vide. Cela peut être utile pour ajouter des métadonnées ou des filtres de point de terminaison à un groupe de points de terminaison sans modifier le modèle de routage.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

L’ajout de filtres ou de métadonnées à un groupe se comporte de la même façon que si vous les ajoutiez individuellement à chaque point de terminaison avant d’ajouter des filtres ou des métadonnées supplémentaires qui ont pu être ajoutés à un groupe interne ou à un point de terminaison spécifique.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

Dans l’exemple ci-dessus, le filtre externe enregistre la requête entrante avant le filtre interne, même si elle a été ajoutée en deuxième. Étant donné que les filtres ont été appliqués à différents groupes, l’ordre dans lequel ils ont été ajoutés les uns par rapport aux autres n’a pas d’importance. Les filtres d’ordre ajoutés sont importants s’ils sont appliqués au même groupe ou au même point de terminaison spécifique.

Une requête sur /outer/inner/ journalisera les éléments suivants :

/outer group filter
/inner group filter
MapGet filter

Liaison de paramètres

La liaison de paramètres est le processus de conversion des données de requête en paramètres fortement typés qui sont exprimés par les gestionnaires de routes. Une source de liaison détermine à partir d’où les paramètres sont liés. Les sources de liaison peuvent être explicites ou déduites en fonction de la méthode HTTP et du type de paramètre.

Sources de liaison prises en charge :

  • Valeurs d’itinéraire
  • Chaîne de requête
  • Header
  • Corps (JSON)
  • Services fournis par l’injection de dépendances
  • Personnalisé

La liaison à partir de valeurs de formulaire n’est pas prise en charge en mode natif dans .NET 6 et 7.

Le gestionnaire de routage GET suivant utilise certaines de ces sources de liaison de paramètres :

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

Le tableau suivant montre la relation entre les paramètres utilisés dans l’exemple précédent et les sources de liaison associées.

Paramètre Source de liaison
id valeur de route
page chaîne de requête
customHeader en-tête
service Fourni par l’injection de dépendances

Les méthodes HTTP GET, HEAD, OPTIONS et DELETE ne sont pas implicitement liées à partir du corps. Pour lier à partir du corps (JSON) pour ces méthodes HTTP, liez explicitement avec [FromBody] ou lisez à partir de HttpRequest.

L’exemple de gestionnaire de routage POST suivant utilise une source de liaison de corps (JSON) pour le paramètre person :

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

Les paramètres des exemples précédents sont tous liés automatiquement à partir des données de requête. Pour illustrer la commodité de la liaison de paramètres, les gestionnaires de routage suivants montrent comment lire les données de requête directement à partir de la requête :

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

Liaison de paramètre explicite

Les attributs peuvent être utilisés pour déclarer explicitement à partir d’où les paramètres sont liés.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
Paramètre Source de liaison
id valeur de routage avec le nom id
page chaîne de requête avec le nom "p"
service Fourni par l’injection de dépendances
contentType en-tête avec le nom "Content-Type"

Remarque

La liaison à partir de valeurs de formulaire n’est pas prise en charge en mode natif dans .NET 6 et 7.

Liaison de paramètre avec injection de dépendances

La liaison de paramètre pour les API minimales lie des paramètres via l’injection de dépendance quand le type est configuré en tant que service. Il n’est pas nécessaire d’appliquer explicitement l’attribut [FromServices] à un paramètre. Dans le code suivant, les deux actions retournent l’heure :

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

Paramètres optionnels

Les paramètres déclarés dans les gestionnaires d’itinéraire sont traités comme requis :

  • Si une requête correspond à l’itinéraire, le gestionnaire d’itinéraire s’exécute uniquement si tous les paramètres requis sont fournis dans la requête.
  • Le fait de ne pas fournir tous les paramètres requis entraîne une erreur.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 3 retournés
/products BadHttpRequestException : le paramètre obligatoire « int pageNumber » n’a pas été fourni à partir de la chaîne de requête.
/products/1 Erreur HTTP 404, aucune route correspondante

Pour rendre pageNumber facultatif, définissez le type comme facultatif ou fournissez une valeur par défaut :

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 3 retournés
/products 1 retourné
/products2 1 retourné

La valeur nullable et la valeur par défaut précédentes s’appliquent à toutes les sources :

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

app.MapPost("/products", (Product? product) => { });

app.Run();

Le code précédent appelle la méthode avec un produit null si aucun corps de requête n’est envoyé.

REMARQUE : Si des données non valides sont fournies et que le paramètre est nullable, le gestionnaire de routage n’est pas exécuté.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 3 retourné
/products 1 retourné
/products?pageNumber=two BadHttpRequestException : Échec de la liaison du paramètre "Nullable<int> pageNumber" à partir de « two ».
/products/two Erreur HTTP 404, aucune route correspondante

Pour plus d’informations, consultez la section Échecs de liaison.

Types spéciaux

Les types suivants sont liés sans attributs explicites :

  • HttpContext : contexte qui contient toutes les informations sur la requête ou la réponse HTTP actuelle :

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest et HttpResponse : requête HTTP et réponse HTTP :

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken : jeton d’annulation associé à la requête HTTP actuelle :

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal : utilisateur associé à la requête, lié à partir de HttpContext.User :

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

Lier le corps de la requête en tant que Stream ou PipeReader

Le corps de la requête peut être lié en tant que Stream ou PipeReader pour prendre en charge efficacement les scénarios où l’utilisateur doit traiter des données et :

  • Stockez les données dans le stockage d’objets blob ou placez les données en file d’attente dans un fournisseur de file d’attente.
  • Traitez les données stockées avec un processus Worker ou une fonction cloud.

Par exemple, les données peuvent être mises en file d’attente pour le Stockage File d’attente Azure ou stockées dans le Stockage Blob Azure.

Le code suivant implémente une file d’attente en arrière-plan :

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

Le code suivant lie le corps de la requête à un Stream :

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

Le code suivant montre l’intégralité du fichier Program.cs :

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • Lors de la lecture de données, le Stream est le même objet que HttpRequest.Body.
  • Le corps de la requête n’est pas mis en mémoire tampon par défaut. Une fois le corps lu, il n’est pas rembobinable. Le flux ne peut pas être lu plusieurs fois.
  • Les Stream et PipeReader ne sont pas utilisables en dehors du gestionnaire d’actions minimal, car les mémoires tampons sous-jacentes seront supprimées ou réutilisées.

Chargements de fichiers à l’aide d’IFormFile et IFormFileCollection

Le code suivant utilise IFormFile et IFormFileCollection pour charger le fichier :

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

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

Les requêtes de chargement de fichiers authentifiés sont prises en charge à l’aide d’un en-tête d’autorisation, d’un certificat client ou d’un en-tête cookie.

Il n’existe pas de prise en charge intégrée de l’antifalsification dans ASP.NET Core 7.0. Antiforgery est disponible dans ASP.NET Core 8,0 et versions ultérieures. Toutefois, elle peut être implémentée à l’aide du service IAntiforgery.

Lier des tableaux et des valeurs de chaîne à partir d’en-têtes et de chaînes de requête

Le code suivant illustre la liaison de chaînes de requête à un tableau de types primitifs, de tableaux de chaînes et de StringValues :

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

La liaison de chaînes de requête ou de valeurs d’en-tête à un tableau de types complexes est prise en charge lorsque le type implémente TryParse. Le code suivant est lié à un tableau de chaînes et retourne tous les éléments avec les balises spécifiées :

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

Le code suivant montre le modèle et l’implémentation TryParse requise :

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

Le code suivant lie un tableau int :

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Pour tester le code précédent, ajoutez le point de terminaison suivant pour remplir la base de données avec des éléments Todo :

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

Utilisez un outil de test d’API comme HttpRepl pour passer les données suivantes au point de terminaison précédent :

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

Le code suivant est lié à la clé d’en-tête X-Todo-Id et retourne les éléments Todo avec des valeurs Id correspondantes :

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Remarque

Lors de la liaison d’un string[] à partir d’une chaîne de requête, l’absence d’une valeur de chaîne de requête correspondante entraîne un tableau vide au lieu d’une valeur Null.

Liaison de paramètres pour les listes d’arguments avec [AsParameters]

AsParametersAttribute active la liaison de paramètres simples aux types et non la liaison de modèle complexe ou récursive.

Examinons le code ci-dessous.

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

Considérez le point de terminaison GET suivant :

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

Le struct suivant peut être utilisé pour remplacer les paramètres mis en évidence précédents :

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

Le point de terminaison GET refactorisé utilise le struct précédent avec l’attribut AsParameters :

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

Le code suivant montre des points de terminaison supplémentaires dans l’application :

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

Les classes suivantes sont utilisées pour refactoriser les listes de paramètres :

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

Le code suivant montre les points de terminaison refactorisés avec AsParameters et le struct et les classes qui précèdent :

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

Les types record suivants peuvent être utilisés pour remplacer les paramètres précédents :

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

L’utilisation de struct avec AsParameters peut être plus performante que l’utilisation d’un type record.

L’exemple de code complet dans le référentiel AspNetCore.Docs.Samples.

Liaison personnalisée

Il existe deux façons de personnaliser la liaison de paramètres :

  1. Pour les sources de liaison de routage, de requête et d’en-tête, liez des types personnalisés en ajoutant une méthode statique TryParse pour le type.
  2. Contrôlez le processus de liaison en implémentant une méthode BindAsync sur un type.

TryParse

TryParse a deux API :

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

Le code suivant affiche Point: 12.3, 10.1 avec l’URI /map?Point=12.3,10.1 :

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync possède les API suivantes :

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

Le code suivant affiche SortBy:xyz, SortDirection:Desc, CurrentPage:99 avec l’URI /products?SortBy=xyz&SortDir=Desc&Page=99 :

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

Échecs de liaison

En cas d’échec de la liaison, le framework enregistre un message de débogage et retourne différents codes d’état au client en fonction du mode d’échec.

Mode d’échec Type de paramètre nullable Source de liaison Code d’état
{ParameterType}.TryParse retourne « false » oui itinéraire/requête/en-tête 400
{ParameterType}.BindAsync retourne « null » oui personnalisé 400
Exceptions {ParameterType}.BindAsync n’a pas d’importance personnalisé 500
Échec de la désérialisation du corps JSON n’a pas d’importance body 400
Type de contenu incorrect (pas application/json) n’a pas d’importance body 415

Priorité de liaison

Règles permettant de déterminer une source de liaison à partir d’un paramètre :

  1. Attribut explicite défini sur le paramètre (attributs From*) dans l’ordre suivant :
    1. Valeurs d’itinéraire : [FromRoute]
    2. Chaîne de requête : [FromQuery]
    3. En-tête : [FromHeader]
    4. Corps : [FromBody]
    5. Service : [FromServices]
    6. Valeurs du paramètre : [AsParameters]
  2. Types spéciaux
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormFileCollection (HttpContext.Request.Form.Files)
    7. IFormFile (HttpContext.Request.Form.Files[paramName])
    8. Stream (HttpContext.Request.Body)
    9. PipeReader (HttpContext.Request.BodyReader)
  3. Le type de paramètre a une méthode BindAsync statique valide.
  4. Le type de paramètre est une chaîne ou a une méthode TryParse statique valide.
    1. Si le nom du paramètre existe dans le modèle d’itinéraire, par exemple app.Map("/todo/{id}", (int id) => {});, il est lié à partir de l’itinéraire.
    2. Lié à partir de la chaîne de requête.
  5. Si le type de paramètre est un service fourni par l’injection de dépendances, il utilise ce service comme source.
  6. Le paramètre provient du corps.

Configurer les options de désérialisation JSON pour la liaison de corps

La source de liaison de corps utilise System.Text.Json pour la désérialisation. Il n’est pas possible de modifier cette valeur par défaut, mais les options de sérialisation et de désérialisation JSON peuvent être configurées.

Configurer globalement les options de désérialisation JSON

Les options qui s’appliquent globalement à une application peuvent être configurées en appelant ConfigureHttpJsonOptions. L’exemple suivant inclut des champs publics et des formats de sortie JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Étant donné que l’exemple de code configure à la fois la sérialisation et la désérialisation, il peut lire NameField et inclure NameField dans la sortie JSON.

Configurer les options de désérialisation JSON pour un point de terminaison

ReadFromJsonAsync a des surcharges qui acceptent un objet JsonSerializerOptions. L’exemple suivant inclut des champs publics et des formats de sortie JSON.

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

Étant donné que le code précédent applique les options personnalisées uniquement à la désérialisation, la sortie JSON exclut NameField.

Lire le corps de la requête

Lisez le corps de la requête directement à l’aide d’un paramètre HttpContext ou HttpRequest :

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

Le code précédent :

  • Accède au corps de la requête à l’aide de HttpRequest.BodyReader.
  • Copie le corps de la requête dans un fichier local.

Réponses

Les gestionnaires de routes prennent en charge les types de valeurs de retour suivants :

  1. Basé sur IResult : cela inclut Task<IResult> et ValueTask<IResult>
  2. string - Cela comprend Task<string> et ValueTask<string>
  3. T (Tout autre type) : cela inclut Task<T> et ValueTask<T>
Valeur de retour Comportement Type de contenu
IResult Le framework appelle IResult.ExecuteAsync Décidé par l’implémentation IResult
string Le framework écrit la chaîne directement dans la réponse text/plain
T (Tout autre type) Le JSON du framework sérialise la réponse application/json

Pour obtenir un guide plus détaillé sur les valeurs de retour du gestionnaire de routes, consultez Créer des réponses dans les applications API minimales.

Exemples de valeurs de retour

Valeurs de retour string

app.MapGet("/hello", () => "Hello World");

Valeurs de retour JSON

app.MapGet("/hello", () => new { Message = "Hello World" });

Retour TypedResults

Le code suivant retourne TypedResults :

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

Le retour de TypedResults est préférable au retour Results. Pour plus d'informations, consultez TypedResults vs Results.

Valeurs de retour IResult

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

L’exemple suivant utilise les types de résultats intégrés pour personnaliser la réponse :

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

Code d’état personnalisé

app.MapGet("/405", () => Results.StatusCode(405));

Texte

app.MapGet("/text", () => Results.Text("This is some text"));

Flux

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

Pour plus d’exemples, consultez Créer des réponses dans les applications API minimales.

Redirection

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Fichier

app.MapGet("/download", () => Results.File("myfile.text"));

Résultats intégrés

Des assistants de résultats communs existent dans les classes Results et TypedResults statiques. Le retour de TypedResults est préférable au retour Results. Pour plus d'informations, consultez TypedResults vs Results.

Personnalisation des résultats

Les applications peuvent contrôler les réponses en implémentant un type IResult personnalisé. Le code suivant est un exemple de type de résultat HTML :

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Nous vous recommandons d’ajouter une méthode d’extension à Microsoft.AspNetCore.Http.IResultExtensions pour rendre ces résultats personnalisés plus détectables.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Résultats typés

L’interface IResult peut représenter des valeurs retournées par les API minimales qui n’utilisent pas la prise en charge implicite de la sérialisation JSON de l’objet retourné dans la réponse HTTP. La classe statique Results est utilisée pour créer différents objets IResult qui représentent différents types de réponses. Par exemple, la définition du code d’état de la réponse ou la redirection vers une autre URL.

Les types implémentant IResult sont publics, ce qui permet les assertions de type lors des tests. Exemple :

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

Vous pouvez examiner les types de retour des méthodes correspondantes sur la classe statique TypedResults pour trouver le type IResult public approprié vers lequel effectuer la conversion.

Pour plus d’exemples, consultez Créer des réponses dans les applications API minimales.

Filtres

Consultez Filtres dans les applications d’API minimales

Autorisation

Les itinéraires peuvent être protégés à l’aide de stratégies d’autorisation. Celles-ci peuvent être déclarées via l’attribut [Authorize] ou à l’aide de la méthode RequireAuthorization :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Le code précédent peut être écrit avec RequireAuthorization :

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

L’exemple suivant utilise l’autorisation basée sur la stratégie :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Autoriser les utilisateurs non authentifiés à accéder à un point de terminaison

[AllowAnonymous] permet aux utilisateurs non authentifiés d’accéder aux points de terminaison :

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

Les itinéraires peuvent avoir CORS activé à l’aide de stratégies CORS. CORS peut être déclaré via l’attribut [EnableCors] ou à l’aide de la méthode RequireCors. Les exemples suivants activent CORS :

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

Pour plus d’informations, consultez Activation les requêtes cross-origin (CORS) dans ASP.NET Core

Voir aussi

Ce document :

Les API minimales sont les suivantes :

WebApplication

Le code suivant est généré par un modèle ASP.NET Core :

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

app.MapGet("/", () => "Hello World!");

app.Run();

Le code précédent peut être créé via dotnet new web sur la ligne de commande ou en sélectionnant le modèle web Vide dans Visual Studio.

Le code suivant crée un WebApplication (app) sans créer explicitement de WebApplicationBuilder :

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create initialise une nouvelle instance de la classe WebApplication avec les valeurs par défaut préconfigurées.

Utilisation des ports

Lorsqu’une application web est créée avec Visual Studio ou dotnet new, un fichier Properties/launchSettings.json est créé et spécifie les ports auxquels l’application répond. Dans les exemples de paramètres de port qui suivent, l’exécution de l’application à partir de Visual Studio renvoie une boîte de dialogue d’erreur Unable to connect to web server 'AppName'. Exécutez les exemples de modification de port suivants à partir de la ligne de commande.

Les sections suivantes définissent le port auquel l’application répond.

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");

Dans le code précédent, l’application répond au port 3000.

Plusieurs ports

Dans le code suivant, l’application répond aux ports 3000 et 4000.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

Définir le port à partir de la ligne de commande

La commande suivante permet à l’application de répondre au port 7777 :

dotnet run --urls="https://localhost:7777"

Si le point de terminaison Kestrel est également configuré dans le fichier appsettings.json, l’URL spécifiée par le fichier appsettings.json est utilisée. Pour plus d’informations, consultez Configuration du point de terminaison Kestrel

Lire le port à partir de l’environnement

Le code suivant lit le port à partir de l’environnement :

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

La méthode recommandée pour définir le port à partir de l’environnement consiste à utiliser la variable d’environnement ASPNETCORE_URLS, comme indiqué dans la section suivante.

Définir les ports via la variable d’environnement ASPNETCORE_URLS

La variable d’environnement ASPNETCORE_URLS est disponible pour définir le port :

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS prend en charge plusieurs URL :

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Écouter sur toutes les interfaces

Les exemples suivants illustrent l’écoute sur toutes les interfaces

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Écoutez toutes les interfaces à l’aide d’ASPNETCORE_URLS

Les exemples précédents peuvent utiliser ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Spécifier HTTPS avec un certificat de développement

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Pour plus d’informations sur le certificat de développement, consultez Approuver le certificat de développement HTTPS ASP.NET Core sur Windows et macOS.

Spécifier HTTPS à l’aide d’un certificat personnalisé

Les sections suivantes montrent comment spécifier le certificat personnalisé à l’aide du fichier appsettings.json et via la configuration.

Spécifier le certificat personnalisé avec appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

Spécifier le certificat personnalisé via la configuration

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Utiliser les API de certificat

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

Lire l’environnement

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

Pour plus d’informations sur l’utilisation de l’environnement, consultez Utiliser plusieurs environnements dans ASP.NET Core

Configuration

Le code suivant est lu à partir du système de configuration :

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();

Pour plus d’informations, consultez Configuration dans ASP.NET Core

Journalisation

Le code suivant écrit un message dans le journal au démarrage de l’application :

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

Pour plus d’informations, consultez Journalisation dans .NET Core et ASP.NET Core

Accéder au conteneur d’injection de dépendances (DI)

Le code suivant montre comment obtenir des services à partir du conteneur d’authentification unique au démarrage de l’application :


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

Pour plus d’informations, consultez Injection de dépendances dans ASP.NET Core.

WebApplicationBuilder

Cette section contient un exemple de code utilisant WebApplicationBuilder.

Modifier la racine du contenu, le nom de l’application et l’environnement

Le code suivant définit la racine du contenu, le nom de l’application et l’environnement :

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder initialise une nouvelle instance de la classe WebApplicationBuilder avec les valeurs par défaut préconfigurées.

Pour plus d’informations, consultez Vue d’ensemble des principes de base d’ASP.NET Core

Modifier la racine du contenu, le nom de l’application et l’environnement à l’aide de variables d’environnement ou de la ligne de commande

Le tableau suivant montre la variable d’environnement et l’argument de ligne de commande utilisés pour modifier la racine du contenu, le nom de l’application et l’environnement :

fonctionnalité Variable d’environnement Argument de ligne de commande
Nom de l'application ASPNETCORE_APPLICATIONNAME --applicationName
Nom de l’environnement ASPNETCORE_ENVIRONMENT --environment
Racine de contenu ASPNETCORE_CONTENTROOT --contentRoot

Ajouter des fournisseurs de configuration

L’exemple suivant ajoute le fournisseur de configuration INI :

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

Pour plus d’informations, consultez Fournisseurs de configuration de fichiers dans Configuration dans ASP.NET Core.

Configuration de lecture

Par défaut, WebApplicationBuilder lit la configuration à partir de plusieurs sources, notamment :

  • appSettings.json et appSettings.{environment}.json
  • Variables d’environnement
  • Ligne de commande

Pour obtenir la liste complète des sources de configuration lues, consultez Configuration par défaut dans Configuration dans ASP.NET Core

Le code suivant lit HelloKey à partir de la configuration et affiche la valeur au niveau du point de terminaison /. Si la valeur de configuration est null, « Hello » est affecté à message :

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Lire l’environnement

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Ajouter des fournisseurs de journalisation

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

Ajouter des services

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Personnaliser IHostBuilder

Les méthodes d’extension existantes sur IHostBuilder sont accessibles à l’aide de la propriété Host :

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Personnaliser IWebHostBuilder

Les méthodes d’extension sur IWebHostBuilder sont accessibles à l’aide de la propriété WebApplicationBuilder.WebHost.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Modifier la racine web

Par défaut, la racine web est relative à la racine de contenu dans le dossier wwwroot. La racine web est l’endroit où l’intergiciel de fichiers statiques recherche les fichiers statiques. La racine web peut être modifiée avec WebHostOptions, la ligne de commande ou avec la méthode UseWebRoot :

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Conteneur d’injection de dépendances (DI) personnalisé

L’exemple suivant utilise Autofac :

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Ajouter un intergiciel

Tout intergiciel ASP.NET Core existant peut être configuré sur WebApplication :

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

Pour plus d’informations, consultez Intergiciel (middleware) ASP.NET Core

Page d’exceptions du développeur

WebApplication.CreateBuilder initialise une nouvelle instance de la classe WebApplicationBuilder avec les valeurs par défaut préconfigurées. La page d’exception du développeur est activée dans les valeurs par défaut préconfigurées. Lorsque le code suivant est exécuté dans l’environnement de développement, la navigation vers / présente une page conviviale qui affiche l’exception.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

Intergiciel (middleware) ASP.NET Core

Le tableau suivant répertorie certains des intergiciels fréquemment utilisés avec les API minimales.

Intergiciel (middleware) Description API
Authentification Prend en charge l’authentification. UseAuthentication
Autorisation Fournit la prise en charge des autorisations. UseAuthorization
CORS Configure le partage des ressources cross-origin (CORS). UseCors
Gestionnaire d’exceptions Gère globalement les exceptions levées par le pipeline d’intergiciels. UseExceptionHandler
En-têtes transférés Transfère les en-têtes en proxy vers la requête actuelle. UseForwardedHeaders
Redirection HTTPS Redirige toutes les requêtes HTTP vers HTTPS. UseHttpsRedirection
HSTS (HTTP Strict Transport Security) Middleware d’amélioration de la sécurité qui ajoute un en-tête de réponse spécial. UseHsts
Journalisation des requêtes Prend en charge la journalisation des requêtes et des réponses HTTP. UseHttpLogging
Journalisation des requêtes W3C Prend en charge la journalisation des requêtes et des réponses HTTP au format W3C. UseW3CLogging
Mise en cache des réponses Prend en charge la mise en cache des réponses. UseResponseCaching
Compression des réponses Prend en charge la compression des réponses. UseResponseCompression
Session Prend en charge la gestion des sessions utilisateur. UseSession
Fichiers statiques Prend en charge le traitement des fichiers statiques et l’exploration des répertoires. UseStaticFiles, UseFileServer
WebSockets Autorise le protocole WebSockets. UseWebSockets

Gestion des demandes

Les sections suivantes couvrent le routage, la liaison de paramètres et les réponses.

Routage

Une WebApplication configurée prend en charge Map{Verb} et MapMethods :

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

Gestionnaires de routes

Les gestionnaires de routes sont des méthodes qui s’exécutent lorsque l’itinéraire correspond. Les gestionnaires de routes peuvent être une fonction de n’importe quelle forme, y compris synchrone ou asynchrone. Les gestionnaires de routes peuvent être une expression lambda, une fonction locale, une méthode d’instance ou une méthode statique.

Expression lambda

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Fonction locale

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Méthode d'instance

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

Méthode statique

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

Les points de terminaison peuvent recevoir des noms afin de générer des URL vers le point de terminaison. L’utilisation d’un point de terminaison nommé évite d’avoir à coder des chemins d’accès en dur dans une application :

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

Le code précédent affiche The link to the hello endpoint is /hello à partir du point de terminaison /.

REMARQUE : Les noms des points de terminaison respectent la casse.

Les noms des points de terminaison :

  • Il doit être globalement unique.
  • Sont utilisés comme ID d’opération OpenAPI lorsque la prise en charge d’OpenAPI est activée. Pour plus d’informations, consultez OpenAPI.

Paramètres de routage

Les paramètres de routage peuvent être capturés dans le cadre de la définition du modèle de route :

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

Le code précédent retourne The user id is 3 and book id is 7 à partir de l’URI /users/3/books/7.

Le gestionnaire de routes peut déclarer les paramètres à capturer. Lorsqu’une requête est effectuée sur une route avec des paramètres déclarés pour la capture, les paramètres sont analysés et transmis au gestionnaire. Cela permet de capturer facilement les valeurs avec un type sécurisé. Dans le code précédent, userId et bookId sont tous deux de type int.

Dans le code précédent, si l’une ou l’autre valeur de route ne peut pas être convertie en int, une exception est levée. La requête GET /users/hello/books/3 lève l’exception suivante :

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Caractères génériques et routes catch all

Les éléments suivants interceptent tous les retours de route Routing to hello à partir du point de terminaison « /posts/hello » :

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Contraintes d'itinéraire

Les contraintes de routage limitent le comportement de correspondance d’une route.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

Le tableau suivant montre les modèles de route précédents et leur comportement :

Modèle de routage Exemple d’URI en correspondance
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

Pour plus d’informations, consultez Référence sur la contrainte d’itinéraire dans Routage dans ASP.NET Core.

Liaison de paramètres

La liaison de paramètres est le processus de conversion des données de requête en paramètres fortement typés qui sont exprimés par les gestionnaires de routes. Une source de liaison détermine à partir d’où les paramètres sont liés. Les sources de liaison peuvent être explicites ou déduites en fonction de la méthode HTTP et du type de paramètre.

Sources de liaison prises en charge :

  • Valeurs d’itinéraire
  • Chaîne de requête
  • Header
  • Corps (JSON)
  • Services fournis par l’injection de dépendances
  • Personnalisé

Remarque

La liaison à partir de valeurs de formulaire n’est pas prise en charge en mode natif dans .NET.

L’exemple de gestionnaire de routes GET suivant utilise certaines de ces sources de liaison de paramètres :

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

Le tableau suivant montre la relation entre les paramètres utilisés dans l’exemple précédent et les sources de liaison associées.

Paramètre Source de liaison
id valeur de route
page chaîne de requête
customHeader en-tête
service Fourni par l’injection de dépendances

Les méthodes HTTP GET, HEAD, OPTIONS et DELETE ne sont pas implicitement liées à partir du corps. Pour lier à partir du corps (JSON) pour ces méthodes HTTP, liez explicitement avec [FromBody] ou lisez à partir de HttpRequest.

L’exemple de gestionnaire de routage POST suivant utilise une source de liaison de corps (JSON) pour le paramètre person :

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

Les paramètres des exemples précédents sont tous liés automatiquement à partir des données de requête. Pour illustrer la commodité de la liaison de paramètres, les exemples de gestionnaires de routage suivants montrent comment lire les données de requête directement à partir de la requête :

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

Liaison de paramètre explicite

Les attributs peuvent être utilisés pour déclarer explicitement à partir d’où les paramètres sont liés.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
Paramètre Source de liaison
id valeur de routage avec le nom id
page chaîne de requête avec le nom "p"
service Fourni par l’injection de dépendances
contentType en-tête avec le nom "Content-Type"

Remarque

La liaison à partir de valeurs de formulaire n’est pas prise en charge en mode natif dans .NET.

Liaison de paramètre avec DI

La liaison de paramètre pour les API minimales lie des paramètres via l’injection de dépendance quand le type est configuré en tant que service. Il n’est pas nécessaire d’appliquer explicitement l’attribut [FromServices] à un paramètre. Dans le code suivant, les deux actions retournent l’heure :

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

Paramètres optionnels

Les paramètres déclarés dans les gestionnaires d’itinéraire sont traités comme requis :

  • Si une requête correspond à l’itinéraire, le gestionnaire d’itinéraire s’exécute uniquement si tous les paramètres requis sont fournis dans la requête.
  • Le fait de ne pas fournir tous les paramètres requis entraîne une erreur.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 3 retournés
/products BadHttpRequestException : le paramètre obligatoire « int pageNumber » n’a pas été fourni à partir de la chaîne de requête.
/products/1 Erreur HTTP 404, aucune route correspondante

Pour rendre pageNumber facultatif, définissez le type comme facultatif ou fournissez une valeur par défaut :

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 3 retournés
/products 1 retourné
/products2 1 retourné

La valeur nullable et la valeur par défaut précédentes s’appliquent à toutes les sources :

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

app.MapPost("/products", (Product? product) => { });

app.Run();

Le code précédent appelle la méthode avec un produit null si aucun corps de requête n’est envoyé.

REMARQUE : Si des données non valides sont fournies et que le paramètre est nullable, le gestionnaire de routage n’est pas exécuté.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 3 retourné
/products 1 retourné
/products?pageNumber=two BadHttpRequestException : Échec de la liaison du paramètre "Nullable<int> pageNumber" à partir de « two ».
/products/two Erreur HTTP 404, aucune route correspondante

Pour plus d’informations, consultez la section Échecs de liaison.

Types spéciaux

Les types suivants sont liés sans attributs explicites :

  • HttpContext : contexte qui contient toutes les informations sur la requête ou la réponse HTTP actuelle :

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest et HttpResponse : requête HTTP et réponse HTTP :

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken : jeton d’annulation associé à la requête HTTP actuelle :

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal : utilisateur associé à la requête, lié à partir de HttpContext.User :

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

Liaison personnalisée

Il existe deux façons de personnaliser la liaison de paramètres :

  1. Pour les sources de liaison de routage, de requête et d’en-tête, liez des types personnalisés en ajoutant une méthode statique TryParse pour le type.
  2. Contrôlez le processus de liaison en implémentant une méthode BindAsync sur un type.

TryParse

TryParse a deux API :

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

Le code suivant affiche Point: 12.3, 10.1 avec l’URI /map?Point=12.3,10.1 :

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync possède les API suivantes :

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

Le code suivant affiche SortBy:xyz, SortDirection:Desc, CurrentPage:99 avec l’URI /products?SortBy=xyz&SortDir=Desc&Page=99 :

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

Échecs de liaison

En cas d’échec de la liaison, le framework enregistre un message de débogage et retourne différents codes d’état au client en fonction du mode d’échec.

Mode d’échec Type de paramètre nullable Source de liaison Code d’état
{ParameterType}.TryParse retourne « false » oui itinéraire/requête/en-tête 400
{ParameterType}.BindAsync retourne « null » oui personnalisé 400
Exceptions {ParameterType}.BindAsync n’a pas d’importance personnalisé 500
Échec de la désérialisation du corps JSON n’a pas d’importance body 400
Type de contenu incorrect (pas application/json) n’a pas d’importance body 415

Priorité de liaison

Règles permettant de déterminer une source de liaison à partir d’un paramètre :

  1. Attribut explicite défini sur le paramètre (attributs From*) dans l’ordre suivant :
    1. Valeurs d’itinéraire : [FromRoute]
    2. Chaîne de requête : [FromQuery]
    3. En-tête : [FromHeader]
    4. Corps : [FromBody]
    5. Service : [FromServices]
  2. Types spéciaux
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
  3. Le type de paramètre a une méthode BindAsync valide.
  4. Le type de paramètre est une chaîne ou a une méthode TryParse valide.
    1. Si le nom du paramètre existe dans le modèle d’itinéraire, par exemple app.Map("/todo/{id}", (int id) => {});, il est lié à partir de l’itinéraire.
    2. Lié à partir de la chaîne de requête.
  5. Si le type de paramètre est un service fourni par l’injection de dépendances, il utilise ce service comme source.
  6. Le paramètre provient du corps.

Personnaliser la liaison JSON

La source de liaison de corps utilise System.Text.Json pour la désérialisation. Il n’est pas possible de modifier cette valeur par défaut, mais la liaison peut être personnalisée à l’aide des autres techniques décrites précédemment. Pour personnaliser les options de sérialiseur JSON, utilisez un code similaire à ce qui suit :

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/products", (Product product) => product);

app.Run();

class Product
{
    // These are public fields, not properties.
    public int Id;
    public string? Name;
}

Le code précédent :

  • Configure les options JSON par défaut d’entrée et de sortie.
  • Retourne la valeur JSON suivante
    {
      "id": 1,
      "name": "Joe Smith"
    }
    
    Lors de la publication
    {
      "Id": 1,
      "Name": "Joe Smith"
    }
    

Lire le corps de la requête

Lisez le corps de la requête directement à l’aide d’un paramètre HttpContext ou HttpRequest :

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

Le code précédent :

  • Accède au corps de la requête à l’aide de HttpRequest.BodyReader.
  • Copie le corps de la requête dans un fichier local.

Réponses

Les gestionnaires de routes prennent en charge les types de valeurs de retour suivants :

  1. Basé sur IResult : cela inclut Task<IResult> et ValueTask<IResult>
  2. string - Cela comprend Task<string> et ValueTask<string>
  3. T (Tout autre type) : cela inclut Task<T> et ValueTask<T>
Valeur de retour Comportement Type de contenu
IResult Le framework appelle IResult.ExecuteAsync Décidé par l’implémentation IResult
string Le framework écrit la chaîne directement dans la réponse text/plain
T (Tout autre type) Le framework sérialise la réponse en JSON application/json

Exemples de valeurs de retour

Valeurs de retour string

app.MapGet("/hello", () => "Hello World");

Valeurs de retour JSON

app.MapGet("/hello", () => new { Message = "Hello World" });

Valeurs de retour IResult

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

L’exemple suivant utilise les types de résultats intégrés pour personnaliser la réponse :

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Code d’état personnalisé
app.MapGet("/405", () => Results.StatusCode(405));
Texte
app.MapGet("/text", () => Results.Text("This is some text"));
Flux
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
Redirection
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Fichier
app.MapGet("/download", () => Results.File("myfile.text"));

Résultats intégrés

Des fonctions d’assistance de résultats courants existent dans la classe statique Microsoft.AspNetCore.Http.Results.

Description Type de réponse Code d’état API
Écrire une réponse JSON avec des options avancées application/json 200 Results.Json
Écrire une réponse JSON application/json 200 Results.Ok
Écrire une réponse texte text/plain (par défaut), configurable 200 Results.Text
Écrire la réponse sous forme d’octets application/octet-stream (par défaut), configurable 200 Results.Bytes
Écrire un flux d’octets dans la réponse application/octet-stream (par défaut), configurable 200 Results.Stream
Diffuser un fichier dans la réponse à télécharger avec l’en-tête content-disposition application/octet-stream (par défaut), configurable 200 Results.File
Définir le code d’état sur 404, avec une réponse JSON facultative N/A 404 Results.NotFound
Définir le code d’état sur 204 N/A 204 Results.NoContent
Définir le code d’état sur 422, avec une réponse JSON facultative N/A 422 Results.UnprocessableEntity
Définir le code d’état sur 400, avec une réponse JSON facultative N/A 400 Results.BadRequest
Définir le code d’état sur 409, avec une réponse JSON facultative N/A 409 Results.Conflict
Écrire un objet JSON des détails du problème dans la réponse N/A 500 (par défaut), configurable Results.Problem
Écrire un objet JSON des détails du problème dans la réponse avec des erreurs de validation N/A N/A, configurable Results.ValidationProblem

Personnalisation des résultats

Les applications peuvent contrôler les réponses en implémentant un type IResult personnalisé. Le code suivant est un exemple de type de résultat HTML :

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Nous vous recommandons d’ajouter une méthode d’extension à Microsoft.AspNetCore.Http.IResultExtensions pour rendre ces résultats personnalisés plus détectables.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Autorisation

Les itinéraires peuvent être protégés à l’aide de stratégies d’autorisation. Celles-ci peuvent être déclarées via l’attribut [Authorize] ou à l’aide de la méthode RequireAuthorization :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Le code précédent peut être écrit avec RequireAuthorization :

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

L’exemple suivant utilise l’autorisation basée sur la stratégie :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Autoriser les utilisateurs non authentifiés à accéder à un point de terminaison

[AllowAnonymous] permet aux utilisateurs non authentifiés d’accéder aux points de terminaison :

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

Les itinéraires peuvent avoir CORS activé à l’aide de stratégies CORS. CORS peut être déclaré via l’attribut [EnableCors] ou à l’aide de la méthode RequireCors. Les exemples suivants activent CORS :

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

Pour plus d’informations, consultez Activation les requêtes cross-origin (CORS) dans ASP.NET Core

Voir aussi

Prise en charge d’OpenAPI dans les API minimales