Sdílet prostřednictvím


Vazby parametrů v minimálních aplikacích API

Vazba parametru je proces převodu dat požadavku na parametry silného typu, které jsou vyjádřeny obslužnými rutinami tras. Zdroj vazby určuje, odkud jsou parametry vázány. Zdroje vazeb můžou být explicitní nebo odvozené na základě metody HTTP a typu parametru.

Podporované zdroje vazeb:

  • Hodnoty tras
  • Řetězec dotazu
  • Hlavička
  • Text (jako JSZAPNUTO)
  • Hodnoty formuláře
  • Služby poskytované injektáží závislostí
  • Vlastní

Následující GET obslužná rutina trasy používá některé z těchto zdrojů vazeb parametrů:

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

Následující tabulka ukazuje vztah mezi parametry použitými v předchozím příkladu a přidruženými zdroji vazeb.

Parametr Zdroj vazby
id hodnota trasy
page řetězec dotazu
customHeader záhlaví
service Poskytuje injektáž závislostí

Metody GETHTTP , HEAD, OPTIONSa DELETE nejsou implicitně vázány z těla. Chcete-li vytvořit vazbu z těla (jako JSON) pro tyto metody HTTP, vytvořte vazbu explicitně s [FromBody] použitím nebo čtením z objektu HttpRequest.

Následující příklad obslužné rutiny trasy POST používá pro parametr zdroj vazby těla (jako JSON):person

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parametry v předchozích příkladech jsou všechny vázány z dat požadavku automaticky. Abychom si ukázali pohodlí, které poskytuje vazba parametrů, následující obslužné rutiny tras ukazují, jak číst data požadavku přímo z požadavku:

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

    // ...
});

Explicitní vazba parametru

Atributy lze použít k explicitní deklaraci, kde jsou parametry vázány.

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 Zdroj vazby
id hodnota trasy s názvem id
page řetězec dotazu s názvem "p"
service Poskytuje injektáž závislostí
contentType header with the name "Content-Type"

Explicitní vazba z hodnot formuláře

Atribut [FromForm] sváže hodnoty formuláře:

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.

Alternativou je použití atributu [AsParameters] s vlastním typem, který má vlastnosti anotované pomocí [FromForm]. Například následující kód vytvoří vazbu z hodnot formuláře na vlastnosti struktury záznamu 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);

Další informace najdete v části o AsParameters dále v tomto článku.

Kompletní ukázkový kód je v úložišti AspNetCore.Docs.Samples .

Zabezpečení vazby z IFormFile a IFormFileCollection

Komplexní vazba formuláře je podporována pomocí IFormFile a IFormFileCollection použití :[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 vázané na požadavek zahrnují [FromForm] anti-forgery token. Anti-forgery token je ověřen při zpracování požadavku. Další informace naleznete v tématu Antiforgery s minimálními rozhraními API.

Další informace naleznete v tématu Vazby formuláře v minimálních rozhraních API.

Kompletní ukázkový kód je v úložišti AspNetCore.Docs.Samples .

Vazba parametru s injektáží závislostí

Vazba parametrů pro minimální rozhraní API vytvoří vazbu parametrů prostřednictvím injektáže závislostí, když je typ nakonfigurovaný jako služba. Atribut není nutné explicitně použít [FromServices] u parametru. V následujícím kódu vrátí obě akce čas:

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

Volitelné parametry

Parametry deklarované v obslužných rutinách tras se považují za povinné:

  • Pokud požadavek odpovídá trase, obslužná rutina trasy se spustí pouze v případě, že jsou v požadavku zadány všechny požadované parametry.
  • Při zadání všech požadovaných parametrů dojde k chybě.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
Identifikátor URI result
/products?pageNumber=3 Vráceno 3
/products BadHttpRequestException: Požadovaný parametr int pageNumber nebyl poskytnut z řetězce dotazu.
/products/1 Chyba HTTP 404, žádná odpovídající trasa

Pokud chcete nastavit pageNumber jako volitelný, definujte typ jako volitelný nebo zadejte výchozí hodnotu:

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();
Identifikátor URI result
/products?pageNumber=3 Vráceno 3
/products 1 vráceno
/products2 1 vráceno

Předchozí hodnota s možnou hodnotou null a výchozí hodnotou platí pro všechny zdroje:

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

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

app.Run();

Předchozí kód volá metodu s produktem null, pokud není odeslán žádný text požadavku.

POZNÁMKA: Pokud jsou zadána neplatná data a parametr má hodnotu null, obslužná rutina trasy se nespustí .

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

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

app.Run();
Identifikátor URI result
/products?pageNumber=3 3 vrácený
/products 1 vrácený
/products?pageNumber=two BadHttpRequestException: Nepodařilo se vytvořit vazbu parametru "Nullable<int> pageNumber" ze dvou.
/products/two Chyba HTTP 404, žádná odpovídající trasa

Další informace najdete v části Selhání vazeb.

Speciální typy

Následující typy jsou vázané bez explicitních atributů:

  • HttpContext: Kontext, který obsahuje všechny informace o aktuálním požadavku HTTP nebo odpovědi:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest a HttpResponse: Požadavek HTTP a odpověď HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Token zrušení přidružený k aktuálnímu požadavku HTTP:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: Uživatel přidružený k požadavku vázanému z HttpContext.User:

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

Vytvoření vazby textu požadavku jako nebo StreamPipeReader

Tělo požadavku může svázat jako Stream scénáře nebo PipeReader efektivně podporovat scénáře, kdy uživatel musí zpracovávat data a:

  • Uložte data do úložiště objektů blob nebo vytvořte frontu dat poskytovateli fronty.
  • Zpracování uložených dat pomocí pracovního procesu nebo cloudové funkce

Data můžou být například zařazená do fronty Azure Storage nebo uložená ve službě Azure Blob Storage.

Následující kód implementuje frontu na pozadí:

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

Následující kód vytvoří vazbu textu požadavku na 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);
});

