Udostępnij za pośrednictwem


Powiązanie parametrów w minimalnych aplikacjach interfejsu API

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Powiązanie parametrów to proces konwertowania danych żądania na silnie typizowane parametry, które są wyrażane przez programy obsługi tras. Źródło powiązania określa, skąd są powiązane parametry. Źródła powiązań mogą być jawne lub wnioskowane na podstawie metody HTTP i typu parametru.

Obsługiwane źródła powiązań:

  • Wartości tras
  • Ciąg zapytania
  • Nagłówek
  • Treść (jako kod JSON)
  • Wartości formularza
  • Usługi udostępniane przez wstrzykiwanie zależności
  • Niestandardowy

GET Poniższa procedura obsługi tras używa niektórych z tych źródeł powiązań parametrów:

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 { }

W poniższej tabeli przedstawiono relację między parametrami używanymi w poprzednim przykładzie i skojarzonymi źródłami powiązań.

Parametr Źródło powiązania
id wartość trasy
page ciąg zapytania
customHeader nagłówek
service Udostępniane przez wstrzykiwanie zależności

Metody GETHTTP , HEAD, OPTIONSi DELETE nie są niejawnie powiązane z treścią. Aby powiązać z treścią (jako kod JSON) dla tych metod HTTP, powiąż jawnie z elementem [FromBody] lub odczyt z pliku HttpRequest.

W poniższym przykładzie procedura obsługi tras POST używa powiązania źródła treści (jako kodu JSON) dla parametru person :

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parametry w poprzednich przykładach są automatycznie powiązane z danymi żądania. Aby zademonstrować wygodę zapewnianą przez powiązanie parametrów, następujące programy obsługi tras pokazują, jak odczytywać dane żądania bezpośrednio z żądania:

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

    // ...
});

Jawne powiązanie parametrów

Atrybuty mogą służyć do jawnego deklarowania, gdzie parametry są powiązane.

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);
Parametr Źródło powiązania
id wartość trasy o nazwie id
page ciąg zapytania o nazwie "p"
service Udostępniane przez wstrzykiwanie zależności
contentType nagłówek o nazwie "Content-Type"

Jawne powiązanie z wartości formularza

Atrybut [FromForm] wiąże wartości formularza:

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.

Alternatywą jest użycie atrybutu [AsParameters] z typem niestandardowym, który ma właściwości oznaczone jako [FromForm]. Na przykład następujący kod wiąże się z wartościami formularza z właściwościami struktury rekordu NewTodoRequest :

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

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

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

        todo.Attachment = attachmentName;
    }

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

    return Results.Ok();
});

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

Aby uzyskać więcej informacji, zobacz sekcję dotyczącą parametrów asparameters w dalszej części tego artykułu.

Kompletny przykładowy kod znajduje się w repozytorium AspNetCore.Docs.Samples .

Bezpieczne powiązanie z klasy IFormFile i IFormFileCollection

Tworzenie złożonego powiązania formularza jest obsługiwane przy użyciu polecenia IFormFile i IFormFileCollection przy użyciu polecenia [FromForm]:

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

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

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

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

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

app.Run();

Parametry powiązane z żądaniem zawierają [FromForm] token antyforgery. Token ochrony przed fałszerzami jest weryfikowany po przetworzeniu żądania. Aby uzyskać więcej informacji, zobacz Antiforgery with Minimal APIs (Antiforgery with Minimal APIs).

Aby uzyskać więcej informacji, zobacz Powiązanie formularza w minimalnych interfejsach API.

Kompletny przykładowy kod znajduje się w repozytorium AspNetCore.Docs.Samples .

Powiązanie parametrów z wstrzyknięciem zależności

Powiązanie parametrów dla minimalnych interfejsów API wiąże parametry za pośrednictwem wstrzykiwania zależności, gdy typ jest skonfigurowany jako usługa. Nie jest konieczne jawne zastosowanie atrybutu [FromServices] do parametru. W poniższym kodzie obie akcje zwracają czas:

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

Parametry opcjonalne

Parametry zadeklarowane w programach obsługi tras są traktowane zgodnie z wymaganiami:

  • Jeśli żądanie pasuje do trasy, procedura obsługi tras jest uruchamiana tylko wtedy, gdy wszystkie wymagane parametry są podane w żądaniu.
  • Niepowodzenie podania wszystkich wymaganych parametrów powoduje wystąpienie błędu.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
