Parameterbindung in Minimal-API-Apps
Hinweis
Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Warnung
Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der .NET- und .NET Core-Supportrichtlinie. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Wichtig
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.
Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
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);
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" |
Explizite Bindung aus Formularwerten
Das [FromForm]
-Attribut bindet Formularwerte:
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.
Den vollständigen Beispielcode finden Sie im AspNetCore.Docs.Samples-Repository.
Sichere Bindung von IFormFile und IFormFileCollection
Komplexe Formularbindung wird unter Verwendung von IFormFile und IFormFileCollection mithilfe von [FromForm]
unterstützt:
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.
Weitere Informationen finden Sie unter Formularbindung in Minimal-APIs.
Den vollständigen Beispielcode finden Sie im AspNetCore.Docs.Samples-Repository.
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 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:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";
app.MapGet("/products2", ListProducts);
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
3 zurückgegeben |
/products |
1 zurückgegeben |
/products2 |
1 zurückgegeben |
Der vorangehende Nullwerte zulassende und Standardwert gilt für alle Quellen:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
Der stehende Code ruft die Methode mit einem NULL-Produkt auf, wenn kein Anforderungstext gesendet wird.
HINWEIS: Wenn ungültige Daten angegeben werden und der Parameter Nullwerte zulässt, wird der Routenhandler nicht ausgeführt.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
3 zurückgegeben |
/products |
1 zurückgegeben |
/products?pageNumber=two |
BadHttpRequestException : Fehler beim Binden von Parameter "Nullable<int> pageNumber" aus „two“. |
/products/two |
HTTP-Fehler vom Typ 404, keine übereinstimmende Route |
Weitere Informationen finden Sie im Abschnitt Bindungsfehler.
Sondertypen
Die folgenden Typen werden ohne explizite Attribute gebunden:
HttpContext: Der Kontext, der alle Informationen zur aktuellen HTTP-Anforderung oder -Antwort enthält:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest und HttpResponse: die HTTP-Anforderung und HTTP-Antwort:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken: das mit der aktuellen HTTP-Anforderung verknüpfte Abbruchtoken:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal: der mit der Anforderung verknüpfte Benutzer, gebunden aus HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
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.
Beispielsweise werden die Daten möglicherweise in Azure Queue Storage eingereiht oder in Azure Blob Storage gespeichert.
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 wieHttpRequest.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
undPipeReader
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 verwendet IFormFile und IFormFileCollection zum Hochladen der Datei:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
Authentifizierte Dateiuploadanforderungen werden mithilfe eines Autorisierungsheaders, eines Clientzertifikats oder eines cookie-Headers unterstützt.
Bindung an Formulare mit IFormCollection, IFormFile und IFormFileCollection
Die Bindung von formularbasierten Parametern mit IFormCollection, IFormFileund IFormFileCollection wird unterstützt. OpenAPI-Metadaten werden für Formularparameter abgeleitet, um die Integration mit der Swagger-Benutzeroberfläche zu unterstützen.
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();
Weitere Informationen zu XSRF-Angriffen finden Sie unter Fälschungsschutz mit minimalen APIs
Weitere Informationen finden Sie unter Formularbindung in Minimal-APIs.
Binden an Sammlungen und komplexe Typen aus Formularen
Die Bindung wird unterstützt für:
- Sammlungen, z. B . Liste und Wörterbuch
- Komplexe Typen, z. B.
Todo
oderProject
Dies wird im folgenden Code veranschaulicht:
- 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 Wertfalse
. Wenn das KontrollkästchenisCompleted
beim Senden des Formulars aktiviert ist, werden die Wertetrue
undfalse
beide als Werte übermittelt. Ist das Kontrollkästchen nicht aktiviert, wird nur der Wertfalse
der ausgeblendeten Eingabe übermittelt. Der ASP.NET Core-Modellbindungsprozess liest bei der Bindung an einenbool
-Wert nur den ersten Wert. Dies führt bei aktivierten Kontrollkästchen zum Ergebnistrue
und bei nicht aktivierten Kontrollkästchen zum Ergebnisfalse
.
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:
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
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:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Fügen Sie zum Testen des vorherigen Codes den folgenden Endpunkt hinzu, um die Datenbank mit Todo
-Elementen aufzufüllen:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Verwenden Sie ein Tool wie HttpRepl
, um die folgenden Daten an den vorherigen Endpunkt zu übergeben:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
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.
Den vollständigen Beispielcode finden Sie im AspNetCore.Docs.Samples-Repository.
Benutzerdefinierte Bindung
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:
- Routenwerte:
[FromRoute]
- Abfragezeichenfolge:
[FromQuery]
- Header:
[FromHeader]
- Hauptteil:
[FromBody]
- Formular:
[FromForm]
- Service:
[FromServices]
- Parameterwerte:
[AsParameters]
- Routenwerte:
- Sondertypen
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormCollection
(HttpContext.Request.Form
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- 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 Parametername in der Routenvorlage vorhanden ist (z. B.
- 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();
Der obige Code:
- Greift mit HttpRequest.BodyReader auf den Anforderungstext zu.
- Kopiert den Anforderungstext in eine lokale Datei.
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:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";
app.MapGet("/products2", ListProducts);
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
3 zurückgegeben |
/products |
1 zurückgegeben |
/products2 |
1 zurückgegeben |
Der vorangehende Nullwerte zulassende und Standardwert gilt für alle Quellen:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
Der stehende Code ruft die Methode mit einem NULL-Produkt auf, wenn kein Anforderungstext gesendet wird.
HINWEIS: Wenn ungültige Daten angegeben werden und der Parameter Nullwerte zulässt, wird der Routenhandler nicht ausgeführt.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
3 zurückgegeben |
/products |
1 zurückgegeben |
/products?pageNumber=two |
BadHttpRequestException : Fehler beim Binden von Parameter "Nullable<int> pageNumber" aus „two“. |
/products/two |
HTTP-Fehler vom Typ 404, keine übereinstimmende Route |
Weitere Informationen finden Sie im Abschnitt Bindungsfehler.
Sondertypen
Die folgenden Typen werden ohne explizite Attribute gebunden:
HttpContext: Der Kontext, der alle Informationen zur aktuellen HTTP-Anforderung oder -Antwort enthält:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest und HttpResponse: die HTTP-Anforderung und HTTP-Antwort:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken: das mit der aktuellen HTTP-Anforderung verknüpfte Abbruchtoken:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal: der mit der Anforderung verknüpfte Benutzer, gebunden aus HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
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.
Beispielsweise werden die Daten möglicherweise in Azure Queue Storage eingereiht oder in Azure Blob Storage gespeichert.
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 wieHttpRequest.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
undPipeReader
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 verwendet IFormFile und IFormFileCollection zum Hochladen der Datei:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
Authentifizierte Dateiuploadanforderungen werden mithilfe eines Autorisierungsheaders, eines Clientzertifikats oder eines cookie-Headers unterstützt.
Es gibt keine integrierte Unterstützung für Anti-Forgery-Systeme in ASP.NET Core 7.0. Antiforgery ist in ASP.NET Core 8.0 und höher verfügbar. Die Implementierung ist jedoch mit dem IAntiforgery
-Dienst möglich.
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:
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
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:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Fügen Sie zum Testen des vorherigen Codes den folgenden Endpunkt hinzu, um die Datenbank mit Todo
-Elementen aufzufüllen:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Verwenden Sie ein API-Testtool wie HttpRepl
, um die folgenden Daten an den vorherigen Endpunkt zu übergeben:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
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.
Den vollständigen Beispielcode finden Sie im AspNetCore.Docs.Samples-Repository.
Benutzerdefinierte Bindung
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:
- Routenwerte:
[FromRoute]
- Abfragezeichenfolge:
[FromQuery]
- Header:
[FromHeader]
- Hauptteil:
[FromBody]
- Service:
[FromServices]
- Parameterwerte:
[AsParameters]
- Routenwerte:
- Sondertypen
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- 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) => {});
gehtid
von der Route aus. - Bindung über die Abfragezeichenfolge.
- Wenn der Parametername in der Routenvorlage vorhanden ist. In
- 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();
Der obige Code:
- Greift mit HttpRequest.BodyReader auf den Anforderungstext zu.
- Kopiert den Anforderungstext in eine lokale Datei.