Následující kód ukazuje úplný Program.cs soubor:

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();
  • Při čtení dat je stejný Stream objekt jako HttpRequest.Body.
  • Tělo požadavku se ve výchozím nastavení neuloží do vyrovnávací paměti. Po přečtení těla se nedá převinout zpět. Stream nejde číst vícekrát.
  • A Stream PipeReader nejsou použitelné mimo minimální obslužnou rutinu akce, protože podkladové vyrovnávací paměti budou uvolněny nebo znovu použity.

Nahrávání souborů pomocí IFormFile a IFormFileCollection

Následující kód používá IFormFile a IFormFileCollection nahrává soubor:

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

Žádosti o nahrání ověřených souborů se podporují pomocí autorizační hlavičky, klientského certifikátu nebo hlavičky cookie .

Vazba na formuláře pomocí IFormCollection, IFormFile a IFormFileCollection

Vazba z parametrů založených na formuláři pomocí IFormCollectionIFormFileIFormFileCollection a je podporována. Metadata OpenAPI se odvozuje pro parametry formuláře pro podporu integrace s uživatelským rozhraním Swagger.

Následující kód nahraje soubory pomocí odvozené vazby 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();

Upozornění: Při implementaci formulářů musí aplikace zabránit útokům XSRF/CSRF (Cross-Site Request Forgery). V předchozím kódu IAntiforgery se služba používá k prevenci útoků XSRF generováním a ověřením tokenu proti padělání:

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

Další informace o útocích XSRF najdete v tématu Antiforgery s minimálními rozhraními API.

Další informace naleznete v tématu Vazby formuláře v minimálních rozhraních API;

Vazba na kolekce a komplexní typy z formulářů

Vazba je podporovaná pro:

  • Kolekce, například Seznam a Slovník
  • Složité typy, například nebo TodoProject

Následující kód ukazuje:

  • Minimální koncový bod, který sváže vstup formuláře s více částmi na komplexní objekt.
  • Jak používat antigery služby pro podporu generování a ověřování anti-padělání tokenů.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

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

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

app.Run();

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

V předchozím kódu:

  • Cílový parametr musí být anotován atributem [FromForm] , aby se z parametrů, které by měly být načteny z JStěla ON.
  • Vazba ze složitých typů nebo typů kolekcí není podporována pro minimální rozhraní API kompilovaná pomocí generátoru delegáta požadavku.
  • Revize zobrazuje další skrytý vstup s názvem isCompleted a hodnotou false. Pokud je zaškrtávací políčko zaškrtnuté isCompleted při odeslání formuláře, hodnoty true a false jsou odeslány jako hodnoty. Pokud políčko není zaškrtnuté, odešle se jenom skrytá vstupní hodnota false . Proces vazby modelu ASP.NET Core čte při vytváření vazby na bool hodnotu pouze první hodnotu, která má za true následek zaškrtnutí políček a false nezaškrtnutá zaškrtávací políčka.