Identyfikator URI result
/products?pageNumber=3 3 zwrócone
/products BadHttpRequestException: Wymagany parametr "int pageNumber" nie został podany z ciągu zapytania.
/products/1 Błąd HTTP 404, brak pasującej trasy

Aby ustawić pageNumber wartość opcjonalną, zdefiniuj typ jako opcjonalny lub podaj wartość domyślną:

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();
Identyfikator URI result
/products?pageNumber=3 3 zwrócone
/products 1 zwrócone
/products2 1 zwrócone

Poprzednia wartość dopuszczająca wartość null i domyślna ma zastosowanie do wszystkich źródeł:

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

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

app.Run();

Powyższy kod wywołuje metodę z produktem o wartości null, jeśli nie zostanie wysłana żadna treść żądania.

UWAGA: Jeśli podano nieprawidłowe dane i parametr ma wartość null, procedura obsługi tras nie jest uruchamiana.

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

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

app.Run();
Identyfikator URI result
/products?pageNumber=3 3 Zwracane
/products 1 Zwracane
/products?pageNumber=two BadHttpRequestException: Nie można powiązać parametru "Nullable<int> pageNumber" z "dwóch".
/products/two Błąd HTTP 404, brak pasującej trasy

Aby uzyskać więcej informacji, zobacz sekcję Błędy powiązań.

Typy specjalne

Następujące typy są powiązane bez jawnych atrybutów:

  • HttpContext: kontekst zawierający wszystkie informacje o bieżącym żądaniu HTTP lub odpowiedzi:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest i HttpResponse: Żądanie HTTP i odpowiedź HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: token anulowania skojarzony z bieżącym żądaniem HTTP:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: użytkownik skojarzony z żądaniem powiązany z elementem HttpContext.User:

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

Powiąż treść żądania jako element Stream lub PipeReader

Treść żądania może wiązać się ze scenariuszami Stream lub PipeReader , w których użytkownik musi przetwarzać dane i:

  • Zapisz dane w magazynie obiektów blob lub zapisz dane w kolejce do dostawcy kolejki.
  • Przetwarzanie przechowywanych danych za pomocą procesu roboczego lub funkcji w chmurze.

Na przykład dane mogą być w kolejce do usługi Azure Queue Storage lub przechowywane w usłudze Azure Blob Storage.

Poniższy kod implementuje kolejkę w tle:

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

Poniższy kod wiąże treść żądania z elementem 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);
});

Poniższy kod przedstawia kompletny Program.cs plik:

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();
  • Podczas odczytywania danych Stream obiekt jest tym samym obiektem co HttpRequest.Body.
  • Treść żądania nie jest domyślnie buforowana. Po odczytaniu treści nie można jej przewijać. Strumień nie może być odczytywany wiele razy.
  • Elementy Stream i PipeReader nie mogą być używane poza minimalną procedurą obsługi akcji, ponieważ bazowe zostaną usunięte lub ponownie użyte.

Przekazywanie plików przy użyciu elementu IFormFile i IFormFileCollection

Poniższy kod używa instrukcji IFormFile i IFormFileCollection do przekazywania pliku:

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

Uwierzytelnione żądania przekazywania plików są obsługiwane przy użyciu nagłówka autoryzacji, certyfikatu klienta lub nagłówka cookie .

Wiązanie z formularzami za pomocą klasy IFormCollection, IFormFile i IFormFileCollection

Powiązanie z parametrów opartych na formularzach przy użyciu parametrów IFormCollection, IFormFilei IFormFileCollection jest obsługiwane. Metadane interfejsu OpenAPI są wnioskowane, aby parametry formularza obsługiwały integrację z interfejsem użytkownika struktury Swagger.

Poniższy kod przekazuje pliki przy użyciu powiązania wnioskowanego z IFormFile typu:

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

Ostrzeżenie: Podczas implementowania formularzy aplikacja musi zapobiegać atakom fałszerzowania żądań między witrynami (XSRF/CSRF). W poprzednim kodzie IAntiforgery usługa jest używana do zapobiegania atakom XSRF przez generowanie i walidację tokenu antyforgery:

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

Aby uzyskać więcej informacji na temat ataków XSRF, zobacz Antiforgery with Minimal APIs (Ochrona przed atakami za pomocą minimalnych interfejsów API)

