Informations de référence rapides sur les API minimales
Remarque
Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 9 de cet article.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la stratégie de support .NET et .NET Core. Pour la version actuelle, consultez la version .NET 9 de cet article.
Important
Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.
Pour la version actuelle, consultez la version .NET 9 de cet article.
Ce document :
- Fournit une référence rapide pour les API minimales.
- Est destiné aux développeurs expérimentés. Pour une introduction, consultez Tutoriel : Créer une API minimale avec ASP.NET Core.
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 lorsqueHostingEnvironment
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 exempleapp.MapGet
.UseEndpoints
est ajouté à la fin du pipeline d’intergiciel si des points de terminaison sont configurés.UseAuthentication
est ajouté immédiatement aprèsUseRouting
, si le code utilisateur n’a pas déjà appeléUseAuthentication
et siIAuthenticationSchemeProvider
peut être détecté dans le fournisseur de services.IAuthenticationSchemeProvider
est ajouté par défaut lors de l’utilisation deAddAuthentication
, et les services sont détectés à l’aide deIServiceProviderIsService
.UseAuthorization
est ajouté après, si le code utilisateur n’a pas déjà appeléUseAuthorization
et siIAuthorizationHandlerProvider
peut être détecté dans le fournisseur de services.IAuthorizationHandlerProvider
est ajouté par défaut lors de l’utilisation deAddAuthorization
, et les services sont détectés à l’aide deIServiceProviderIsService
.- Les intergiciels et les points de terminaison configurés par l’utilisateur sont ajoutés entre
UseRouting
etUseEndpoints
.
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
etUseEndpoints
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
etappSettings.{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.
Points de terminaison nommés et génération de liens
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 route 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 QueryPrivateTodos
fabrique de filtre de point de terminaison est une fonction locale qui modifie les paramètres TodoDb
du gestionnaire d’itinéraires pour permettre l’accès et le stockage de données todo privées.
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
- En-tête
- 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 queHttpRequest.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
etPipeReader
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 attaques 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
ouProject
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 antiforgery 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 estfalse
. Si la caseisCompleted
est cochée lorsque le formulaire est envoyé, les deux valeurstrue
etfalse
sont envoyées en tant que valeurs. Si la case à cocher est décochée, seule la valeur d’entrée masquéefalse
est envoyée. Le processus de liaison de modèle ASP.NET Core lit uniquement la première valeur lors de la liaison à une valeurbool
, ce qui donnetrue
pour les cases à cocher cochées etfalse
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 :
- 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. - 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 :
- Attribut explicite défini sur le paramètre (attributs From*) dans l’ordre suivant :
- Valeurs d’itinéraire :
[FromRoute]
- Chaîne de requête :
[FromQuery]
- En-tête :
[FromHeader]
- Corps :
[FromBody]
- Formulaire :
[FromForm]
- Service :
[FromServices]
- Valeurs du paramètre :
[AsParameters]
- Valeurs d’itinéraire :
- Types spéciaux
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormCollection
(HttpContext.Request.Form
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- Le type de paramètre a une méthode
BindAsync
statique valide. - Le type de paramètre est une chaîne ou a une méthode
TryParse
statique valide.- 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. - Lié à partir de la chaîne de requête.
- Si le nom du paramètre existe dans le modèle d’itinéraire, par exemple,
- Si le type de paramètre est un service fourni par l’injection de dépendances, il utilise ce service comme source.
- 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 :
- Basé sur
IResult
: cela inclutTask<IResult>
etValueTask<IResult>
string
- Cela comprendTask<string>
etValueTask<string>
T
(Tout autre type) : cela inclutTask<T>
etValueTask<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
Pour plus d’informations, consultez Filtres dans les applications 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
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
- Informations de référence rapides sur les API minimales
- Générer des documents OpenAPI
- Créer des réponses dans les applications API minimales
- Filtres dans les applications d’API minimales
- Gérer des erreurs d’API minimales
- Authentification et autorisation dans les API minimales
- Tester les applications API minimales
- Acheminement en court-circuit
- Identity Points de terminaison de l'API
- Prise en charge du conteneur d’injection de dépendances de service à clé
- Un aperçu des coulisses des points de terminaison minimaux de l'API
- Organisation des API ASP.NET Core Minimal
- Discussion sur la validation Fluent sur GitHub
Ce document :
- Fournit une référence rapide pour les API minimales.
- Est destiné aux développeurs expérimentés. Pour une introduction, consultez Tutoriel : Créer une API minimale avec ASP.NET Core
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 lorsqueHostingEnvironment
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 exempleapp.MapGet
.UseEndpoints
est ajouté à la fin du pipeline d’intergiciel si des points de terminaison sont configurés.UseAuthentication
est ajouté immédiatement aprèsUseRouting
, si le code utilisateur n’a pas déjà appeléUseAuthentication
et siIAuthenticationSchemeProvider
peut être détecté dans le fournisseur de services.IAuthenticationSchemeProvider
est ajouté par défaut lors de l’utilisation deAddAuthentication
, et les services sont détectés à l’aide deIServiceProviderIsService
.UseAuthorization
est ajouté après, si le code utilisateur n’a pas déjà appeléUseAuthorization
et siIAuthorizationHandlerProvider
peut être détecté dans le fournisseur de services.IAuthorizationHandlerProvider
est ajouté par défaut lors de l’utilisation deAddAuthorization
, et les services sont détectés à l’aide deIServiceProviderIsService
.- Les intergiciels et les points de terminaison configurés par l’utilisateur sont ajoutés entre
UseRouting
etUseEndpoints
.
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
etUseEndpoints
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
etappSettings.{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.
Points de terminaison nommés et génération de liens
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 route 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 QueryPrivateTodos
fabrique de filtre de point de terminaison est une fonction locale qui modifie les paramètres TodoDb
du gestionnaire d’itinéraires pour permettre l’accès et le stockage de données todo privées.
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
- En-tête
- 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 queHttpRequest.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
etPipeReader
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 :
- 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. - 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 :
- Attribut explicite défini sur le paramètre (attributs From*) dans l’ordre suivant :
- Valeurs d’itinéraire :
[FromRoute]
- Chaîne de requête :
[FromQuery]
- En-tête :
[FromHeader]
- Corps :
[FromBody]
- Service :
[FromServices]
- Valeurs du paramètre :
[AsParameters]
- Valeurs d’itinéraire :
- Types spéciaux
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- Le type de paramètre a une méthode
BindAsync
statique valide. - Le type de paramètre est une chaîne ou a une méthode
TryParse
statique valide.- Si le nom du paramètre existe dans le modèle de route. Dans
app.Map("/todo/{id}", (int id) => {});
,id
est lié à partir de la route. - Lié à partir de la chaîne de requête.
- Si le nom du paramètre existe dans le modèle de route. Dans
- Si le type de paramètre est un service fourni par l’injection de dépendances, il utilise ce service comme source.
- 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 :
- Basé sur
IResult
: cela inclutTask<IResult>
etValueTask<IResult>
string
- Cela comprendTask<string>
etValueTask<string>
T
(Tout autre type) : cela inclutTask<T>
etValueTask<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 :
- Fournit une référence rapide pour les API minimales.
- Est destiné aux développeurs expérimentés. Pour une introduction, consultez Tutoriel : Créer une API minimale avec ASP.NET Core
Les API minimales sont les suivantes :
- WebApplication et WebApplicationBuilder
- Gestionnaires de routes
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
etappSettings.{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";
}
}
Points de terminaison nommés et génération de liens
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
- En-tête
- 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 :
- 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. - 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 :
- Attribut explicite défini sur le paramètre (attributs From*) dans l’ordre suivant :
- Valeurs d’itinéraire :
[FromRoute]
- Chaîne de requête :
[FromQuery]
- En-tête :
[FromHeader]
- Corps :
[FromBody]
- Service :
[FromServices]
- Valeurs d’itinéraire :
- Types spéciaux
- Le type de paramètre a une méthode
BindAsync
valide. - Le type de paramètre est une chaîne ou a une méthode
TryParse
valide.- Si le nom du paramètre existe dans le modèle de route. Dans
app.Map("/todo/{id}", (int id) => {});
,id
est lié à partir de la route. - Lié à partir de la chaîne de requête.
- Si le nom du paramètre existe dans le modèle de route. Dans
- Si le type de paramètre est un service fourni par l’injection de dépendances, il utilise ce service comme source.
- 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 le code JSON suivant :
Lors de la publication{ "id": 1, "name": "Joe Smith" }
{ "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 :
- Basé sur
IResult
: cela inclutTask<IResult>
etValueTask<IResult>
string
- Cela comprendTask<string>
etValueTask<string>
T
(Tout autre type) : cela inclutTask<T>
etValueTask<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