Příklad dat formuláře odeslaných do předchozího koncového bodu vypadá takto:

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

Vytvoření vazby polí a řetězcových hodnot ze záhlaví a řetězců dotazu

Následující kód ukazuje vazby řetězců dotazu na pole primitivních typů, řetězcových polí a 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]}");

Vazby řetězců dotazu nebo hodnot hlaviček na pole komplexních typů se podporují, když je TryParse typ implementovaný. Následující kód se sváže s řetězcovým polem a vrátí všechny položky se zadanými značkami:

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

Následující kód ukazuje model a požadovanou TryParse implementaci:

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

Následující kód se sváže s polem 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();
});

Pokud chcete otestovat předchozí kód, přidejte následující koncový bod pro naplnění databáze položkami 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);
});

Pomocí nástroje, jako HttpRepl je předání následujících dat do předchozího koncového bodu:

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

Následující kód vytvoří vazbu na klíč X-Todo-Id záhlaví a vrátí Todo položky s odpovídajícími Id hodnotami:

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

Poznámka:

Když vytvoříte vazbu string[] z řetězce dotazu, absence odpovídající hodnoty řetězce dotazu bude mít místo hodnoty null prázdné pole.

Vazba parametrů pro seznamy argumentů s [AsParameters]

AsParametersAttribute umožňuje jednoduchou vazbu parametrů na typy, nikoli komplexní nebo rekurzivní vazbu modelu.

Uvažujte následující kód:

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.

Zvažte následující GET koncový bod:

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

K nahrazení předchozích zvýrazněných parametrů můžete použít následující struct :

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

Refaktorovaný GET koncový bod používá předchozí struct s atributem 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());

Následující kód ukazuje další koncové body v aplikaci:

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

K refaktoringu seznamů parametrů se používají následující třídy:

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

Následující kód ukazuje refaktorované koncové body používající AsParameters a předchozí struct a třídy:

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

K nahrazení předchozích parametrů je možné použít následující record typy:

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

Použití funkce with struct AsParameters může být výkonnější než použití record typu.

Kompletní ukázkový kód v úložišti AspNetCore.Docs.Samples .

Vlastní vazba

Vazby parametrů můžete přizpůsobit dvěma způsoby:

  1. Pro zdroje vazeb tras, dotazů a hlaviček vytvořte vazbu vlastních typů přidáním statické TryParse metody pro tento typ.
  2. Proces vazby můžete řídit implementací BindAsync metody typu.

TryParse

TryParse má dvě rozhraní API:

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

Následující kód se zobrazí Point: 12.3, 10.1 s identifikátorem 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 má následující rozhraní API:

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

Následující kód se zobrazí SortBy:xyz, SortDirection:Desc, CurrentPage:99 s identifikátorem 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
}

Selhání vazeb

Pokud vazba selže, architektura zaznamená ladicí zprávu a v závislosti na režimu selhání vrátí klientovi různé stavové kódy.

Režim selhání Typ parametru s možnou hodnotou null Zdroj vazby Stavový kód
{ParameterType}.TryParse návraty false ano route/query/header 400
{ParameterType}.BindAsync návraty null ano vlastní 400
{ParameterType}.BindAsync hází Nezáleží vlastní 500
Selhání deserializace JStěla ON Nezáleží text 400
Nesprávný typ obsahu (ne application/json) Nezáleží text 415

Priorita vazby

Pravidla pro určení zdroje vazby z parametru:

  1. Explicitní atribut definovaný u atributů parametru (From*) v následujícím pořadí:
    1. Hodnoty tras: [FromRoute]
    2. Řetězec dotazu: [FromQuery]
    3. Záhlaví: [FromHeader]
    4. Tělo: [FromBody]
    5. Formulář: [FromForm]
    6. Služba: [FromServices]
    7. Hodnoty parametrů: [AsParameters]
  2. Speciální typy
    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 má platnou statickou BindAsync metodu.
  4. Typ parametru je řetězec nebo má platnou statickou TryParse metodu.
    1. Pokud například název parametru existuje v šabloně app.Map("/todo/{id}", (int id) => {});trasy, je svázaný s trasou.
    2. Vázáno z řetězce dotazu.
  5. Pokud je typ parametru služba poskytovaná injektáží závislostí, použije tuto službu jako zdroj.
  6. Parametr je z těla.