Aby uzyskać więcej informacji, zobacz Powiązanie formularza w minimalnych interfejsach API;

Wiązanie z kolekcjami i typami złożonymi z formularzy

Powiązanie jest obsługiwane w następujących celach:

  • Kolekcje, na przykład Lista i Słownik
  • Typy złożone, na przykład lub TodoProject

Poniższy kod pokazuje:

  • Minimalny punkt końcowy, który wiąże dane wejściowe z wieloczęściowym formularzem wejściowym ze złożonym obiektem.
  • Jak używać usług antyforgery do obsługi generowania i walidacji tokenów antyforgeryjnych.
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));
}

Powyższy kod:

  • Parametr docelowy musi być oznaczony adnotacją z atrybutem [FromForm] , aby uściślić parametry, które powinny być odczytywane z treści JSON.
  • Powiązanie z typów złożonych lub kolekcji nie jest obsługiwane w przypadku minimalnych interfejsów API skompilowanych za pomocą generatora delegatów żądań.
  • Znacznik pokazuje dodatkowe ukryte dane wejściowe o nazwie isCompleted i wartości false. isCompleted Jeśli pole wyboru jest zaznaczone podczas przesyłania formularza, obie wartości true i false są przesyłane jako wartości. Jeśli pole wyboru jest niezaznaczone, zostanie przesłana tylko ukryta wartość false wejściowa. Proces powiązania modelu ASP.NET Core odczytuje tylko pierwszą wartość podczas tworzenia powiązania z wartością bool , co powoduje true zaznaczenie pól wyboru zaznaczonego i false niezaznaczonego pola wyboru.

Przykład danych formularza przesłanych do poprzedniego punktu końcowego wygląda następująco:

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

Wiązanie tablic i wartości ciągów z nagłówków i ciągów zapytania

Poniższy kod demonstruje powiązania ciągów zapytania z tablicą typów pierwotnych, tablic ciągów i 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]}");

Powiązanie ciągów zapytania lub wartości nagłówków z tablicą typów złożonych jest obsługiwane, gdy typ został TryParse zaimplementowany. Poniższy kod wiąże się z tablicą ciągów i zwraca wszystkie elementy z określonymi tagami:

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

Poniższy kod przedstawia model i wymaganą TryParse implementację:

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

Poniższy kod wiąże się z tablicą int :

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

Aby przetestować poprzedni kod, dodaj następujący punkt końcowy, aby wypełnić bazę danych elementami Todo :

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

    return Results.Ok(todos);
});

Użyj narzędzia, takiego jak HttpRepl , aby przekazać następujące dane do poprzedniego punktu końcowego:

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

Poniższy kod wiąże się z kluczem X-Todo-Id nagłówka i zwraca Todo elementy z pasującymi Id wartościami:

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

Uwaga

Podczas tworzenia powiązania string[] elementu z ciągu zapytania brak pasującej wartości ciągu zapytania spowoduje, że zamiast wartości null zostanie pusta tablica.

Powiązanie parametrów dla list argumentów za pomocą parametrów [AsParameters]

AsParametersAttribute Umożliwia proste powiązanie parametrów z typami, a nie złożonymi lub cyklicznych powiązań modelu.

Spójrzmy na poniższy kod:

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.

Rozważ następujący GET punkt końcowy:

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

struct Następujące elementy mogą służyć do zastępowania poprzednich wyróżnionych parametrów:

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

Refaktoryzowany GET punkt końcowy używa powyższego struct atrybutu AsParameters :

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

Poniższy kod przedstawia dodatkowe punkty końcowe w aplikacji:

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

Następujące klasy służą do refaktoryzacji list parametrów:

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

Poniższy kod przedstawia refaktoryzowane punkty końcowe przy użyciu poleceń AsParameters oraz poprzednie struct klasy i :

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

Zastąp powyższe parametry następującymi record typami:

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

Użycie elementu z AsParameters funkcją struct może być bardziej wydajne niż użycie record typu.

Kompletny przykładowy kod w repozytorium AspNetCore.Docs.Samples .

Wiązanie niestandardowe

Istnieją dwa sposoby dostosowywania powiązania parametrów:

  1. W przypadku źródeł tras, zapytań i powiązań nagłówków powiąż typy niestandardowe, dodając metodę statyczną TryParse dla typu.
  2. Kontrolowanie procesu wiązania przez zaimplementowanie BindAsync metody dla typu.

