Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.
UseRouting wird zweitens hinzugefügt, wenn der Benutzercode UseRouting noch nicht aufgerufen hat, und wenn Endpunkte konfiguriert sind, z. B. app.MapGet.
UseEndpoints wird am Ende der Middlewarepipeline hinzugefügt, wenn Endpunkte konfiguriert sind.
UseAuthentication wird unmittelbar nach UseRouting hinzugefügt, wenn der Benutzercode UseAuthentication noch nicht aufgerufen hat und wenn IAuthenticationSchemeProvider im Dienstanbieter erkannt werden kann. IAuthenticationSchemeProvider wird standardmäßig hinzugefügt, wenn die Verwendung AddAuthentication von Diensten mit IServiceProviderIsService erkannt wird.
UseAuthorization wird als Nächstes hinzugefügt, wenn der Benutzercode UseAuthorization noch nicht aufgerufen hat und wenn IAuthorizationHandlerProvider im Dienstanbieter erkannt werden kann. IAuthorizationHandlerProvider wird standardmäßig hinzugefügt, wenn AddAuthorization verwendet wird, und Dienste mit IServiceProviderIsService erkannt werden.
Benutzerkonfigurierte Middleware und Endpunkte werden zwischen UseRouting und UseEndpoints hinzugefügt.
Nachfolgend sehen Sie den Code, der von der automatischen Middleware erzeugt wird, die zur App hinzugefügt wird:
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 => {});
In einigen Fällen eignet sich die standardmäßige Middleware-Konfiguration nicht für die App und muss geändert werden. Beispielsweise sollte UseCors vor UseAuthentication und UseAuthorization aufgerufen werden. Die App muss UseAuthentication und UseAuthorization aufrufen, wenn UseCors aufgerufen wird:
Wenn Middleware ausgeführt werden muss, bevor der Routenabgleich erfolgt, muss UseRouting aufgerufen werden, und die Middleware muss vor dem Aufruf von UseRouting platziert werden. UseEndpoints ist in diesem Fall nicht erforderlich, da es wie zuvor beschrieben automatisch hinzugefügt wird:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
Beim Hinzufügen einer Terminal-Middleware:
Die Middleware muss nach UseEndpoints hinzugefügt werden.
Die App muss UseRouting und UseEndpoints aufrufen, damit die Terminal-Middleware an der richtigen Position platziert werden kann.
Terminal-Middleware ist Middleware, die ausgeführt wird, wenn kein Endpunkt die Anforderung verarbeitet.
Arbeiten mit Ports
Beim Erstellen einer Web-App mit Visual Studio oder dotnet new wird eine Datei Properties/launchSettings.json erstellt, die die Ports angibt, an denen die Anwendung antwortet. In den folgenden Beispielen für Porteinstellungen wird beim Ausführen der App in Visual Studio ein Fehlerdialogfeld Unable to connect to web server 'AppName' angezeigt. Visual Studio gibt einen Fehler zurück, da der in Properties/launchSettings.jsonangegebene Port erwartet wird, die App jedoch den von app.Run("http://localhost:3000") angegebenen Port verwendet. Führen Sie die folgenden Beispiele für Portänderungen über die Befehlszeile aus.
In den folgenden Abschnitten wird der Port festgelegt, auf den die App reagiert.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
Im vorangehenden Code antwortet die App auf Port 3000.
Mehrere Ports
Im folgenden Code antwortet die App auf Port 3000 und 4000.
Mit dem folgenden Befehl antwortet die App auf Port 7777:
dotnet run --urls="https://localhost:7777"
Wenn der Endpunkt Kestrel ebenfalls in der Datei appsettings.json konfiguriert ist, wird die in der Datei appsettings.json angegebene URL verwendet. Weitere Informationen finden Sie unter Kestrel-Endpunktkonfiguration.
Lesen des Ports aus der Umgebung
Der folgende Code liest den Port aus der Umgebung:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
Die bevorzugte Methode zur Festlegung des Ports über die Umgebung ist die Verwendung der Umgebungsvariablen ASPNETCORE_URLS, die im folgenden Abschnitt beschrieben wird.
Festlegen der Ports über die ASPNETCORE_URLS-Umgebungsvariable
Für die Festlegung des Ports steht die Umgebungsvariable ASPNETCORE_URLS zur Verfügung:
Zugreifen auf den Container für Abhängigkeitsinjektion
Der folgende Code zeigt, wie Dienste während des Anwendungsstarts aus dem Abhängigkeitsinjektionscontainer abzurufen sind:
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();
Der folgende Code zeigt, wie Sie mit dem [FromKeyedServices]-Attribut auf Schlüssel aus dem DI-Container (Dependency Injection, Abhängigkeitsinjektion) zugreifen können:
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.";
}
Ändern von Inhaltsstamm, App-Name und Umgebung über Umgebungsvariablen oder Befehlszeile
Die folgende Tabelle zeigt die Umgebungsvariablen und Befehlszeilenargumente, die zum Ändern von Inhaltsstamm, Anwendungsname und Umgebung verwendet werden:
Feature
Umgebungsvariable
Befehlszeilenargument
Anwendungsname
ASPNETCORE_APPLICATIONNAME
--applicationName
Umgebungsname
ASPNETCORE_ENVIRONMENT
--environment
Inhaltsstammverzeichnis
ASPNETCORE_CONTENTROOT
--contentRoot
Hinzufügen von Konfigurationsanbietern
Im folgenden Beispiel wird der INI-Konfigurationsanbieter hinzugefügt:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Der folgende Code liest HelloKey aus der Konfiguration und zeigt den Wert am Endpunkt / an. Wenn der Konfigurationswert NULL ist, wird „Hello“ message zugewiesen:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Lesen der Umgebung
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Hinzufügen von Protokollierungsanbietern
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();
Hinzufügen von Diensten
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();
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();
Ändern des Webstamms
Standardmäßig ist der Webstamm relativ zum Inhaltsstamm im Ordner wwwroot angegeben. Im Webstamm sucht die Middleware für statische Dateien nach statischen Dateien. Der Webstamm kann mit WebHostOptions, der Befehlszeile oder mit der Methode UseWebRoot geändert werden:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Container für benutzerdefinierte Abhängigkeitsinjektion
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();
Hinzufügen von Middleware
Für die WebApplication kann eine beliebige vorhandene ASP.NET Core-Middleware konfiguriert werden:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.CreateBuilder initialisiert eine neue Instanz der WebApplicationBuilder-Klasse mit vorkonfigurierten Standardwerten. Die Seite mit Ausnahmen für Entwickler ist in den vorkonfigurierten Standardwerten aktiviert. Durch Ausführung des folgende Codes in der Entwicklungsumgebung wird beim Navigieren zu / eine benutzerfreundliche Seite geöffnet, auf der die Ausnahme angezeigt wird.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ASP.NET Core-Middleware
In der folgenden Tabelle werden einige der Middlewarekomponenten aufgeführt, die häufig mit Minimal-APIs verwendet wird.
In den folgenden Abschnitten werden Routing, Parameterbindung und Antworten behandelt.
Routing
Eine konfigurierte WebApplication unterstützt Map{Verb} und MapMethods, wobei {Verb} eine HTTP-Methode mit Camel-Case-Schreibweise wie Get, Post, Put oder Delete ist:
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();
Die an diese Methoden übergebenen Delegate-Argumente werden als „Routenhandler“ bezeichnet.
Routenhandler
Routenhandler sind Methoden, die ausgeführt werden, wenn die Route übereinstimmt. Als Routenhandler kann ein Lambdaausdruck, eine lokale Funktion, eine Instanzmethode oder eine statische Methode verwendet werden. Routenhandler können synchron oder asynchron sein.
Lambdaausdruck
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();
Lokale Funktion
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
für eine Instanzmethode
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";
}
}
für eine statische Methode
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";
}
}
Endpunkt außerhalb von Program.cs definiert
Minimale APIs müssen sich nicht in Program.cs befinden.
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" });
});
}
}
Weitere Informationen dazu finden Sie auch unter Routengruppen weiter unten in diesem Artikel.
Benannte Endpunkte und Linkgenerierung
Endpunkte können Namen erhalten, um URLs für den jeweiligen Endpunkt zu generieren. Durch die Verwendung eines benannten Endpunkts entfällt das Hartcodieren von Pfaden in einer App:
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();
Der vorangehende Code zeigt The link to the hello route is /hello vom Endpunkt / an.
HINWEIS: Für Endpunktnamen muss Groß-/Kleinschreibung beachtet werden.
Endpunktnamen:
Dieser muss global eindeutig sein.
Werden als OpenAPI-Vorgangs-ID verwendet, wenn die OpenAPI-Unterstützung aktiviert ist. Weitere Informationen finden Sie unter OpenAPI.
Routenparameter
Routenparameter können als Teil der Routenmusterdefinition erfasst werden:
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();
Der vorstehende Code gibt The user id is 3 and book id is 7 aus dem URI /users/3/books/7 zurück.
Der Routenhandler kann die zu erfassende Parameter deklarieren. Bei einer Anforderung an eine Route mit Parametern, deren Erfassung deklariert wurde, werden die Parameter geparst und an den Handler übergeben. Dadurch können die Werte problemlos typsicher erfasst werden. Im vorangegangenen Code sind userId und bookId beide vom Typ int.
Wenn im vorstehenden Code einer der beiden Routenwerte nicht in den Typ int umgewandelt werden kann, wird eine Ausnahme ausgelöst. Die GET-Anforderung /users/hello/books/3 löst die folgende Ausnahme aus:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Platzhalterzeichen und Abfangen aller Routen
Der folgende Code zum Abfangen aller Routen gibt Routing to hello vom Endpunkt „/posts/hello“ zurück:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Routeneinschränkungen
Routeneinschränkungen schränken das Abgleichsverhalten einer Route ein.
Die MapGroup-Erweiterungsmethode hilft, Gruppen von Endpunkten mit einem gemeinsamen Präfix zu organisieren. Sie reduziert sich wiederholenden Code und ermöglicht die benutzerdefinierte Anpassung ganzer Gruppen von Endpunkten mit einem einzigen Aufruf von Methoden wie RequireAuthorization und WithMetadata,die Endpunktmetadaten hinzufügen.
Der folgende Code erstellt beispielsweise zwei ähnliche Endpunktgruppen:
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;
}
};
}
In diesem Szenario können Sie eine relative Adresse für den Location-Header im 201 Created-Ergebnis verwenden:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
Die erste Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /public/todos und ist ohne Authentifizierung zugänglich. Die zweite Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /private/todos und erfordert Authentifizierung.
Die QueryPrivateTodos-Endpunktfilterfactory ist eine lokale Funktion, die die TodoDb-Parameter des Routenhandlers so ändert, dass Zugriff auf private Aufgabendaten zulässig ist und diese gespeichert werden können.
Routengruppen unterstützen auch geschachtelte Gruppen und komplexe Präfixmuster mit Routenparametern und -einschränkungen. Im folgenden Beispiel kann der der user-Gruppe zugeordnete Routenhandler die Routenparameter {org} und {group} erfassen, die in den Präfixen der äußeren Gruppe definiert sind.
Das Präfix kann auch leer sein. Dies kann hilfreich sein, um Endpunktmetadaten oder Filter einer Gruppe von Endpunkten hinzuzufügen, ohne das Routenmuster zu ändern.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
Das Hinzufügen von Filtern oder Metadaten zu einer Gruppe verhält sich genauso wie das individuelle Hinzufügen zu jedem Endpunkt, bevor zusätzliche Filter oder Metadaten hinzugefügt werden, die einer inneren Gruppe oder einem bestimmten Endpunkt hinzugefügt wurden.
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);
});
Im Beispiel oben protokolliert der äußere Filter die eingehende Anforderung vor dem inneren Filter, obwohl er als zweiter hinzugefügt wurde. Da die Filter auf verschiedene Gruppen angewendet wurden, spielt die Reihenfolge, in der sie relativ zueinander hinzugefügt wurden, keine Rolle. Die Reihenfolge, in der Filter hinzugefügt werden, spielt eine Rolle, wenn sie auf dieselbe Gruppe oder einen bestimmten Endpunkt angewendet werden.
Eine Anforderung an /outer/inner/ protokolliert Folgendes:
/outer group filter
/inner group filter
MapGet filter
Parameterbindung
Die Parameterbindung ist der Prozess der Umwandlung von Anforderungsdaten in stark typisierte Parameter, die durch Routenhandler ausgedrückt werden. Eine Bindungsquelle bestimmt, von wo aus Parameter gebunden werden. Bindungsquellen können basierend auf der HTTP-Methode und dem Parametertyp explizit sein oder abgeleitet werden.
Unterstützte Bindungsquellen:
Routenwerte
Abfragezeichenfolge
Header
Text (als JSON)
Formularwerte
Von der Abhängigkeitsinjektion bereitgestellte Dienste
Benutzerdefiniert
Im folgenden Beispiel verwendet der GET-Routenhandler einige dieser Parameterbindungsquellen:
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 { }
Die folgende Tabelle zeigt die Beziehung zwischen den im vorherigen Beispiel verwendeten Parametern und den zugeordneten Bindungsquellen.
Parameter
Bindungsquelle
id
Routenwert
page
Abfragezeichenfolge
customHeader
header
service
Von der Abhängigkeitsinjektion bereitgestellt
Bei den HTTP-Methoden GET, HEAD, OPTIONS und DELETE erfolgt keine implizite Bindung aus dem Text. Um eine Bindung vom Textkörper (als JSON) für diese HTTP-Methoden zu verwenden, führen Sie explizit eine Bindung mit [FromBody] oder einen Lesevorgang aus HttpRequest durch.
Im folgenden Beispiel verwendet der POST-Routenhandler eine Bindungsquelle des Textkörpers (als JSON) für den Parameter person:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Die Parameter in den vorherigen Beispielen werden alle automatisch über Anforderungsdaten gebunden. Um die Benutzerfreundlichkeit der Parameterbindung zu veranschaulichen, zeigen die folgenden Routenhandler, wie Anforderungsdaten direkt aus der Anforderung gelesen werden:
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>();
// ...
});
Explizite Parameterbindung
Attribute können verwendet werden, um explizit zu deklarieren, von wo Parameter gebunden werden.
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);
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.
Eine Alternative besteht darin, das [AsParameters]-Attribut mit einem benutzerdefinierten Typ zu verwenden, der über Eigenschaften verfügt, die mit [FromForm] versehen sind. Der folgende Code bindet z. B. von Formularwerten an Eigenschaften der NewTodoRequest-Datensatzstruktur:
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);
Weitere Informationen finden Sie im Abschnitt zu AsParameters weiter unten in diesem Artikel.
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();
Parameter, die an die Anforderung mit [FromForm] gebunden sind, enthalten ein Antifälschungstoken. Das Antifälschungstoken wird überprüft, wenn die Anforderung verarbeitet wird. Weitere Informationen finden Sie unter Schutz vor Fälschung mit Minimal APIs.
Parameterbindung für minimale APIs bindet Parameter durch Abhängigkeitsinjektion (Dependency Injection, DI), wenn der Typ als Dienst konfiguriert wird. Es ist nicht erforderlich, das [FromServices]-Attribut explizit auf einen Parameter anzuwenden. Im folgenden Code geben beide Aktionen die Uhrzeit zurück:
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();
Optionale Parameter
In Routenhandlern deklarierte Parameter werden als erforderlich behandelt:
Wenn eine Anforderung der Route entspricht, wird der Routenhandler nur ausgeführt, wenn alle erforderlichen Parameter in der Anforderung angegeben sind.
Sind nicht alle erforderlichen Parameter enthalten, kommt es zu einem Fehler.
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 zurückgegeben
/products
BadHttpRequestException: Der erforderliche Parameter „int pageNumber“ wurde nicht in der Abfragezeichenfolge bereitgestellt.
/products/1
HTTP-Fehler vom Typ 404, keine übereinstimmende Route
Um pageNumber als optional festzulegen, definieren Sie den Typ als optional, oder geben Sie einen Standardwert an:
Binden des Anforderungstexts als Stream oder PipeReader
Der Anforderungstext kann als Stream oder PipeReader gebunden werden, um effizient Szenarien zu unterstützen, in denen der Benutzer Daten verarbeiten und Folgendes tun muss:
Daten im Blobspeicher speichern oder Daten in die Warteschlange eines Warteschlangenanbieters einreihen.
Die gespeicherten Daten mit einem Workerprozess oder einer Cloudfunktion verarbeiten.
Der folgende Code implementiert eine Hintergrundwarteschlange:
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;
}
Der folgende Code bindet den Anforderungstext an einen 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);
});
Der folgende Code veranschaulicht die vollständige Program.cs-Datei:
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();
Beim Lesen von Daten ist der Stream dasselbe Objekt wie HttpRequest.Body.
Der Anforderungstext wird standardmäßig nicht gepuffert. Nachdem der Textkörper gelesen wurde, ist eine Rückkehr zum Anfang nicht möglich. Der Datenstrom kann nicht mehrmals gelesen werden.
Stream und PipeReader sind außerhalb des minimalen Aktionshandlers nicht verwendbar, da die zugrunde liegenden Puffer verworfen oder wiederverwendet werden.
Dateiuploads mit IFormFile und IFormFileCollection
Der folgende Code lädt Dateien mithilfe der abgeleiteten Bindung vom IFormFile-Typ hoch:
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();
Warnung: Bei der Implementierung von Formularen muss die App Cross-Site Request Forgery-Angriffe (XSRF/CSRF) verhindern. Im vorherigen Code wird der IAntiforgery-Dienst verwendet, um XSRF-Angriffe zu verhindern, indem ein Antifälschungstoken generiert und validiert wird:
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();
Ein minimaler Endpunkt, der eine mehrteilige Formulareingabe an ein komplexes Objekt bindet.
Verwenden der Antifälschungsdienste zur Unterstützung der Generierung und Validierung von Antifälschungstoken
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));
}
Im obigen Code:
Der Zielparameter muss mit dem Attribut [FromForm] versehen werden, um ihn von den Parametern zu unterscheiden, die aus dem JSON-Text gelesen werden sollen.
Die Bindung von komplexen Typen oder Collectiontypen wird für minimale APIs, die mit dem Anforderungsdelegat-Generator kompiliert werden, nicht unterstützt.
Das Markup enthält eine zusätzliche ausgeblendete Eingabe mit dem Namen isCompleted und dem Wert false. Wenn das Kontrollkästchen isCompleted beim Senden des Formulars aktiviert ist, werden die Werte true und false beide als Werte übermittelt. Ist das Kontrollkästchen nicht aktiviert, wird nur der Wert false der ausgeblendeten Eingabe übermittelt. Der ASP.NET Core-Modellbindungsprozess liest bei der Bindung an einen bool-Wert nur den ersten Wert. Dies führt bei aktivierten Kontrollkästchen zum Ergebnis true und bei nicht aktivierten Kontrollkästchen zum Ergebnis false.
Ein Beispiel für die an den vorherigen Endpunkt übermittelten Formulardaten sieht wie folgt aus:
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
Binden von Arrays und Zeichenfolgenwerten aus Headern und Abfragezeichenfolgen
Der folgende Code veranschaulicht die Bindung von Abfragezeichenfolgen an ein Array von primitiven Typen, Zeichenfolgenarrays und 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]}");
Die Bindung von Abfragezeichenfolgen oder Headerwerten an ein Array komplexer Typen wird unterstützt, wenn im Typ TryParse implementiert ist. Der folgende Code bindet an ein Zeichenfolgenarray und gibt alle Elemente mit den angegebenen Tags zurück:
Der folgende Code zeigt das Modell und die erforderliche TryParse-Implementierung:
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;
}
}
Der folgende Code richtet eine Bindung mit einem int-Array ein:
Der folgende Code bindet an den Headerschlüssel X-Todo-Id und gibt die Todo-Elemente mit übereinstimmenden Id-Werten zurück:
// 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();
});
Hinweis
Beim Binden eines string[]-Werts aus einer Abfragezeichenfolge führt das Fehlen einer übereinstimmenden Abfragezeichenfolge zu einem leeren Array anstelle eines NULL-Werts.
Parameterbindung für Argumentlisten mit [AsParameters]
AsParametersAttribute ermöglicht einfache Parameterbindung an Typen und nicht komplexe oder rekursive Modellbindung.
Betrachten Sie folgenden Code:
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.
Betrachten Sie den folgenden GET-Endpunkt:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
Mit der folgenden struct können die vorherigen hervorgehobenen Parameter ersetzt werden:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
Der umgestaltete GET-Endpunkt verwendet die vorherige struct mit dem AsParameters-Attribut:
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());
Der folgende Code zeigt zusätzliche Endpunkte in der App:
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();
});
Die folgenden Klassen werden verwendet, um die Parameterlisten umzugestalten:
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!;
}
Der folgende Code zeigt die umgestalteten Endpunkte mit AsParameters und der vorherigen struct und Klassen:
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();
});
Die folgenden record-Typen können verwendet werden, um die vorherigen Parameter zu ersetzen:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
Die Verwendung einer struct mit AsParameters kann effizienter sein als die Verwendung eines record-Typs.
Es gibt zwei Möglichkeiten zum Anpassen der Parameterbindung:
Binden Sie für Routen-, Abfrage- und Headerbindungsquellen benutzerdefinierte Typen, indem Sie eine statische TryParse-Methode für den Typ hinzufügen.
Steuern Sie den Bindungsprozess, indem Sie eine BindAsync-Methode für einen Typ implementieren.
TryParse
TryParse umfasst zwei APIs:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Der folgende Code zeigt Point: 12.3, 10.1 mit dem URI /map?Point=12.3,10.1 an:
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 umfasst die folgenden APIs:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Der nachstehende Code zeigt SortBy:xyz, SortDirection:Desc, CurrentPage:99 mit dem URI /products?SortBy=xyz&SortDir=Desc&Page=99 an:
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
}
Bindungsfehler
Wenn die Bindung fehlschlägt, protokolliert das Framework eine Debugmeldung und gibt abhängig vom Fehlermodus verschiedene Statuscodes an den Client zurück.
Fehlermodus
Parametertypen, die Nullwerte zulassen
Bindungsquelle
Statuscode
{ParameterType}.TryParse gibt false zurück.
ja
Route/Abfrage/Header
400
{ParameterType}.BindAsync gibt null zurück.
ja
custom
400
{ParameterType}.BindAsync wird ausgelöst
ist nicht wichtig.
custom
500
Fehler beim Deserialisieren des JSON-Texts
ist nicht wichtig.
Text
400
Falscher Inhaltstyp (nicht application/json)
ist nicht wichtig.
Text
415
Bindungsrangfolge
Die Regeln zum Bestimmen einer Bindungsquelle anhand eines Parameters:
Explizites Attribut, das für den Parameter (From*-Attribute) in der folgenden Reihenfolge definiert ist:
Der Parametertyp verfügt über eine gültige statische BindAsync-Methode.
Der Parametertyp lautet „string“ oder verfügt über eine gültige statische TryParse-Methode.
Wenn der Parametername in der Routenvorlage vorhanden ist (z. B. app.Map("/todo/{id}", (int id) => {});), wird er über die Route gebunden.
Bindung über die Abfragezeichenfolge.
Wenn der Parametertyp ein durch die Abhängigkeitsinjektion bereitgestellter Dienst ist, wird dieser Dienst als Quelle verwendet.
Der Parameter stammt aus dem Text.
Konfigurieren von JSON-Deserialisierungsoptionen für die Textbindung
Die Textbindungsquelle verwendet System.Text.Json für die Deserialisierung. Es ist nicht möglich, diese Standardeinstellung zu ändern, aber die JSON-Serialisierungs- und Deserialisierungsoptionen können konfiguriert werden.
Globales Konfigurieren von JSON-Deserialisierungsoptionen
Optionen, die global für eine App gelten, können durch Aufrufen von ConfigureHttpJsonOptions konfiguriert werden. Das folgende Beispiel enthält öffentliche Felder und Formate der JSON-Ausgabe.
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
// }
Da der Beispielcode sowohl die Serialisierung als auch die Deserialisierung konfiguriert, kann er NameField lesen und NameField in die JSON-Ausgabe einschließen.
Konfigurieren von JSON-Deserialisierungsoptionen für einen Endpunkt
ReadFromJsonAsync verfügt über Überladungen, die ein JsonSerializerOptions-Objekt akzeptieren. Das folgende Beispiel enthält öffentliche Felder und Formate der JSON-Ausgabe.
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
// }
Da der vorangehende Code die angepassten Optionen nur auf die Deserialisierung anwendet, schließt die JSON-Ausgabe NameField aus.
Lesen des Anforderungstexts
Lesen Sie den Anforderungstext direkt mithilfe des Parameters HttpContext oder 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();
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
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");
});
Die statischen Klassen Results und TypedResults enthalten allgemeine Ergebnishilfen. Die Rückgabe von TypedResults wird der Rückgabe von Results vorgezogen. Weitere Informationen finden Sie unter Vergleich von TypedResults und Results.
Anpassen von Ergebnissen
Anwendungen können Antworten steuern, indem sie einen benutzerdefinierten IResult-Typ implementieren. Der folgende Code ist ein Beispiel für einen HTML-Ergebnistyp:
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);
}
}
Es wird empfohlen, Microsoft.AspNetCore.Http.IResultExtensions eine Erweiterungsmethode hinzuzufügen, um diese benutzerdefinierten Ergebnisse leichter auffindbar zu machen.
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();
Eingegebene Ergebnisse
Die IResult-Schnittstelle kann von minimalen APIs zurückgegebene Werte darstellen, welche nicht die implizite Unterstützung für die JSON-Serialisierung des zurückgegebenen Objekts in die HTTP-Antwort verwenden. Die statische Results-Klasse wird verwendet, um unterschiedliche IResult-Objekte zu erstellen, die verschiedene Arten von Antworten darstellen. Beispiel: Festlegen des Antwortstatuscodes oder Umleitung an eine andere URL.
Die IResult implementierenden Typen sind öffentlich, sodass Typassertionen beim Testen zugelassen werden. Beispiel:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
Sie können die Rückgabetypen der entsprechenden Methoden in der statischen TypedResults-Klasse anzeigen, um den richtigen öffentlichen IResult-Typ zu finden, in den Werte umgewandelt erden sollen.
Routen können mithilfe von Autorisierungsrichtlinien geschützt werden. Diese können mit dem Attribut [Authorize] oder der Methode RequireAuthorization angegeben werden.
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();
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();
Nicht authentifizierten Benutzern den Zugriff auf einen Endpunkt gestatten
Durch [AllowAnonymous] können nicht authentifizierte Benutzer auf Endpunkte zugreifen:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Routen können mithilfe von CORS-Richtlinien für CORS aktiviert werden. CORS kann über das Attribut [EnableCors] oder mit der Methode RequireCors deklariert werden. In den folgenden Beispielen wird CORS aktiviert:
Wenn ValidateOnBuildtrue ist, überprüft der DI-Container die Dienstkonfiguration zur Buildzeit. Wenn die Dienstkonfiguration ungültig ist, schlägt der Build beim Starten der App und nicht zur Laufzeit fehl, wenn der Dienst angefordert wird.
Wenn ValidateScopestrue ist, überprüft der DI-Container, dass ein bereichsbezogener Dienst nicht aus dem Stammbereich aufgelöst wird. Das Auflösen eines bereichsbezogenen Diensts aus dem Stammbereich kann zu einem Arbeitsspeicherverlust führen, da der Dienst länger als der Anforderungsbereich im Arbeitsspeicher aufbewahrt wird.
ValidateScopes und ValidateOnBuild sind aus Leistungsgründen standardmäßig in Nichtentwicklungsmodi falsch.
Der folgende Code zeigt, dass ValidateScopes im Entwicklungsmodus standardmäßig aktiviert, aber im Releasemodus deaktiviert ist:
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 { }
Der folgende Code zeigt, dass ValidateOnBuild im Entwicklungsmodus standardmäßig aktiviert, aber im Releasemodus deaktiviert ist:
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 { }
Der folgende Code deaktiviert ValidateScopes und ValidateOnBuild in 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;
});
}
UseRouting wird zweitens hinzugefügt, wenn der Benutzercode UseRouting noch nicht aufgerufen hat, und wenn Endpunkte konfiguriert sind, z. B. app.MapGet.
UseEndpoints wird am Ende der Middlewarepipeline hinzugefügt, wenn Endpunkte konfiguriert sind.
UseAuthentication wird unmittelbar nach UseRouting hinzugefügt, wenn der Benutzercode UseAuthentication noch nicht aufgerufen hat und wenn IAuthenticationSchemeProvider im Dienstanbieter erkannt werden kann. IAuthenticationSchemeProvider wird standardmäßig hinzugefügt, wenn die Verwendung AddAuthentication von Diensten mit IServiceProviderIsService erkannt wird.
UseAuthorization wird als Nächstes hinzugefügt, wenn der Benutzercode UseAuthorization noch nicht aufgerufen hat und wenn IAuthorizationHandlerProvider im Dienstanbieter erkannt werden kann. IAuthorizationHandlerProvider wird standardmäßig hinzugefügt, wenn AddAuthorization verwendet wird, und Dienste mit IServiceProviderIsService erkannt werden.
Benutzerkonfigurierte Middleware und Endpunkte werden zwischen UseRouting und UseEndpoints hinzugefügt.
Nachfolgend sehen Sie den Code, der von der automatischen Middleware erzeugt wird, die zur App hinzugefügt wird:
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 => {});
In einigen Fällen eignet sich die standardmäßige Middleware-Konfiguration nicht für die App und muss geändert werden. Beispielsweise sollte UseCors vor UseAuthentication und UseAuthorization aufgerufen werden. Die App muss UseAuthentication und UseAuthorization aufrufen, wenn UseCors aufgerufen wird:
Wenn Middleware ausgeführt werden muss, bevor der Routenabgleich erfolgt, muss UseRouting aufgerufen werden, und die Middleware muss vor dem Aufruf von UseRouting platziert werden. UseEndpoints ist in diesem Fall nicht erforderlich, da es wie zuvor beschrieben automatisch hinzugefügt wird:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
Beim Hinzufügen einer Terminal-Middleware:
Die Middleware muss nach UseEndpoints hinzugefügt werden.
Die App muss UseRouting und UseEndpoints aufrufen, damit die Terminal-Middleware an der richtigen Position platziert werden kann.
Terminal-Middleware ist Middleware, die ausgeführt wird, wenn kein Endpunkt die Anforderung verarbeitet.
Arbeiten mit Ports
Beim Erstellen einer Web-App mit Visual Studio oder dotnet new wird eine Datei Properties/launchSettings.json erstellt, die die Ports angibt, an denen die Anwendung antwortet. In den folgenden Beispielen für Porteinstellungen wird beim Ausführen der App in Visual Studio ein Fehlerdialogfeld Unable to connect to web server 'AppName' angezeigt. Visual Studio gibt einen Fehler zurück, da der in Properties/launchSettings.jsonangegebene Port erwartet wird, die App jedoch den von app.Run("http://localhost:3000") angegebenen Port verwendet. Führen Sie die folgenden Beispiele für Portänderungen über die Befehlszeile aus.
In den folgenden Abschnitten wird der Port festgelegt, auf den die App reagiert.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
Im vorangehenden Code antwortet die App auf Port 3000.
Mehrere Ports
Im folgenden Code antwortet die App auf Port 3000 und 4000.
Mit dem folgenden Befehl antwortet die App auf Port 7777:
dotnet run --urls="https://localhost:7777"
Wenn der Endpunkt Kestrel ebenfalls in der Datei appsettings.json konfiguriert ist, wird die in der Datei appsettings.json angegebene URL verwendet. Weitere Informationen finden Sie unter Kestrel-Endpunktkonfiguration.
Lesen des Ports aus der Umgebung
Der folgende Code liest den Port aus der Umgebung:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
Die bevorzugte Methode zur Festlegung des Ports über die Umgebung ist die Verwendung der Umgebungsvariablen ASPNETCORE_URLS, die im folgenden Abschnitt beschrieben wird.
Festlegen der Ports über die ASPNETCORE_URLS-Umgebungsvariable
Für die Festlegung des Ports steht die Umgebungsvariable ASPNETCORE_URLS zur Verfügung:
Ändern von Inhaltsstamm, App-Name und Umgebung über Umgebungsvariablen oder Befehlszeile
Die folgende Tabelle zeigt die Umgebungsvariablen und Befehlszeilenargumente, die zum Ändern von Inhaltsstamm, Anwendungsname und Umgebung verwendet werden:
Feature
Umgebungsvariable
Befehlszeilenargument
Anwendungsname
ASPNETCORE_APPLICATIONNAME
--applicationName
Umgebungsname
ASPNETCORE_ENVIRONMENT
--environment
Inhaltsstammverzeichnis
ASPNETCORE_CONTENTROOT
--contentRoot
Hinzufügen von Konfigurationsanbietern
Im folgenden Beispiel wird der INI-Konfigurationsanbieter hinzugefügt:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Standardmäßig liest die WebApplicationBuilder die Konfiguration aus mehreren Quellen, darunter:
appSettings.json und appSettings.{environment}.json
Umgebungsvariablen
Die Befehlszeile
Der folgende Code liest HelloKey aus der Konfiguration und zeigt den Wert am Endpunkt / an. Wenn der Konfigurationswert NULL ist, wird „Hello“ message zugewiesen:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
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();
Ändern des Webstamms
Standardmäßig ist der Webstamm relativ zum Inhaltsstamm im Ordner wwwroot angegeben. Im Webstamm sucht die Middleware für statische Dateien nach statischen Dateien. Der Webstamm kann mit WebHostOptions, der Befehlszeile oder mit der Methode UseWebRoot geändert werden:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Container für benutzerdefinierte Abhängigkeitsinjektion
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();
Hinzufügen von Middleware
Für die WebApplication kann eine beliebige vorhandene ASP.NET Core-Middleware konfiguriert werden:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.CreateBuilder initialisiert eine neue Instanz der WebApplicationBuilder-Klasse mit vorkonfigurierten Standardwerten. Die Seite mit Ausnahmen für Entwickler ist in den vorkonfigurierten Standardwerten aktiviert. Durch Ausführung des folgende Codes in der Entwicklungsumgebung wird beim Navigieren zu / eine benutzerfreundliche Seite geöffnet, auf der die Ausnahme angezeigt wird.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ASP.NET Core-Middleware
In der folgenden Tabelle werden einige der Middlewarekomponenten aufgeführt, die häufig mit Minimal-APIs verwendet wird.
In den folgenden Abschnitten werden Routing, Parameterbindung und Antworten behandelt.
Routing
Eine konfigurierte WebApplication unterstützt Map{Verb} und MapMethods, wobei {Verb} eine HTTP-Methode mit Camel-Case-Schreibweise wie Get, Post, Put oder Delete ist:
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();
Die an diese Methoden übergebenen Delegate-Argumente werden als „Routenhandler“ bezeichnet.
Routenhandler
Routenhandler sind Methoden, die ausgeführt werden, wenn die Route übereinstimmt. Als Routenhandler kann ein Lambdaausdruck, eine lokale Funktion, eine Instanzmethode oder eine statische Methode verwendet werden. Routenhandler können synchron oder asynchron sein.
Lambdaausdruck
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();
Lokale Funktion
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
für eine Instanzmethode
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";
}
}
für eine statische Methode
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";
}
}
Endpunkt außerhalb von Program.cs definiert
Minimale APIs müssen sich nicht in Program.cs befinden.
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" });
});
}
}
Weitere Informationen dazu finden Sie auch unter Routengruppen weiter unten in diesem Artikel.
Benannte Endpunkte und Linkgenerierung
Endpunkte können Namen erhalten, um URLs für den jeweiligen Endpunkt zu generieren. Durch die Verwendung eines benannten Endpunkts entfällt das Hartcodieren von Pfaden in einer App:
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();
Der vorangehende Code zeigt The link to the hello route is /hello vom Endpunkt / an.
HINWEIS: Für Endpunktnamen muss Groß-/Kleinschreibung beachtet werden.
Endpunktnamen:
Dieser muss global eindeutig sein.
Werden als OpenAPI-Vorgangs-ID verwendet, wenn die OpenAPI-Unterstützung aktiviert ist. Weitere Informationen finden Sie unter OpenAPI.
Routenparameter
Routenparameter können als Teil der Routenmusterdefinition erfasst werden:
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();
Der vorstehende Code gibt The user id is 3 and book id is 7 aus dem URI /users/3/books/7 zurück.
Der Routenhandler kann die zu erfassende Parameter deklarieren. Bei einer Anforderung an eine Route mit Parametern, deren Erfassung deklariert wurde, werden die Parameter geparst und an den Handler übergeben. Dadurch können die Werte problemlos typsicher erfasst werden. Im vorangegangenen Code sind userId und bookId beide vom Typ int.
Wenn im vorstehenden Code einer der beiden Routenwerte nicht in den Typ int umgewandelt werden kann, wird eine Ausnahme ausgelöst. Die GET-Anforderung /users/hello/books/3 löst die folgende Ausnahme aus:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Platzhalterzeichen und Abfangen aller Routen
Der folgende Code zum Abfangen aller Routen gibt Routing to hello vom Endpunkt „/posts/hello“ zurück:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Routeneinschränkungen
Routeneinschränkungen schränken das Abgleichsverhalten einer Route ein.
Die MapGroup-Erweiterungsmethode hilft, Gruppen von Endpunkten mit einem gemeinsamen Präfix zu organisieren. Sie reduziert sich wiederholenden Code und ermöglicht die benutzerdefinierte Anpassung ganzer Gruppen von Endpunkten mit einem einzigen Aufruf von Methoden wie RequireAuthorization und WithMetadata,die Endpunktmetadaten hinzufügen.
Der folgende Code erstellt beispielsweise zwei ähnliche Endpunktgruppen:
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;
}
};
}
In diesem Szenario können Sie eine relative Adresse für den Location-Header im 201 Created-Ergebnis verwenden:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
Die erste Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /public/todos und ist ohne Authentifizierung zugänglich. Die zweite Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /private/todos und erfordert Authentifizierung.
Die QueryPrivateTodos-Endpunktfilterfactory ist eine lokale Funktion, die die TodoDb-Parameter des Routenhandlers so ändert, dass Zugriff auf private Aufgabendaten zulässig ist und diese gespeichert werden können.
Routengruppen unterstützen auch geschachtelte Gruppen und komplexe Präfixmuster mit Routenparametern und -einschränkungen. Im folgenden Beispiel kann der der user-Gruppe zugeordnete Routenhandler die Routenparameter {org} und {group} erfassen, die in den Präfixen der äußeren Gruppe definiert sind.
Das Präfix kann auch leer sein. Dies kann hilfreich sein, um Endpunktmetadaten oder Filter einer Gruppe von Endpunkten hinzuzufügen, ohne das Routenmuster zu ändern.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
Das Hinzufügen von Filtern oder Metadaten zu einer Gruppe verhält sich genauso wie das individuelle Hinzufügen zu jedem Endpunkt, bevor zusätzliche Filter oder Metadaten hinzugefügt werden, die einer inneren Gruppe oder einem bestimmten Endpunkt hinzugefügt wurden.
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);
});
Im Beispiel oben protokolliert der äußere Filter die eingehende Anforderung vor dem inneren Filter, obwohl er als zweiter hinzugefügt wurde. Da die Filter auf verschiedene Gruppen angewendet wurden, spielt die Reihenfolge, in der sie relativ zueinander hinzugefügt wurden, keine Rolle. Die Reihenfolge, in der Filter hinzugefügt werden, spielt eine Rolle, wenn sie auf dieselbe Gruppe oder einen bestimmten Endpunkt angewendet werden.
Eine Anforderung an /outer/inner/ protokolliert Folgendes:
/outer group filter
/inner group filter
MapGet filter
Parameterbindung
Die Parameterbindung ist der Prozess der Umwandlung von Anforderungsdaten in stark typisierte Parameter, die durch Routenhandler ausgedrückt werden. Eine Bindungsquelle bestimmt, von wo aus Parameter gebunden werden. Bindungsquellen können basierend auf der HTTP-Methode und dem Parametertyp explizit sein oder abgeleitet werden.
Unterstützte Bindungsquellen:
Routenwerte
Abfragezeichenfolge
Header
Text (als JSON)
Von der Abhängigkeitsinjektion bereitgestellte Dienste
Benutzerdefiniert
Die Bindung aus Formularen wird in .NET 6 und 7 nicht nativ unterstützt.
Im folgenden Beispiel verwendet der GET-Routenhandler einige dieser Parameterbindungsquellen:
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 { }
Die folgende Tabelle zeigt die Beziehung zwischen den im vorherigen Beispiel verwendeten Parametern und den zugeordneten Bindungsquellen.
Parameter
Bindungsquelle
id
Routenwert
page
Abfragezeichenfolge
customHeader
header
service
Von der Abhängigkeitsinjektion bereitgestellt
Bei den HTTP-Methoden GET, HEAD, OPTIONS und DELETE erfolgt keine implizite Bindung aus dem Text. Um eine Bindung vom Textkörper (als JSON) für diese HTTP-Methoden zu verwenden, führen Sie explizit eine Bindung mit [FromBody] oder einen Lesevorgang aus HttpRequest durch.
Im folgenden Beispiel verwendet der POST-Routenhandler eine Bindungsquelle des Textkörpers (als JSON) für den Parameter person:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Die Parameter in den vorherigen Beispielen werden alle automatisch über Anforderungsdaten gebunden. Um die Benutzerfreundlichkeit der Parameterbindung zu veranschaulichen, zeigen die folgenden Routenhandler, wie Anforderungsdaten direkt aus der Anforderung gelesen werden:
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>();
// ...
});
Explizite Parameterbindung
Attribute können verwendet werden, um explizit zu deklarieren, von wo Parameter gebunden werden.
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);
Parameter
Bindungsquelle
id
Routenwert mit dem Namen id
page
Abfragezeichenfolge mit dem Namen "p"
service
Von der Abhängigkeitsinjektion bereitgestellt
contentType
Header mit dem Namen "Content-Type"
Hinweis
Die Bindung aus Formularen wird in .NET 6 und 7 nicht nativ unterstützt.
Parameterbindung mit Abhängigkeitsinjektion
Parameterbindung für minimale APIs bindet Parameter durch Abhängigkeitsinjektion (Dependency Injection, DI), wenn der Typ als Dienst konfiguriert wird. Es ist nicht erforderlich, das [FromServices]-Attribut explizit auf einen Parameter anzuwenden. Im folgenden Code geben beide Aktionen die Uhrzeit zurück:
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();
Optionale Parameter
In Routenhandlern deklarierte Parameter werden als erforderlich behandelt:
Wenn eine Anforderung der Route entspricht, wird der Routenhandler nur ausgeführt, wenn alle erforderlichen Parameter in der Anforderung angegeben sind.
Sind nicht alle erforderlichen Parameter enthalten, kommt es zu einem Fehler.
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 zurückgegeben
/products
BadHttpRequestException: Der erforderliche Parameter „int pageNumber“ wurde nicht von der Abfragezeichenfolge bereitgestellt.
/products/1
HTTP-Fehler vom Typ 404, keine übereinstimmende Route
Um pageNumber als optional festzulegen, definieren Sie den Typ als optional, oder geben Sie einen Standardwert an:
Binden des Anforderungstexts als Stream oder PipeReader
Der Anforderungstext kann als Stream oder PipeReader gebunden werden, um effizient Szenarien zu unterstützen, in denen der Benutzer Daten verarbeiten und Folgendes tun muss:
Daten im Blobspeicher speichern oder Daten in die Warteschlange eines Warteschlangenanbieters einreihen.
Die gespeicherten Daten mit einem Workerprozess oder einer Cloudfunktion verarbeiten.
Der folgende Code implementiert eine Hintergrundwarteschlange:
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;
}
Der folgende Code bindet den Anforderungstext an einen 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);
});
Der folgende Code veranschaulicht die vollständige Program.cs-Datei:
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();
Beim Lesen von Daten ist der Stream dasselbe Objekt wie HttpRequest.Body.
Der Anforderungstext wird standardmäßig nicht gepuffert. Nachdem der Textkörper gelesen wurde, ist eine Rückkehr zum Anfang nicht möglich. Der Datenstrom kann nicht mehrmals gelesen werden.
Stream und PipeReader sind außerhalb des minimalen Aktionshandlers nicht verwendbar, da die zugrunde liegenden Puffer verworfen oder wiederverwendet werden.
Dateiuploads mit IFormFile und IFormFileCollection
Binden von Arrays und Zeichenfolgenwerten aus Headern und Abfragezeichenfolgen
Der folgende Code veranschaulicht die Bindung von Abfragezeichenfolgen an ein Array von primitiven Typen, Zeichenfolgenarrays und 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]}");
Die Bindung von Abfragezeichenfolgen oder Headerwerten an ein Array komplexer Typen wird unterstützt, wenn im Typ TryParse implementiert ist. Der folgende Code bindet an ein Zeichenfolgenarray und gibt alle Elemente mit den angegebenen Tags zurück:
Der folgende Code zeigt das Modell und die erforderliche TryParse-Implementierung:
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;
}
}
Der folgende Code richtet eine Bindung mit einem int-Array ein:
Der folgende Code bindet an den Headerschlüssel X-Todo-Id und gibt die Todo-Elemente mit übereinstimmenden Id-Werten zurück:
// 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();
});
Hinweis
Beim Binden eines string[]-Werts aus einer Abfragezeichenfolge führt das Fehlen einer übereinstimmenden Abfragezeichenfolge zu einem leeren Array anstelle eines NULL-Werts.
Parameterbindung für Argumentlisten mit [AsParameters]
AsParametersAttribute ermöglicht einfache Parameterbindung an Typen und nicht komplexe oder rekursive Modellbindung.
Betrachten Sie folgenden Code:
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.
Betrachten Sie den folgenden GET-Endpunkt:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
Mit der folgenden struct können die vorherigen hervorgehobenen Parameter ersetzt werden:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
Der umgestaltete GET-Endpunkt verwendet die vorherige struct mit dem AsParameters-Attribut:
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());
Der folgende Code zeigt zusätzliche Endpunkte in der App:
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();
});
Die folgenden Klassen werden verwendet, um die Parameterlisten umzugestalten:
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!;
}
Der folgende Code zeigt die umgestalteten Endpunkte mit AsParameters und der vorherigen struct und Klassen:
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();
});
Die folgenden record-Typen können verwendet werden, um die vorherigen Parameter zu ersetzen:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
Die Verwendung einer struct mit AsParameters kann effizienter sein als die Verwendung eines record-Typs.
Es gibt zwei Möglichkeiten zum Anpassen der Parameterbindung:
Binden Sie für Routen-, Abfrage- und Headerbindungsquellen benutzerdefinierte Typen, indem Sie eine statische TryParse-Methode für den Typ hinzufügen.
Steuern Sie den Bindungsprozess, indem Sie eine BindAsync-Methode für einen Typ implementieren.
TryParse
TryParse umfasst zwei APIs:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Der folgende Code zeigt Point: 12.3, 10.1 mit dem URI /map?Point=12.3,10.1 an:
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 umfasst die folgenden APIs:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Der nachstehende Code zeigt SortBy:xyz, SortDirection:Desc, CurrentPage:99 mit dem URI /products?SortBy=xyz&SortDir=Desc&Page=99 an:
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
}
Bindungsfehler
Wenn die Bindung fehlschlägt, protokolliert das Framework eine Debugmeldung und gibt abhängig vom Fehlermodus verschiedene Statuscodes an den Client zurück.
Fehlermodus
Parametertypen, die Nullwerte zulassen
Bindungsquelle
Statuscode
{ParameterType}.TryParse gibt false zurück.
ja
Route/Abfrage/Header
400
{ParameterType}.BindAsync gibt null zurück.
ja
custom
400
{ParameterType}.BindAsync wird ausgelöst
Nicht relevant
custom
500
Fehler beim Deserialisieren des JSON-Texts
Nicht relevant
body
400
Falscher Inhaltstyp (nicht application/json)
Nicht relevant
body
415
Bindungsrangfolge
Die Regeln zum Bestimmen einer Bindungsquelle anhand eines Parameters:
Explizites Attribut, das für den Parameter (From*-Attribute) in der folgenden Reihenfolge definiert ist:
Der Parametertyp verfügt über eine gültige statische BindAsync-Methode.
Der Parametertyp lautet „string“ oder verfügt über eine gültige statische TryParse-Methode.
Wenn der Parametername in der Routenvorlage vorhanden ist. In app.Map("/todo/{id}", (int id) => {}); geht id von der Route aus.
Bindung über die Abfragezeichenfolge.
Wenn der Parametertyp ein durch die Abhängigkeitsinjektion bereitgestellter Dienst ist, wird dieser Dienst als Quelle verwendet.
Der Parameter stammt aus dem Text.
Konfigurieren von JSON-Deserialisierungsoptionen für die Textbindung
Die Textbindungsquelle verwendet System.Text.Json für die Deserialisierung. Es ist nicht möglich, diese Standardeinstellung zu ändern, aber die JSON-Serialisierungs- und Deserialisierungsoptionen können konfiguriert werden.
Globales Konfigurieren von JSON-Deserialisierungsoptionen
Optionen, die global für eine App gelten, können durch Aufrufen von ConfigureHttpJsonOptions konfiguriert werden. Das folgende Beispiel enthält öffentliche Felder und Formate der JSON-Ausgabe.
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
// }
Da der Beispielcode sowohl die Serialisierung als auch die Deserialisierung konfiguriert, kann er NameField lesen und NameField in die JSON-Ausgabe einschließen.
Konfigurieren von JSON-Deserialisierungsoptionen für einen Endpunkt
ReadFromJsonAsync verfügt über Überladungen, die ein JsonSerializerOptions-Objekt akzeptieren. Das folgende Beispiel enthält öffentliche Felder und Formate der JSON-Ausgabe.
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
// }
Da der vorangehende Code die angepassten Optionen nur auf die Deserialisierung anwendet, schließt die JSON-Ausgabe NameField aus.
Lesen des Anforderungstexts
Lesen Sie den Anforderungstext direkt mithilfe des Parameters HttpContext oder 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();
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
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");
});
Die statischen Klassen Results und TypedResults enthalten allgemeine Ergebnishilfen. Die Rückgabe von TypedResults wird der Rückgabe von Results vorgezogen. Weitere Informationen finden Sie unter Vergleich von TypedResults und Results.
Anpassen von Ergebnissen
Anwendungen können Antworten steuern, indem sie einen benutzerdefinierten IResult-Typ implementieren. Der folgende Code ist ein Beispiel für einen HTML-Ergebnistyp:
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);
}
}
Es wird empfohlen, Microsoft.AspNetCore.Http.IResultExtensions eine Erweiterungsmethode hinzuzufügen, um diese benutzerdefinierten Ergebnisse leichter auffindbar zu machen.
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();
Eingegebene Ergebnisse
Die IResult-Schnittstelle kann von minimalen APIs zurückgegebene Werte darstellen, welche nicht die implizite Unterstützung für die JSON-Serialisierung des zurückgegebenen Objekts in die HTTP-Antwort verwenden. Die statische Results-Klasse wird verwendet, um unterschiedliche IResult-Objekte zu erstellen, die verschiedene Arten von Antworten darstellen. Beispiel: Festlegen des Antwortstatuscodes oder Umleitung an eine andere URL.
Die IResult implementierenden Typen sind öffentlich, sodass Typassertionen beim Testen zugelassen werden. Beispiel:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
Sie können die Rückgabetypen der entsprechenden Methoden in der statischen TypedResults-Klasse anzeigen, um den richtigen öffentlichen IResult-Typ zu finden, in den Werte umgewandelt erden sollen.
Routen können mithilfe von Autorisierungsrichtlinien geschützt werden. Diese können mit dem Attribut [Authorize] oder der Methode RequireAuthorization angegeben werden.
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();
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();
Nicht authentifizierten Benutzern den Zugriff auf einen Endpunkt gestatten
Durch [AllowAnonymous] können nicht authentifizierte Benutzer auf Endpunkte zugreifen:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Routen können mithilfe von CORS-Richtlinien für CORS aktiviert werden. CORS kann über das Attribut [EnableCors] oder mit der Methode RequireCors deklariert werden. In den folgenden Beispielen wird CORS aktiviert:
Beim Erstellen einer Web-App mit Visual Studio oder dotnet new wird eine Datei Properties/launchSettings.json erstellt, die die Ports angibt, an denen die Anwendung antwortet. In den folgenden Beispielen für Porteinstellungen wird beim Ausführen der App in Visual Studio ein Fehlerdialogfeld Unable to connect to web server 'AppName' angezeigt. Führen Sie die folgenden Beispiele für Portänderungen über die Befehlszeile aus.
In den folgenden Abschnitten wird der Port festgelegt, auf den die App reagiert.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
Im vorangehenden Code antwortet die App auf Port 3000.
Mehrere Ports
Im folgenden Code antwortet die App auf Port 3000 und 4000.
Mit dem folgenden Befehl antwortet die App auf Port 7777:
dotnet run --urls="https://localhost:7777"
Wenn der Endpunkt Kestrel ebenfalls in der Datei appsettings.json konfiguriert ist, wird die in der Datei appsettings.json angegebene URL verwendet. Weitere Informationen finden Sie unter Kestrel-Endpunktkonfiguration.
Lesen des Ports aus der Umgebung
Der folgende Code liest den Port aus der Umgebung:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
Die bevorzugte Methode zur Festlegung des Ports über die Umgebung ist die Verwendung der Umgebungsvariablen ASPNETCORE_URLS, die im folgenden Abschnitt beschrieben wird.
Festlegen der Ports über die ASPNETCORE_URLS-Umgebungsvariable
Für die Festlegung des Ports steht die Umgebungsvariable ASPNETCORE_URLS zur Verfügung:
Ändern von Inhaltsstamm, App-Name und Umgebung über Umgebungsvariablen oder Befehlszeile
Die folgende Tabelle zeigt die Umgebungsvariablen und Befehlszeilenargumente, die zum Ändern von Inhaltsstamm, Anwendungsname und Umgebung verwendet werden:
Feature
Umgebungsvariable
Befehlszeilenargument
Anwendungsname
ASPNETCORE_APPLICATIONNAME
--applicationName
Umgebungsname
ASPNETCORE_ENVIRONMENT
--environment
Inhaltsstammverzeichnis
ASPNETCORE_CONTENTROOT
--contentRoot
Hinzufügen von Konfigurationsanbietern
Im folgenden Beispiel wird der INI-Konfigurationsanbieter hinzugefügt:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Der folgende Code liest HelloKey aus der Konfiguration und zeigt den Wert am Endpunkt / an. Wenn der Konfigurationswert NULL ist, wird „Hello“ message zugewiesen:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Lesen der Umgebung
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Hinzufügen von Protokollierungsanbietern
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();
Hinzufügen von Diensten
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();
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();
Ändern des Webstamms
Standardmäßig ist der Webstamm relativ zum Inhaltsstamm im Ordner wwwroot angegeben. Im Webstamm sucht die Middleware für statische Dateien nach statischen Dateien. Der Webstamm kann mit WebHostOptions, der Befehlszeile oder mit der Methode UseWebRoot geändert werden:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Container für benutzerdefinierte Abhängigkeitsinjektion
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();
Hinzufügen von Middleware
Für die WebApplication kann eine beliebige vorhandene ASP.NET Core-Middleware konfiguriert werden:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.CreateBuilder initialisiert eine neue Instanz der WebApplicationBuilder-Klasse mit vorkonfigurierten Standardwerten. Die Seite mit Ausnahmen für Entwickler ist in den vorkonfigurierten Standardwerten aktiviert. Durch Ausführung des folgende Codes in der Entwicklungsumgebung wird beim Navigieren zu / eine benutzerfreundliche Seite geöffnet, auf der die Ausnahme angezeigt wird.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ASP.NET Core-Middleware
In der folgenden Tabelle werden einige der Middlewarekomponenten aufgeführt, die häufig mit Minimal-APIs verwendet wird.
In den folgenden Abschnitten werden Routing, Parameterbindung und Antworten behandelt.
Routing
Eine konfigurierte WebApplication unterstützt Map{Verb} und 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();
Routenhandler
Routenhandler sind Methoden, die ausgeführt werden, wenn die Route übereinstimmt. Routenhandler können eine Funktion in beliebiger Form sein, sowohl synchron als auch asynchron. Als Routenhandler kann ein Lambdaausdruck, eine lokale Funktion, eine Instanzmethode oder eine statische Methode verwendet werden.
Lambdaausdruck
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();
Lokale Funktion
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
für eine Instanzmethode
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";
}
}
für eine statische Methode
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";
}
}
Benannte Endpunkte und Linkgenerierung
Endpunkte können Namen erhalten, um URLs für den jeweiligen Endpunkt zu generieren. Durch die Verwendung eines benannten Endpunkts entfällt das Hartcodieren von Pfaden in einer App:
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();
Der vorangehende Code zeigt The link to the hello endpoint is /hello vom Endpunkt / an.
HINWEIS: Für Endpunktnamen muss Groß-/Kleinschreibung beachtet werden.
Endpunktnamen:
Dieser muss global eindeutig sein.
Werden als OpenAPI-Vorgangs-ID verwendet, wenn die OpenAPI-Unterstützung aktiviert ist. Weitere Informationen finden Sie unter OpenAPI.
Routenparameter
Routenparameter können als Teil der Routenmusterdefinition erfasst werden:
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();
Der vorstehende Code gibt The user id is 3 and book id is 7 aus dem URI /users/3/books/7 zurück.
Der Routenhandler kann die zu erfassende Parameter deklarieren. Wenn eine Anforderung über eine Route mit Parametern gestellt wird, die zur Erfassung deklariert sind, werden die Parameter analysiert und an den Handler übergeben. Dadurch können die Werte problemlos typsicher erfasst werden. Im vorangegangenen Code sind userId und bookId beide vom Typ int.
Wenn im vorstehenden Code einer der beiden Routenwerte nicht in den Typ int umgewandelt werden kann, wird eine Ausnahme ausgelöst. Die GET-Anforderung /users/hello/books/3 löst die folgende Ausnahme aus:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Platzhalterzeichen und Abfangen aller Routen
Der folgende Code zum Abfangen aller Routen gibt Routing to hello vom Endpunkt „/posts/hello“ zurück:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Routeneinschränkungen
Routeneinschränkungen schränken das Abgleichsverhalten einer Route ein.
Die Parameterbindung ist der Prozess der Umwandlung von Anforderungsdaten in stark typisierte Parameter, die durch Routenhandler ausgedrückt werden. Eine Bindungsquelle bestimmt, von wo aus Parameter gebunden werden. Bindungsquellen können basierend auf der HTTP-Methode und dem Parametertyp explizit sein oder abgeleitet werden.
Unterstützte Bindungsquellen:
Routenwerte
Abfragezeichenfolge
Header
Text (als JSON)
Von der Abhängigkeitsinjektion bereitgestellte Dienste
Benutzerdefiniert
Hinweis
Die Bindung aus Formularen wird in .NET nicht nativ unterstützt.
Im folgenden Beispiel verwendet der GET-Routenhandler einige dieser Parameterbindungsquellen:
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 { }
Die folgende Tabelle zeigt die Beziehung zwischen den im vorherigen Beispiel verwendeten Parametern und den zugeordneten Bindungsquellen.
Parameter
Bindungsquelle
id
Routenwert
page
Abfragezeichenfolge
customHeader
header
service
Von der Abhängigkeitsinjektion bereitgestellt
Bei den HTTP-Methoden GET, HEAD, OPTIONS und DELETE erfolgt keine implizite Bindung aus dem Text. Um eine Bindung vom Textkörper (als JSON) für diese HTTP-Methoden zu verwenden, führen Sie explizit eine Bindung mit [FromBody] oder einen Lesevorgang aus HttpRequest durch.
Im folgenden Beispiel verwendet der POST-Routenhandler eine Bindungsquelle des Textkörpers (als JSON) für den Parameter person:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Die Parameter in den vorherigen Beispielen werden alle automatisch über Anforderungsdaten gebunden. Um die Benutzerfreundlichkeit der Parameterbindung zu veranschaulichen, zeigen die folgenden Beispielroutenhandler, wie Anforderungsdaten direkt aus der Anforderung gelesen werden:
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>();
// ...
});
Explizite Parameterbindung
Attribute können verwendet werden, um explizit zu deklarieren, von wo Parameter gebunden werden.
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);
Parameter
Bindungsquelle
id
Routenwert mit dem Namen id
page
Abfragezeichenfolge mit dem Namen "p"
service
Von der Abhängigkeitsinjektion bereitgestellt
contentType
Header mit dem Namen "Content-Type"
Hinweis
Die Bindung aus Formularen wird in .NET nicht nativ unterstützt.
Parameterbindung mit DI
Parameterbindung für minimale APIs bindet Parameter durch Abhängigkeitsinjektion (Dependency Injection, DI), wenn der Typ als Dienst konfiguriert wird. Es ist nicht erforderlich, das [FromServices]-Attribut explizit auf einen Parameter anzuwenden. Im folgenden Code geben beide Aktionen die Uhrzeit zurück:
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();
Optionale Parameter
In Routenhandlern deklarierte Parameter werden als erforderlich behandelt:
Wenn eine Anforderung der Route entspricht, wird der Routenhandler nur ausgeführt, wenn alle erforderlichen Parameter in der Anforderung angegeben sind.
Sind nicht alle erforderlichen Parameter enthalten, kommt es zu einem Fehler.
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 zurückgegeben
/products
BadHttpRequestException: Der erforderliche Parameter „int pageNumber“ wurde nicht von der Abfragezeichenfolge bereitgestellt.
/products/1
HTTP-Fehler vom Typ 404, keine übereinstimmende Route
Um pageNumber als optional festzulegen, definieren Sie den Typ als optional, oder geben Sie einen Standardwert an:
Es gibt zwei Möglichkeiten zum Anpassen der Parameterbindung:
Binden Sie für Routen-, Abfrage- und Headerbindungsquellen benutzerdefinierte Typen, indem Sie eine statische TryParse-Methode für den Typ hinzufügen.
Steuern Sie den Bindungsprozess, indem Sie eine BindAsync-Methode für einen Typ implementieren.
TryParse
TryParse umfasst zwei APIs:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Der folgende Code zeigt Point: 12.3, 10.1 mit dem URI /map?Point=12.3,10.1 an:
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 umfasst die folgenden APIs:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Der nachstehende Code zeigt SortBy:xyz, SortDirection:Desc, CurrentPage:99 mit dem URI /products?SortBy=xyz&SortDir=Desc&Page=99 an:
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
}
Bindungsfehler
Wenn die Bindung fehlschlägt, protokolliert das Framework eine Debugmeldung und gibt abhängig vom Fehlermodus verschiedene Statuscodes an den Client zurück.
Fehlermodus
Parametertypen, die Nullwerte zulassen
Bindungsquelle
Statuscode
{ParameterType}.TryParse gibt false zurück.
ja
Route/Abfrage/Header
400
{ParameterType}.BindAsync gibt null zurück.
ja
custom
400
{ParameterType}.BindAsync wird ausgelöst
Nicht relevant
custom
500
Fehler beim Deserialisieren des JSON-Texts
Nicht relevant
body
400
Falscher Inhaltstyp (nicht application/json)
Nicht relevant
body
415
Bindungsrangfolge
Die Regeln zum Bestimmen einer Bindungsquelle anhand eines Parameters:
Explizites Attribut, das für den Parameter (From*-Attribute) in der folgenden Reihenfolge definiert ist:
Der Parametertyp umfasst eine gültige BindAsync-Methode.
Der Parametertyp lautet „string“ oder umfasst eine gültige TryParse-Methode.
Wenn der Parametername in der Routenvorlage vorhanden ist. In app.Map("/todo/{id}", (int id) => {}); geht id von der Route aus.
Bindung über die Abfragezeichenfolge.
Wenn der Parametertyp ein durch die Abhängigkeitsinjektion bereitgestellter Dienst ist, wird dieser Dienst als Quelle verwendet.
Der Parameter stammt aus dem Text.
Anpassen der JSON-Bindung
Die Textbindungsquelle verwendet System.Text.Json für die Deserialisierung. Es ist nicht möglich, diese Standardeinstellung zu ändern, aber die Bindung kann mithilfe von anderen zuvor beschriebenen Techniken angepasst werden. Verwenden Sie zum Anpassen von JSON-Serialisierungsoptionen Code ähnlich dem folgenden:
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;
}
Der vorangehende Code:
Konfiguriert die JSON-Standardoptionen sowohl für die Eingabe als auch für die Ausgabe.
Gibt folgenden JSON-Code zurück:
{
"id": 1,
"name": "Joe Smith"
}
Wenn Folgendes gepostet wird:
{
"Id": 1,
"Name": "Joe Smith"
}
Lesen des Anforderungstexts
Lesen Sie den Anforderungstext direkt mithilfe des Parameters HttpContext oder 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();
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
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();
Anwendungen können Antworten steuern, indem sie einen benutzerdefinierten IResult-Typ implementieren. Der folgende Code ist ein Beispiel für einen HTML-Ergebnistyp:
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);
}
}
Es wird empfohlen, Microsoft.AspNetCore.Http.IResultExtensions eine Erweiterungsmethode hinzuzufügen, um diese benutzerdefinierten Ergebnisse leichter auffindbar zu machen.
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();
Autorisierung
Routen können mithilfe von Autorisierungsrichtlinien geschützt werden. Diese können mit dem Attribut [Authorize] oder der Methode RequireAuthorization angegeben werden.
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();
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();
Nicht authentifizierten Benutzern den Zugriff auf einen Endpunkt gestatten
Durch [AllowAnonymous] können nicht authentifizierte Benutzer auf Endpunkte zugreifen:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Routen können mithilfe von CORS-Richtlinien für CORS aktiviert werden. CORS kann über das Attribut [EnableCors] oder mit der Methode RequireCors deklariert werden. In den folgenden Beispielen wird CORS aktiviert:
Hinweis: Der Autor hat diesen Artikel mit Unterstützung von KI erstellt. Weitere Informationen
Zusammenarbeit auf GitHub
Die Quelle für diesen Inhalt finden Sie auf GitHub, wo Sie auch Issues und Pull Requests erstellen und überprüfen können. Weitere Informationen finden Sie in unserem Leitfaden für Mitwirkende.
Feedback zu ASP.NET Core
ASP.NET Core ist ein Open Source-Projekt. Wählen Sie einen Link aus, um Feedback zu geben:
Das ultimative Power BI-, Fabric-, SQL- und AI-communitygeführte Ereignis. 31. März bis 2. April. Verwenden Sie den Code MSCUST für einen Rabatt von 150 USD. Die Preise steigen am 11. Februar.
Erfahren Sie, wie Sie eine Web-API mit .NET erstellen. Außerdem erfahren Sie, wie Sie verschiedene Routen einrichten, um sowohl Lesen als auch Schreiben zu verarbeiten.