Konfigurace JSmožností deserializace ON pro vazbu těla

Zdroj vazby těla používá System.Text.Json k deserializaci. Toto výchozí nastavení není možné změnit, ale JSlze nakonfigurovat možnosti serializace a deserializace ON.

Globální konfigurace JSmožností deserializace ON

Možnosti, které platí globálně pro aplikaci, je možné nakonfigurovat vyvoláním ConfigureHttpJsonOptions. Následující příklad obsahuje veřejná pole a formáty JSON výstupu.

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

Vzhledem k tomu, že vzorový kód konfiguruje serializaci i deserializaci, může číst NameField a zahrnout NameField do výstupu JSON.

Konfigurace JSmožností deserializace ON pro koncový bod

ReadFromJsonAsync má přetížení, které přijímají JsonSerializerOptions objekt. Následující příklad obsahuje veřejná pole a formáty JSON výstupu.

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

Vzhledem k tomu, že předchozí kód aplikuje přizpůsobené možnosti pouze na deserializaci, výstup JSON vylučuje NameField.

Čtení textu požadavku

Text požadavku si můžete přečíst přímo pomocí parametru nebo parametru HttpContext 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();

Předchozí kód:

  • Přistupuje k textu požadavku pomocí HttpRequest.BodyReader.
  • Zkopíruje text požadavku do místního souboru.

Vazba parametru je proces převodu dat požadavku na parametry silného typu, které jsou vyjádřeny obslužnými rutinami tras. Zdroj vazby určuje, odkud jsou parametry vázány. Zdroje vazeb můžou být explicitní nebo odvozené na základě metody HTTP a typu parametru.

Podporované zdroje vazeb:

  • Hodnoty tras
  • Řetězec dotazu
  • Hlavička
  • Text (jako JSZAPNUTO)
  • Služby poskytované injektáží závislostí
  • Vlastní

Vazba z hodnot formulářů není nativně podporována v .NET 6 a 7.

Následující GET obslužná rutina trasy používá některé z těchto zdrojů vazeb parametrů:

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

Následující tabulka ukazuje vztah mezi parametry použitými v předchozím příkladu a přidruženými zdroji vazeb.

Parametr Zdroj vazby
id hodnota trasy
page řetězec dotazu
customHeader záhlaví
service Poskytuje injektáž závislostí

Metody GETHTTP , HEAD, OPTIONSa DELETE nejsou implicitně vázány z těla. Chcete-li vytvořit vazbu z těla (jako JSON) pro tyto metody HTTP, vytvořte vazbu explicitně s [FromBody] použitím nebo čtením z objektu HttpRequest.

Následující příklad obslužné rutiny trasy POST používá pro parametr zdroj vazby těla (jako JSON):person

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parametry v předchozích příkladech jsou všechny vázány z dat požadavku automaticky. Abychom si ukázali pohodlí, které poskytuje vazba parametrů, následující obslužné rutiny tras ukazují, jak číst data požadavku přímo z požadavku:

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

    // ...
});

Explicitní vazba parametru

Atributy lze použít k explicitní deklaraci, kde jsou parametry vázány.

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 Zdroj vazby
id hodnota trasy s názvem id
page řetězec dotazu s názvem "p"
service Poskytuje injektáž závislostí
contentType header with the name "Content-Type"

Poznámka:

Vazba z hodnot formulářů není nativně podporována v .NET 6 a 7.

Vazba parametru s injektáží závislostí

Vazba parametrů pro minimální rozhraní API vytvoří vazbu parametrů prostřednictvím injektáže závislostí, když je typ nakonfigurovaný jako služba. Atribut není nutné explicitně použít [FromServices] u parametru. V následujícím kódu vrátí obě akce čas:

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

Volitelné parametry

Parametry deklarované v obslužných rutinách tras se považují za povinné:

  • Pokud požadavek odpovídá trase, obslužná rutina trasy se spustí pouze v případě, že jsou v požadavku zadány všechny požadované parametry.
  • Při zadání všech požadovaných parametrů dojde k chybě.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
Identifikátor URI result
/products?pageNumber=3 Vráceno 3
/products BadHttpRequestException: Požadovaný parametr int pageNumber nebyl z řetězce dotazu zadaný.
/products/1 Chyba HTTP 404, žádná odpovídající trasa