TryParse

TryParse ma dwa interfejsy API:

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

Poniższy kod jest wyświetlany Point: 12.3, 10.1 za pomocą identyfikatora URI /map?Point=12.3,10.1:

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

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

app.Run();

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

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

        point = null;
        return false;
    }
}

BindAsync

BindAsync ma następujące interfejsy API:

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

Poniższy kod jest wyświetlany SortBy:xyz, SortDirection:Desc, CurrentPage:99 za pomocą identyfikatora URI /products?SortBy=xyz&SortDir=Desc&Page=99:

using System.Reflection;

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

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

app.Run();

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

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

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

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

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

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

Błędy powiązań

Gdy powiązanie zakończy się niepowodzeniem, platforma rejestruje komunikat debugowania i zwraca różne kody stanu do klienta w zależności od trybu awarii.

Tryb awarii Typ parametru dopuszczalnego do wartości null Źródło powiązania Kod stanu
{ParameterType}.TryParse Zwraca false tak route/query/header 400
{ParameterType}.BindAsync Zwraca null tak niestandardowe 400
{ParameterType}.BindAsync Zgłasza nie ma znaczenia niestandardowe 500
Nie można wykonać deserializacji treści JSON nie ma znaczenia treść 400
Nieprawidłowy typ zawartości (nie application/json) nie ma znaczenia treść 415

Pierwszeństwo powiązania

Reguły określania źródła powiązania z parametru:

  1. Jawny atrybut zdefiniowany w parametrze (atrybuty From*) w następującej kolejności:
    1. Wartości tras: [FromRoute]
    2. Ciąg zapytania: [FromQuery]
    3. Nagłówek: [FromHeader]
    4. Ciało: [FromBody]
    5. Formularz: [FromForm]
    6. Usługa: [FromServices]
    7. Wartości parametrów: [AsParameters]
  2. Typy specjalne
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormCollection (HttpContext.Request.Form)
    7. IFormFileCollection (HttpContext.Request.Form.Files)
    8. IFormFile (HttpContext.Request.Form.Files[paramName])
    9. Stream (HttpContext.Request.Body)
    10. PipeReader (HttpContext.Request.BodyReader)
  3. Typ parametru ma prawidłową metodę statyczną BindAsync .
  4. Typ parametru jest ciągiem lub ma prawidłową metodę statyczną TryParse .
    1. Jeśli nazwa parametru istnieje na przykład w szablonie trasy, app.Map("/todo/{id}", (int id) => {});jest ona powiązana z trasą.
    2. Powiązana z ciągu zapytania.
  5. Jeśli typ parametru jest usługą dostarczaną przez iniekcję zależności, używa tej usługi jako źródła.
  6. Parametr pochodzi z treści.

Konfigurowanie opcji deserializacji JSON dla powiązania treści

Źródło powiązania treści używa System.Text.Json do deserializacji. Nie można zmienić tej wartości domyślnej, ale można skonfigurować opcje serializacji i deserializacji JSON.

Globalne konfigurowanie opcji deserializacji JSON

Opcje stosowane globalnie dla aplikacji można skonfigurować, wywołując ConfigureHttpJsonOptionsmetodę . Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych JSON.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

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

Ponieważ przykładowy kod konfiguruje zarówno serializacji, jak i deserializacji, może odczytywać NameField i uwzględniać NameField dane wyjściowe w formacie JSON.

Konfigurowanie opcji deserializacji JSON dla punktu końcowego

ReadFromJsonAsync ma przeciążenia, które akceptują JsonSerializerOptions obiekt. Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych JSON.

using System.Text.Json;

var app = WebApplication.Create();

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

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

app.Run();

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

Ponieważ powyższy kod stosuje dostosowane opcje tylko do deserializacji, dane wyjściowe JSON wykluczają NameFieldwartość .

Odczytywanie treści żądania

Odczytywanie treści żądania bezpośrednio przy użyciu parametru HttpContext lub 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();

Powyższy kod ma następujące działanie:

  • Uzyskuje dostęp do treści żądania przy użyciu polecenia HttpRequest.BodyReader.
  • Kopiuje treść żądania do pliku lokalnego.

Powiązanie parametrów to proces konwertowania danych żądania na silnie typizowane parametry, które są wyrażane przez programy obsługi tras. Źródło powiązania określa, skąd są powiązane parametry. Źródła powiązań mogą być jawne lub wnioskowane na podstawie metody HTTP i typu parametru.

Obsługiwane źródła powiązań:

  • Wartości tras
  • Ciąg zapytania
  • Nagłówek
  • Treść (jako kod JSON)
  • Usługi udostępniane przez wstrzykiwanie zależności
  • Niestandardowy

Powiązanie z wartości formularza nie jest natywnie obsługiwane na platformie .NET 6 i 7.

GET Poniższa procedura obsługi tras używa niektórych z tych źródeł powiązań parametrów:

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 { }

W poniższej tabeli przedstawiono relację między parametrami używanymi w poprzednim przykładzie i skojarzonymi źródłami powiązań.

Parametr Źródło powiązania
id wartość trasy
page ciąg zapytania
customHeader nagłówek
service Udostępniane przez wstrzykiwanie zależności

Metody GETHTTP , HEAD, OPTIONSi DELETE nie są niejawnie powiązane z treścią. Aby powiązać z treścią (jako kod JSON) dla tych metod HTTP, powiąż jawnie z elementem [FromBody] lub odczyt z pliku HttpRequest.

W poniższym przykładzie procedura obsługi tras POST używa powiązania źródła treści (jako kodu JSON) dla parametru person :

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parametry w poprzednich przykładach są automatycznie powiązane z danymi żądania. Aby zademonstrować wygodę zapewnianą przez powiązanie parametrów, następujące programy obsługi tras pokazują, jak odczytywać dane żądania bezpośrednio z żądania:

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

    // ...
});

Jawne powiązanie parametrów

Atrybuty mogą służyć do jawnego deklarowania, gdzie parametry są powiązane.

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);
Parametr Źródło powiązania
id wartość trasy o nazwie id
page ciąg zapytania o nazwie "p"
service Udostępniane przez wstrzykiwanie zależności
contentType nagłówek o nazwie "Content-Type"

Uwaga

Powiązanie z wartości formularza nie jest natywnie obsługiwane na platformie .NET 6 i 7.

Powiązanie parametrów z wstrzyknięciem zależności

Powiązanie parametrów dla minimalnych interfejsów API wiąże parametry za pośrednictwem wstrzykiwania zależności, gdy typ jest skonfigurowany jako usługa. Nie jest konieczne jawne zastosowanie atrybutu [FromServices] do parametru. W poniższym kodzie obie akcje zwracają czas:

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

Parametry opcjonalne

Parametry zadeklarowane w programach obsługi tras są traktowane zgodnie z wymaganiami:

  • Jeśli żądanie pasuje do trasy, procedura obsługi tras jest uruchamiana tylko wtedy, gdy wszystkie wymagane parametry są podane w żądaniu.
  • Niepowodzenie podania wszystkich wymaganych parametrów powoduje wystąpienie błędu.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
Identyfikator URI result
/products?pageNumber=3 3 zwrócone
/products BadHttpRequestException: Wymagany parametr "int pageNumber" nie został podany z ciągu zapytania.
/products/1 Błąd HTTP 404, brak pasującej trasy

Aby ustawić pageNumber wartość opcjonalną, zdefiniuj typ jako opcjonalny lub podaj wartość domyślną:

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();
Identyfikator URI result
/products?pageNumber=3 3 zwrócone
/products 1 zwrócone
/products2 1 zwrócone

Poprzednia wartość dopuszczająca wartość null i domyślna ma zastosowanie do wszystkich źródeł:

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

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

app.Run();

Powyższy kod wywołuje metodę z produktem o wartości null, jeśli nie zostanie wysłana żadna treść żądania.

UWAGA: Jeśli podano nieprawidłowe dane i parametr ma wartość null, procedura obsługi tras nie jest uruchamiana.

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

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

app.Run();
Identyfikator URI result
/products?pageNumber=3 3 Zwracane
/products 1 Zwracane
/products?pageNumber=two BadHttpRequestException: Nie można powiązać parametru "Nullable<int> pageNumber" z "dwóch".
/products/two Błąd HTTP 404, brak pasującej trasy

Aby uzyskać więcej informacji, zobacz sekcję Błędy powiązań.

Typy specjalne

Następujące typy są powiązane bez jawnych atrybutów:

  • HttpContext: kontekst zawierający wszystkie informacje o bieżącym żądaniu HTTP lub odpowiedzi:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest i HttpResponse: Żądanie HTTP i odpowiedź HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: token anulowania skojarzony z bieżącym żądaniem HTTP:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: użytkownik skojarzony z żądaniem powiązany z elementem HttpContext.User:

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