Pokud chcete nastavit pageNumber jako volitelný, definujte typ jako volitelný nebo zadejte výchozí hodnotu:

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();
Identifikátor URI result
/products?pageNumber=3 Vráceno 3
/products 1 vráceno
/products2 1 vráceno

Předchozí hodnota s možnou hodnotou null a výchozí hodnotou platí pro všechny zdroje:

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

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

app.Run();

Předchozí kód volá metodu s produktem null, pokud není odeslán žádný text požadavku.

POZNÁMKA: Pokud jsou zadána neplatná data a parametr má hodnotu null, obslužná rutina trasy se nespustí .

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

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

app.Run();
Identifikátor URI result
/products?pageNumber=3 3 vrácený
/products 1 vrácený
/products?pageNumber=two BadHttpRequestException: Nepodařilo se vytvořit vazbu parametru "Nullable<int> pageNumber" ze dvou.
/products/two Chyba HTTP 404, žádná odpovídající trasa

Další informace najdete v části Selhání vazeb.

Speciální typy

Následující typy jsou vázané bez explicitních atributů:

  • HttpContext: Kontext, který obsahuje všechny informace o aktuálním požadavku HTTP nebo odpovědi:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest a HttpResponse: Požadavek HTTP a odpověď HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Token zrušení přidružený k aktuálnímu požadavku HTTP:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: Uživatel přidružený k požadavku vázanému z HttpContext.User:

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

Vytvoření vazby textu požadavku jako nebo StreamPipeReader

Tělo požadavku může svázat jako Stream scénáře nebo PipeReader efektivně podporovat scénáře, kdy uživatel musí zpracovávat data a:

  • Uložte data do úložiště objektů blob nebo vytvořte frontu dat poskytovateli fronty.
  • Zpracování uložených dat pomocí pracovního procesu nebo cloudové funkce

Data můžou být například zařazená do fronty Azure Storage nebo uložená ve službě Azure Blob Storage.

Následující kód implementuje frontu na pozadí:

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

Následující kód vytvoří vazbu textu požadavku na 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);
});

Následující kód ukazuje úplný Program.cs soubor:

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();
  • Při čtení dat je stejný Stream objekt jako HttpRequest.Body.
  • Tělo požadavku se ve výchozím nastavení neuloží do vyrovnávací paměti. Po přečtení těla se nedá převinout zpět. Stream nejde číst vícekrát.
  • A Stream PipeReader nejsou použitelné mimo minimální obslužnou rutinu akce, protože podkladové vyrovnávací paměti budou uvolněny nebo znovu použity.

Nahrávání souborů pomocí IFormFile a IFormFileCollection

Následující kód používá IFormFile a IFormFileCollection nahrává soubor:

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

Žádosti o nahrání ověřených souborů se podporují pomocí autorizační hlavičky, klientského certifikátu nebo hlavičky cookie .

V systému ASP.NET Core 7.0 neexistuje integrovaná podpora antiforgery . Antiforgery je k dispozici v systému ASP.NET Core 8.0 a novějším. Dá se ale implementovat pomocí IAntiforgery služby.

Vytvoření vazby polí a řetězcových hodnot ze záhlaví a řetězců dotazu

Následující kód ukazuje vazby řetězců dotazu na pole primitivních typů, řetězcových polí a 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]}");

Vazby řetězců dotazu nebo hodnot hlaviček na pole komplexních typů se podporují, když je TryParse typ implementovaný. Následující kód se sváže s řetězcovým polem a vrátí všechny položky se zadanými značkami:

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

Následující kód ukazuje model a požadovanou TryParse implementaci:

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

Následující kód se sváže s polem 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();
});

Pokud chcete otestovat předchozí kód, přidejte následující koncový bod pro naplnění databáze položkami 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);
});

Použijte testovací nástroj rozhraní API, jako HttpRepl je předání následujících dat do předchozího koncového bodu:

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

Následující kód vytvoří vazbu na klíč X-Todo-Id záhlaví a vrátí Todo položky s odpovídajícími Id hodnotami:

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

Poznámka:

Když vytvoříte vazbu string[] z řetězce dotazu, absence odpovídající hodnoty řetězce dotazu bude mít místo hodnoty null prázdné pole.

Vazba parametrů pro seznamy argumentů s [AsParameters]