Powiąż treść żądania jako element Stream lub PipeReader

Treść żądania może wiązać się ze scenariuszami Stream lub PipeReader , w których użytkownik musi przetwarzać dane i:

  • Zapisz dane w magazynie obiektów blob lub zapisz dane w kolejce do dostawcy kolejki.
  • Przetwarzanie przechowywanych danych za pomocą procesu roboczego lub funkcji w chmurze.

Na przykład dane mogą być w kolejce do usługi Azure Queue Storage lub przechowywane w usłudze Azure Blob Storage.

Poniższy kod implementuje kolejkę w tle:

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

Poniższy kod wiąże treść żądania z elementem 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);
});

Poniższy kod przedstawia kompletny Program.cs plik:

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();
  • Podczas odczytywania danych Stream obiekt jest tym samym obiektem co HttpRequest.Body.
  • Treść żądania nie jest domyślnie buforowana. Po odczytaniu treści nie można jej przewijać. Strumień nie może być odczytywany wiele razy.
  • Elementy Stream i PipeReader nie mogą być używane poza minimalną procedurą obsługi akcji, ponieważ bazowe zostaną usunięte lub ponownie użyte.

Przekazywanie plików przy użyciu elementu IFormFile i IFormFileCollection

Poniższy kod używa instrukcji IFormFile i IFormFileCollection do przekazywania pliku:

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

Uwierzytelnione żądania przekazywania plików są obsługiwane przy użyciu nagłówka autoryzacji, certyfikatu klienta lub nagłówka cookie .

Nie ma wbudowanej obsługi antyforgery w ASP.NET Core 7.0. Antiforgery jest dostępny w ASP.NET Core 8.0 lub nowszym. Można go jednak zaimplementować przy użyciu IAntiforgery usługi.

Wiązanie tablic i wartości ciągów z nagłówków i ciągów zapytania

Poniższy kod demonstruje powiązania ciągów zapytania z tablicą typów pierwotnych, tablic ciągów i 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]}");

Powiązanie ciągów zapytania lub wartości nagłówków z tablicą typów złożonych jest obsługiwane, gdy typ został TryParse zaimplementowany. Poniższy kod wiąże się z tablicą ciągów i zwraca wszystkie elementy z określonymi tagami:

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

Poniższy kod przedstawia model i wymaganą TryParse implementację:

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

Poniższy kod wiąże się z tablicą int :

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

Aby przetestować poprzedni kod, dodaj następujący punkt końcowy, aby wypełnić bazę danych elementami Todo :

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

    return Results.Ok(todos);
});

Użyj narzędzia do testowania interfejsu API, takiego jak HttpRepl przekazywanie następujących danych do poprzedniego punktu końcowego:

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

Poniższy kod wiąże się z kluczem X-Todo-Id nagłówka i zwraca Todo elementy z pasującymi Id wartościami:

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

Uwaga

Podczas tworzenia powiązania string[] elementu z ciągu zapytania brak pasującej wartości ciągu zapytania spowoduje, że zamiast wartości null zostanie pusta tablica.

Powiązanie parametrów dla list argumentów za pomocą parametrów [AsParameters]

AsParametersAttribute Umożliwia proste powiązanie parametrów z typami, a nie złożonymi lub cyklicznych powiązań modelu.

Spójrzmy na poniższy kod:

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.

Rozważ następujący GET punkt końcowy:

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

struct Następujące elementy mogą służyć do zastępowania poprzednich wyróżnionych parametrów:

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

Refaktoryzowany GET punkt końcowy używa powyższego struct atrybutu AsParameters :

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

Poniższy kod przedstawia dodatkowe punkty końcowe w aplikacji:

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

Następujące klasy służą do refaktoryzacji list parametrów:

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

Poniższy kod przedstawia refaktoryzowane punkty końcowe przy użyciu poleceń AsParameters oraz poprzednie struct klasy i :

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

Zastąp powyższe parametry następującymi record typami:

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

Użycie elementu z AsParameters funkcją struct może być bardziej wydajne niż użycie record typu.

Kompletny przykładowy kod w repozytorium AspNetCore.Docs.Samples .

Wiązanie niestandardowe

Istnieją dwa sposoby dostosowywania powiązania parametrów:

  1. W przypadku źródeł tras, zapytań i powiązań nagłówków powiąż typy niestandardowe, dodając metodę statyczną TryParse dla typu.
  2. Kontrolowanie procesu wiązania przez zaimplementowanie BindAsync metody dla typu.

TryParse

TryParse ma dwa interfejsy API:

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

Poniższy kod jest wyświetlany Point: 12.3, 10.1 za pomocą identyfikatora URI /map?Point=12.3,10.1:

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

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

app.Run();

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

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

        point = null;
        return false;
    }
}

BindAsync

BindAsync ma następujące interfejsy API:

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

Poniższy kod jest wyświetlany SortBy:xyz, SortDirection:Desc, CurrentPage:99 za pomocą identyfikatora URI /products?SortBy=xyz&SortDir=Desc&Page=99:

using System.Reflection;

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

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

app.Run();

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

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

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

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

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

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

Błędy powiązań

Gdy powiązanie zakończy się niepowodzeniem, platforma rejestruje komunikat debugowania i zwraca różne kody stanu do klienta w zależności od trybu awarii.

Tryb awarii Typ parametru dopuszczalnego do wartości null Źródło powiązania Kod stanu
{ParameterType}.TryParse Zwraca false tak route/query/header 400
{ParameterType}.BindAsync Zwraca null tak niestandardowe 400
{ParameterType}.BindAsync Zgłasza nie ma znaczenia niestandardowe 500
Nie można wykonać deserializacji treści JSON nie ma znaczenia treść 400
Nieprawidłowy typ zawartości (nie application/json) nie ma znaczenia treść 415

Pierwszeństwo powiązania

Reguły określania źródła powiązania z parametru:

  1. Jawny atrybut zdefiniowany w parametrze (atrybuty From*) w następującej kolejności:
    1. Wartości tras: [FromRoute]
    2. Ciąg zapytania: [FromQuery]
    3. Nagłówek: [FromHeader]
    4. Ciało: [FromBody]
    5. Usługa: [FromServices]
    6. Wartości parametrów: [AsParameters]
  2. Typy specjalne
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormFileCollection (HttpContext.Request.Form.Files)
    7. IFormFile (HttpContext.Request.Form.Files[paramName])
    8. Stream (HttpContext.Request.Body)
    9. PipeReader (HttpContext.Request.BodyReader)
  3. Typ parametru ma prawidłową metodę statyczną BindAsync .
  4. Typ parametru jest ciągiem lub ma prawidłową metodę statyczną TryParse .
    1. Jeśli nazwa parametru istnieje w szablonie trasy. W elemecie app.Map("/todo/{id}", (int id) => {});id jest powiązana z trasą.
    2. Powiązana z ciągu zapytania.
  5. Jeśli typ parametru jest usługą dostarczaną przez iniekcję zależności, używa tej usługi jako źródła.
  6. Parametr pochodzi z treści.

Konfigurowanie opcji deserializacji JSON dla powiązania treści

Źródło powiązania treści używa System.Text.Json do deserializacji. Nie można zmienić tej wartości domyślnej, ale można skonfigurować opcje serializacji i deserializacji JSON.

Globalne konfigurowanie opcji deserializacji JSON

Opcje stosowane globalnie dla aplikacji można skonfigurować, wywołując ConfigureHttpJsonOptionsmetodę . Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych JSON.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

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

Ponieważ przykładowy kod konfiguruje zarówno serializacji, jak i deserializacji, może odczytywać NameField i uwzględniać NameField dane wyjściowe w formacie JSON.

Konfigurowanie opcji deserializacji JSON dla punktu końcowego

ReadFromJsonAsync ma przeciążenia, które akceptują JsonSerializerOptions obiekt. Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych JSON.

using System.Text.Json;

var app = WebApplication.Create();

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

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

app.Run();

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

Ponieważ powyższy kod stosuje dostosowane opcje tylko do deserializacji, dane wyjściowe JSON wykluczają NameFieldwartość .

Odczytywanie treści żądania

Odczytywanie treści żądania bezpośrednio przy użyciu parametru HttpContext lub 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();

Powyższy kod ma następujące działanie:

  • Uzyskuje dostęp do treści żądania przy użyciu polecenia HttpRequest.BodyReader.
  • Kopiuje treść żądania do pliku lokalnego.