AsParametersAttribute umožňuje jednoduchou vazbu parametrů na typy, nikoli komplexní nebo rekurzivní vazbu modelu.

Uvažujte následující kód:

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.

Zvažte následující GET koncový bod:

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

K nahrazení předchozích zvýrazněných parametrů můžete použít následující struct :

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

Refaktorovaný GET koncový bod používá předchozí struct s atributem 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());

Následující kód ukazuje další koncové body v aplikaci:

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

K refaktoringu seznamů parametrů se používají následující třídy:

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

Následující kód ukazuje refaktorované koncové body používající AsParameters a předchozí struct a třídy:

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

K nahrazení předchozích parametrů je možné použít následující record typy:

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

Použití funkce with struct AsParameters může být výkonnější než použití record typu.

Kompletní ukázkový kód v úložišti AspNetCore.Docs.Samples .

Vlastní vazba

Vazby parametrů můžete přizpůsobit dvěma způsoby:

  1. Pro zdroje vazeb tras, dotazů a hlaviček vytvořte vazbu vlastních typů přidáním statické TryParse metody pro tento typ.
  2. Proces vazby můžete řídit implementací BindAsync metody typu.

TryParse

TryParse má dvě rozhraní API:

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

Následující kód se zobrazí Point: 12.3, 10.1 s identifikátorem 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 má následující rozhraní API:

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

Následující kód se zobrazí SortBy:xyz, SortDirection:Desc, CurrentPage:99 s identifikátorem 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
}

Selhání vazeb

Pokud vazba selže, architektura zaznamená ladicí zprávu a v závislosti na režimu selhání vrátí klientovi různé stavové kódy.

Režim selhání Typ parametru s možnou hodnotou null Zdroj vazby Stavový kód
{ParameterType}.TryParse návraty false ano route/query/header 400
{ParameterType}.BindAsync návraty null ano vlastní 400
{ParameterType}.BindAsync hází nezáleží na tom, vlastní 500
Selhání deserializace JStěla ON nezáleží na tom, text 400
Nesprávný typ obsahu (ne application/json) nezáleží na tom, text 415

Priorita vazby

Pravidla pro určení zdroje vazby z parametru:

  1. Explicitní atribut definovaný u atributů parametru (From*) v následujícím pořadí:
    1. Hodnoty tras: [FromRoute]
    2. Řetězec dotazu: [FromQuery]
    3. Záhlaví: [FromHeader]
    4. Tělo: [FromBody]
    5. Služba: [FromServices]
    6. Hodnoty parametrů: [AsParameters]
  2. Speciální typy
    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 má platnou statickou BindAsync metodu.
  4. Typ parametru je řetězec nebo má platnou statickou TryParse metodu.
    1. Pokud název parametru existuje v šabloně trasy, například app.Map("/todo/{id}", (int id) => {});, je svázaný s trasou.
    2. Vázáno z řetězce dotazu.
  5. Pokud je typ parametru služba poskytovaná injektáží závislostí, použije tuto službu jako zdroj.
  6. Parametr je z těla.

Konfigurace JSmožností deserializace ON pro vazbu těla

Zdroj vazby těla používá System.Text.Json k deserializaci. Toto výchozí nastavení není možné změnit, ale JSlze nakonfigurovat možnosti serializace a deserializace ON.

Globální konfigurace JSmožností deserializace ON

Možnosti, které platí globálně pro aplikaci, je možné nakonfigurovat vyvoláním ConfigureHttpJsonOptions. Následující příklad obsahuje veřejná pole a formáty JSON výstupu.

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

Vzhledem k tomu, že vzorový kód konfiguruje serializaci i deserializaci, může číst NameField a zahrnout NameField do výstupu JSON.

Konfigurace JSmožností deserializace ON pro koncový bod

ReadFromJsonAsync má přetížení, které přijímají JsonSerializerOptions objekt. Následující příklad obsahuje veřejná pole a formáty JSON výstupu.

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

Vzhledem k tomu, že předchozí kód aplikuje přizpůsobené možnosti pouze na deserializaci, výstup JSON vylučuje NameField.

Čtení textu požadavku

Text požadavku si můžete přečíst přímo pomocí parametru nebo parametru HttpContext 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();

Předchozí kód:

  • Přistupuje k textu požadavku pomocí HttpRequest.BodyReader.
  • Zkopíruje text požadavku do místního souboru.