Dela via


Parameterbindning i minimala API-appar

Anmärkning

Det här är inte den senaste versionen av den här artikeln. För den nuvarande utgåvan, se .NET 9-versionen av den här artikeln .

Varning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i supportpolicyn för .NET och .NET Core. För den nuvarande utgåvan, se .NET 9-versionen av den här artikeln .

Viktigt!

Den här informationen gäller en förhandsversionsprodukt som kan ändras avsevärt innan den släpps kommersiellt. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, med avseende på den information som tillhandahålls här.

För den nuvarande utgåvan, se .NET 9-versionen av den här artikeln .

Parameterbindning är processen att konvertera begärandedata till starkt skrivna parametrar som uttrycks av routningshanterare. En bindningskälla avgör var parametrarna är bundna från. Bindningskällor kan vara explicita eller härledda baserat på HTTP-metod och parametertyp.

Bindningskällor som stöds:

  • Vägvärden
  • Frågesträng
  • Rubrik
  • Kropp (som JSON)
  • Formulärvärden
  • Tjänster som tillhandahålls av dependency injection
  • Skräddarsydd

Följande GET routningshanterare använder några av dessa parameterbindningskällor:

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

I följande tabell visas relationen mellan de parametrar som används i föregående exempel och de associerade bindningskällorna.

Parameter Bindningskälla
id ruttvärde
page Frågesträngen
customHeader rubrik
service Tillhandahålls av beroendeinjektion

HTTP-metoderna GET, HEAD, OPTIONSoch DELETE binder inte implicit från brödtexten. Om du vill binda från brödtexten (som JSON) för dessa HTTP-metoder du uttryckligen binda med [FromBody] eller läsa från HttpRequest.

I följande exempel använder POST-routningshanteraren en bindningskälla för brödtext (som JSON) för parametern person:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parametrarna i föregående exempel är alla bundna från begärandedata automatiskt. För att demonstrera bekvämligheten med parameterbindningen visar följande routningshanterare hur du läser begärandedata direkt från begäran:

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

    // ...
});

Explicit parameterbindning

Attribut kan användas för att explicit deklarera var parametrarna är bundna från.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
Parameter Bindningskälla
id routningsvärde med namnet id
page frågesträng med namnet "p"
service Tillhandahålls av beroendeinjektion
contentType rubrik med namnet "Content-Type"

Explicit bindning från formulärvärden

Det [FromForm] attributet binder formulärvärden:

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.

Ett alternativ är att använda attributet [AsParameters] med en anpassad typ som har egenskaper som kommenterats med [FromForm]. Följande kod binder, till exempel, från formulärvärden till egenskaper för NewTodoRequest record struct:

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

Mer information finns i avsnittet om AsParameters senare i den här artikeln.

Den fullständiga exempelkoden finns på lagringsplatsen AspNetCore.Docs.Samples.

Säker bindning från IFormFile och IFormFileCollection

Komplex formulärbindning stöds med hjälp av IFormFile och IFormFileCollection med hjälp av [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();

Parametrar som är kopplade till begäran med [FromForm] inkluderar en antiförfalsknings-token. Antiforgery-token verifieras när begäran bearbetas. För mer information, se Förfalskningsskydd med minimala API:er.

Mer information finns i Formulärbindning i minimala API:er.

Den fullständiga exempelkoden finns på lagringsplatsen AspNetCore.Docs.Samples.

Parameterbindning med beroendeinmatning

Parameterbindning för minimala API:er binder parametrar via beroendeinmatning när typen konfigureras som en tjänst. Det är inte nödvändigt att uttryckligen tillämpa attributet [FromServices] på en parameter. I följande kod returnerar båda åtgärderna tiden:

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

Valfria parametrar

Parametrar som deklareras i routningshanterare behandlas efter behov:

  • Om en begäran matchar vägen körs routningshanteraren endast om alla obligatoriska parametrar anges i begäran.
  • Om du inte anger alla obligatoriska parametrar resulterar det i ett fel.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
URI (Uniform Resource Identifier) resultat
/products?pageNumber=3 3 returnerade
/products BadHttpRequestException: Den obligatoriska parametern "int pageNumber" angavs inte från frågesträngen.
/products/1 HTTP 404-fel, ingen matchande väg

Om du vill göra pageNumber valfri definierar du typen som valfri eller anger ett standardvärde:

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

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

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI (Uniform Resource Identifier) resultat
/products?pageNumber=3 3 returnerade
/products 1 returnerade
/products2 1 returnerade

Föregående null- och standardvärde gäller för alla källor:

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

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

app.Run();

Föregående kod anropar metoden med en null-produkt om ingen begärandetext skickas.

NOTE: Om ogiltiga data anges och parametern är null kan routningshanteraren inte köras.

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

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

app.Run();
URI (Uniform Resource Identifier) resultat
/products?pageNumber=3 3 har returnerats
/products 1 har returnerats
/products?pageNumber=two BadHttpRequestException: Det gick inte att binda parametern "Nullable<int> pageNumber" från "två".
/products/two HTTP 404-fel, ingen matchande väg

För mer information, se avsnittet bindningsfel.

Särskilda typer

Följande typer är bundna utan explicita attribut:

  • HttpContext: Kontexten som innehåller all information om den aktuella HTTP-begäran eller -svaret:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest och HttpResponse: HTTP-begäran och HTTP-svar:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Den annulleringstoken som är associerad med den aktuella HTTP-begäran:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: Användaren som är associerad med begäran, bunden från HttpContext.User:

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

Binda begärandetexten som en Stream eller PipeReader

Begärandetexten kan bindas som en Stream eller PipeReader för att effektivt stödja scenarier där användaren måste bearbeta data och:

  • Lagra data i bloblagring eller köa data till en köleverantör.
  • Bearbeta lagrade data med en arbetsprocess eller molnfunktion.

Data kan till exempel läggas i kö för lagring i Azure Queue Storage eller lagras i Azure Blob Storage.

Följande kod implementerar en bakgrundskö:

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

Följande kod binder begärandetexten till en 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);
});

Följande kod visar den fullständiga Program.cs filen:

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();
  • När du läser data är Stream samma objekt som HttpRequest.Body.
  • Begärandetexten buffrades inte som standard. När brödtexten har lästs kan den inte spolas tillbaka. Dataströmmen kan inte läsas flera gånger.
  • Stream och PipeReader kan inte användas utanför den minimala åtgärdshanteraren eftersom de underliggande buffertarna tas bort eller återanvänds.

Filuppladdningar med IFormFile och IFormFileCollection

Filuppladdningar med IFormFile och IFormFileCollection i minimala API:er kräver multipart/form-data-kodning. Parameternamnet i routningshanteraren måste matcha formulärfältets namn i begäran. Minimala API:er stöder inte bindning av hela begärandetexten direkt till en IFormFile parameter utan formulärkodning.

Om du behöver binda hela begärandetexten, till exempel när du arbetar med JSON, binära data eller andra innehållstyper, se:

Följande kod använder IFormFile och IFormFileCollection för att ladda upp filen:

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

Autentiserade filuppladdningsbegäranden stöds med hjälp av ett auktoriseringshuvud, ett klientcertifikateller ett cookie-huvud.

Binda till formulär med IFormCollection, IFormFile och IFormFileCollection

Bindning från formulärbaserade parametrar med hjälp av IFormCollection, IFormFileoch IFormFileCollection stöds. OpenAPI- metadata härleds automatiskt för formulärparametrar för att stödja integrering med Swagger UI.

Följande kod laddar upp filer med hjälp av härledd bindning från typen IFormFile.

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

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

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

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

Varning: När formulär implementeras måste appen förhindraXSRF-attacker (Cross-Site Request Forgery). I föregående kod används IAntiforgery-tjänsten för att förhindra XSRF-attacker genom att generera och verifiera en antiforgery-token:

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

Mer information om XSRF-attacker finns i Antiforgery med minimala API:er

Mer information finns i Formulärbindning i minimala API:er;

Binda till samlingar och komplexa typer från formulär

Stöd för bindning gäller:

  • Samlingar, till exempel List och Dictionary
  • Komplexa typer, till exempel Todo eller Project

Följande kod visar:

  • En minimal slutpunkt som binder en formulärinmatning i flera delar till ett komplext objekt.
  • Hur man använder antiforgery-tjänsterna för att stödja generering och validering av antiforgery-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 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));
}

I koden ovan:

  • Målparametern måste kommenteras med attributet [FromForm] för att skilja sig från parametrar som ska läsas från JSON-brödtexten.
  • Bindning från komplexa typer eller samlingstyper stöds inte för minimala API:er som kompileras med generatorn för begärandedelegat.
  • Markeringen visar ytterligare dolda indata med namnet isCompleted och värdet false. Om kryssrutan isCompleted markeras när formuläret skickas skickas både värden true och false som värden. Om kryssrutan är avmarkerad skickas endast det dolda indatavärdet false. Den ASP.NET Core-modellbindningsprocessen läser bara det första värdet vid bindning till ett bool värde, vilket resulterar i true för markerade kryssrutor och false för avmarkerade kryssrutor.

Ett exempel på formulärdata som skickas till föregående slutpunkt ser ut så här:

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

Binda matriser och strängvärden från rubriker och frågesträngar

Följande kod visar bindning av frågesträngar till en matris med primitiva typer, strängmatriser och 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]}");

Bindning av frågesträngar eller sidhuvudvärden till en matris med komplexa typer stöds när typen har TryParse implementerats. Följande kod binder till en strängmatris och returnerar alla objekt med de angivna taggarna:

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

Följande kod visar modellen och den nödvändiga TryParse implementeringen:

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

Följande kod binder till en int matris:

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

Om du vill testa koden ovan lägger du till följande slutpunkt för att fylla databasen med Todo objekt:

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

    return Results.Ok(todos);
});

Använd ett verktyg som HttpRepl för att skicka följande data till föregående slutpunkt:

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

Följande kod binder till huvudnyckeln X-Todo-Id och returnerar Todo objekt med matchande Id värden:

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

Anmärkning

När du binder en string[] från en frågesträng resulterar frånvaron av ett matchande frågesträngsvärde i en tom matris i stället för ett null-värde.

Parameterbindning för argumentlistor med [AsParameters]

AsParametersAttribute möjliggör enkel parameterbindning till typer och inte komplex eller rekursiv modellbindning.

Överväg följande 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.

Överväg följande GET slutpunkt:

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

Följande struct kan användas för att ersätta de föregående markerade parametrarna:

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

Den omstrukturerade GET-slutpunkten använder föregående struct med attributet 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());

Följande kod visar ytterligare slutpunkter i appen:

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

Följande klasser används för att omstrukturera parameterlistorna:

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

Följande kod visar de refaktorerade slutpunkterna med hjälp av AsParameters samt de tidigare struct och klasser.

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

Följande record typer kan användas för att ersätta de föregående parametrarna:

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

Att använda en struct med AsParameters kan vara mer högpresterande än att använda en record typ.

Den kompletta exempelkoden i repositoryn AspNetCore.Docs.Samples.

Anpassad bindning

Det finns tre sätt att anpassa parameterbindning:

  1. För routnings-, fråge- och rubrikbindningskällor binder du anpassade typer genom att lägga till en statisk TryParse metod för typen.
  2. Kontrollera bindningsprocessen genom att implementera en BindAsync-metod för en typ.
  3. För avancerade scenarier implementerar du IBindableFromHttpContext<TSelf> gränssnittet för att tillhandahålla anpassad bindningslogik direkt från HttpContext.

TryParse

TryParse har två API:er:

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

Följande kod visar Point: 12.3, 10.1 med 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 har följande API:er:

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

Följande kod visar SortBy:xyz, SortDirection:Desc, CurrentPage:99 med 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
}

Anpassad parameterbindning med IBindableFromHttpContext

ASP.NET Core har stöd för anpassad parameterbindning i Minimala API:er med hjälp av IBindableFromHttpContext<TSelf> gränssnittet. Med det här gränssnittet, som introducerades med C# 11:s statiska abstrakta medlemmar, kan du skapa typer som kan bindas från en HTTP-kontext direkt i routningshanterarparametrarna.

public interface IBindableFromHttpContext<TSelf>
    where TSelf : class, IBindableFromHttpContext<TSelf>
{
    static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}

Genom att implementera IBindableFromHttpContext<TSelf>kan du skapa anpassade typer som hanterar sin egen bindningslogik från HttpContext. När en routningshanterare innehåller en parameter av den här typen anropar ramverket automatiskt den statiska BindAsync metoden för att skapa instansen:

using CustomBindingExample;

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

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, IBindableFromHttpContext example!");

app.MapGet("/custom-binding", (CustomBoundParameter param) =>
{
    return $"Value from custom binding: {param.Value}";
});

app.MapGet("/combined/{id}", (int id, CustomBoundParameter param) =>
{
    return $"ID: {id}, Custom Value: {param.Value}";
});

Följande är ett exempel på implementering av en anpassad parameter som binder från ett HTTP-huvud:

using System.Reflection;

namespace CustomBindingExample;

public class CustomBoundParameter : IBindableFromHttpContext<CustomBoundParameter>
{
    public string Value { get; init; } = default!;

    public static ValueTask<CustomBoundParameter?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Custom binding logic here
        // This example reads from a custom header
        var value = context.Request.Headers["X-Custom-Header"].ToString();
        
        // If no header was provided, you could fall back to a query parameter
        if (string.IsNullOrEmpty(value))
        {
            value = context.Request.Query["customValue"].ToString();
        }
        
        return ValueTask.FromResult<CustomBoundParameter?>(new CustomBoundParameter 
        {
            Value = value
        });
    }
}

Du kan också implementera validering i din anpassade bindningslogik:

app.MapGet("/validated", (ValidatedParameter param) =>
{
    if (string.IsNullOrEmpty(param.Value))
    {
        return Results.BadRequest("Value cannot be empty");
    }
    
    return Results.Ok($"Validated value: {param.Value}");
});

Visa eller ladda ned exempelkoden (ladda ned)

Bindningsfel

När bindningen misslyckas loggar ramverket ett felsökningsmeddelande och returnerar olika statuskoder till klienten beroende på felläget.

Felläge Parametertyp som kan vara null Bindningskälla Statuskod
{ParameterType}.TryParse returnerar false Ja rutt/förfrågan/huvud 400
{ParameterType}.BindAsync returnerar null Ja anpassad 400
{ParameterType}.BindAsync kastar spelar ingen roll anpassad 500
Det gick inte att deserialisera JSON-data spelar ingen roll kropp 400
Fel innehållstyp (inte application/json) spelar ingen roll kropp 415

Bindningsprioritet

Reglerna för att fastställa en bindningskälla från en parameter:

  1. Den explicita attribut som definieras på parametern (From*-attribut) i följande ordning:
    1. Routvärden: [FromRoute]
    2. Frågesträng: [FromQuery]
    3. Rubrik: [FromHeader]
    4. Brödtext: [FromBody]
    5. Formulär: [FromForm]
    6. Tjänst: [FromServices]
    7. Parametervärden: [AsParameters]
  2. Särskilda typer
    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. Parametertypen har en giltig statisk BindAsync-metod.
  4. Parametertypen är en sträng eller har en giltig statisk TryParse-metod.
    1. Om parameternamnet finns i routemallen, till exempel app.Map("/todo/{id}", (int id) => {});, då binds det från routen.
    2. Bunden till frågesträngen.
  5. Om parametertypen är en tjänst som tillhandahålls av beroendeinmatning använder den tjänsten som källa.
  6. Parametern kommer från kroppen.

Konfigurera JSON-deserialiseringsalternativ för brödtextbindning

Kroppsbindningskällan använder System.Text.Json för deserialisering. Det är inte möjligt att ändra den här standardinställningen, men JSON-serialiserings- och deserialiseringsalternativ kan konfigureras.

Konfigurera JSON-deserialiseringsalternativ globalt

Alternativ som gäller globalt för en app kan konfigureras genom att anropa ConfigureHttpJsonOptions. Följande exempel innehåller offentliga fält och formaterar JSON-utdata.

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

Eftersom exempelkoden konfigurerar både serialisering och deserialisering kan den läsa NameField och inkludera NameField i utdata-JSON.

Konfigurera JSON-deserialiseringsalternativ för en slutpunkt

ReadFromJsonAsync har överbelastningar som accepterar ett JsonSerializerOptions-objekt. Följande exempel innehåller offentliga fält och formaterar JSON-utdata.

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

Eftersom föregående kod endast tillämpar de anpassade alternativen för deserialisering utesluter utdata-JSON NameField.

Läs begärandetexten

Läs begärandetexten direkt med hjälp av en HttpContext- eller HttpRequest-parameter:

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

Föregående kod:

Parameterbindning är processen att konvertera begärandedata till starkt skrivna parametrar som uttrycks av routningshanterare. En bindningskälla avgör var parametrarna är bundna från. Bindningskällor kan vara explicita eller härledda baserat på HTTP-metod och parametertyp.

Bindningskällor som stöds:

  • Vägvärden
  • Frågesträng
  • Rubrik
  • Kropp (som JSON)
  • Tjänster som tillhandahålls av dependency injection
  • Skräddarsydd

Bindning från formulärvärden är inte stöds internt i .NET 6 och 7.

Följande GET routningshanterare använder några av dessa parameterbindningskällor:

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

I följande tabell visas relationen mellan de parametrar som används i föregående exempel och de associerade bindningskällorna.

Parameter Bindningskälla
id ruttvärde
page Frågesträngen
customHeader rubrik
service Tillhandahålls av beroendeinjektion

HTTP-metoderna GET, HEAD, OPTIONSoch DELETE binder inte implicit från brödtexten. Om du vill binda från brödtexten (som JSON) för dessa HTTP-metoder du uttryckligen binda med [FromBody] eller läsa från HttpRequest.

I följande exempel använder POST-routningshanteraren en bindningskälla för brödtext (som JSON) för parametern person:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parametrarna i föregående exempel är alla bundna från begärandedata automatiskt. För att demonstrera bekvämligheten med parameterbindningen visar följande routningshanterare hur du läser begärandedata direkt från begäran:

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

    // ...
});

Explicit parameterbindning

Attribut kan användas för att explicit deklarera var parametrarna är bundna från.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
Parameter Bindningskälla
id routningsvärde med namnet id
page frågesträng med namnet "p"
service Tillhandahålls av beroendeinjektion
contentType rubrik med namnet "Content-Type"

Anmärkning

Bindning från formulärvärden är inte stöds internt i .NET 6 och 7.

Parameterbindning med beroendeinmatning

Parameterbindning för minimala API:er binder parametrar via beroendeinmatning när typen konfigureras som en tjänst. Det är inte nödvändigt att uttryckligen tillämpa attributet [FromServices] på en parameter. I följande kod returnerar båda åtgärderna tiden:

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

Valfria parametrar

Parametrar som deklareras i routningshanterare behandlas efter behov:

  • Om en begäran matchar vägen körs routningshanteraren endast om alla obligatoriska parametrar anges i begäran.
  • Om du inte anger alla obligatoriska parametrar resulterar det i ett fel.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
URI (Uniform Resource Identifier) resultat
/products?pageNumber=3 3 returnerade
/products BadHttpRequestException: Den obligatoriska parametern "int pageNumber" angavs inte från frågesträngen.
/products/1 HTTP 404-fel, ingen matchande väg

Om du vill göra pageNumber valfri definierar du typen som valfri eller anger ett standardvärde:

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

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

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI (Uniform Resource Identifier) resultat
/products?pageNumber=3 3 returnerade
/products 1 returnerade
/products2 1 returnerade

Föregående null- och standardvärde gäller för alla källor:

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

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

app.Run();

Föregående kod anropar metoden med en null-produkt om ingen begärandetext skickas.

NOTE: Om ogiltiga data anges och parametern är null kan routningshanteraren inte köras.

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

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

app.Run();
URI (Uniform Resource Identifier) resultat
/products?pageNumber=3 3 har returnerats
/products 1 har returnerats
/products?pageNumber=two BadHttpRequestException: Det gick inte att binda parametern "Nullable<int> pageNumber" från "två".
/products/two HTTP 404-fel, ingen matchande väg

För mer information, se avsnittet bindningsfel.

Särskilda typer

Följande typer är bundna utan explicita attribut:

  • HttpContext: Kontexten som innehåller all information om den aktuella HTTP-begäran eller -svaret:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest och HttpResponse: HTTP-begäran och HTTP-svar:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Den annulleringstoken som är associerad med den aktuella HTTP-begäran:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: Användaren som är associerad med begäran, bunden från HttpContext.User:

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

Binda begärandetexten som en Stream eller PipeReader

Begärandetexten kan bindas som en Stream eller PipeReader för att effektivt stödja scenarier där användaren måste bearbeta data och:

  • Lagra data i bloblagring eller köa data till en köleverantör.
  • Bearbeta lagrade data med en arbetsprocess eller molnfunktion.

Data kan till exempel läggas i kö för lagring i Azure Queue Storage eller lagras i Azure Blob Storage.

Följande kod implementerar en bakgrundskö:

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

Följande kod binder begärandetexten till en 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);
});

Följande kod visar den fullständiga Program.cs filen:

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();
  • När du läser data är Stream samma objekt som HttpRequest.Body.
  • Begärandetexten buffrades inte som standard. När brödtexten har lästs kan den inte spolas tillbaka. Dataströmmen kan inte läsas flera gånger.
  • Stream och PipeReader kan inte användas utanför den minimala åtgärdshanteraren eftersom de underliggande buffertarna tas bort eller återanvänds.

Filuppladdningar med IFormFile och IFormFileCollection

Följande kod använder IFormFile och IFormFileCollection för att ladda upp filen:

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

Autentiserade filuppladdningsbegäranden stöds med hjälp av ett auktoriseringshuvud, ett klientcertifikateller ett cookie-huvud.

Det finns inget inbyggt stöd för antiforgery i ASP.NET Core i .NET 7. Antiforgery finns i ASP.NET Core i .NET 8 eller senare. Den kan dock implementeras med hjälp av IAntiforgery-tjänsten.

Binda matriser och strängvärden från rubriker och frågesträngar

Följande kod visar bindning av frågesträngar till en matris med primitiva typer, strängmatriser och 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]}");

Bindning av frågesträngar eller sidhuvudvärden till en matris med komplexa typer stöds när typen har TryParse implementerats. Följande kod binder till en strängmatris och returnerar alla objekt med de angivna taggarna:

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

Följande kod visar modellen och den nödvändiga TryParse implementeringen:

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

Följande kod binder till en int matris:

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

Om du vill testa koden ovan lägger du till följande slutpunkt för att fylla databasen med Todo objekt:

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

    return Results.Ok(todos);
});

Använd ett API-testverktyg som HttpRepl för att skicka följande data till föregående slutpunkt:

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

Följande kod binder till huvudnyckeln X-Todo-Id och returnerar Todo objekt med matchande Id värden:

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

Anmärkning

När du binder en string[] från en frågesträng resulterar frånvaron av ett matchande frågesträngsvärde i en tom matris i stället för ett null-värde.

Parameterbindning för argumentlistor med [AsParameters]

AsParametersAttribute möjliggör enkel parameterbindning till typer och inte komplex eller rekursiv modellbindning.

Överväg följande 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.

Överväg följande GET slutpunkt:

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

Följande struct kan användas för att ersätta de föregående markerade parametrarna:

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

Den omstrukturerade GET-slutpunkten använder föregående struct med attributet 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());

Följande kod visar ytterligare slutpunkter i appen:

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

Följande klasser används för att omstrukturera parameterlistorna:

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

Följande kod visar de refaktorerade slutpunkterna med hjälp av AsParameters samt de tidigare struct och klasser.

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

Följande record typer kan användas för att ersätta de föregående parametrarna:

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

Att använda en struct med AsParameters kan vara mer högpresterande än att använda en record typ.

Den kompletta exempelkoden i repositoryn AspNetCore.Docs.Samples.

Anpassad bindning

Det finns tre sätt att anpassa parameterbindning:

  1. För routnings-, fråge- och rubrikbindningskällor binder du anpassade typer genom att lägga till en statisk TryParse metod för typen.
  2. Kontrollera bindningsprocessen genom att implementera en BindAsync-metod för en typ.
  3. För avancerade scenarier implementerar du IBindableFromHttpContext<TSelf> gränssnittet för att tillhandahålla anpassad bindningslogik direkt från HttpContext.

TryParse

TryParse har två API:er:

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

Följande kod visar Point: 12.3, 10.1 med 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 har följande API:er:

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

Följande kod visar SortBy:xyz, SortDirection:Desc, CurrentPage:99 med 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
}

Anpassad parameterbindning med IBindableFromHttpContext

ASP.NET Core har stöd för anpassad parameterbindning i Minimala API:er med hjälp av IBindableFromHttpContext<TSelf> gränssnittet. Med det här gränssnittet, som introducerades med C# 11:s statiska abstrakta medlemmar, kan du skapa typer som kan bindas från en HTTP-kontext direkt i routningshanterarparametrarna.

public interface IBindableFromHttpContext<TSelf>
    where TSelf : class, IBindableFromHttpContext<TSelf>
{
    static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}

Genom att implementera IBindableFromHttpContext<TSelf> gränssnittet kan du skapa anpassade typer som hanterar sin egen bindningslogik från HttpContext. När en routningshanterare innehåller en parameter av den här typen anropar ramverket automatiskt den statiska BindAsync-metoden för att skapa instansen:

using CustomBindingExample;

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

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, IBindableFromHttpContext example!");

app.MapGet("/custom-binding", (CustomBoundParameter param) =>
{
    return $"Value from custom binding: {param.Value}";
});

app.MapGet("/combined/{id}", (int id, CustomBoundParameter param) =>
{
    return $"ID: {id}, Custom Value: {param.Value}";
});

Följande är ett exempel på implementering av en anpassad parameter som binder från ett HTTP-huvud:

using System.Reflection;

namespace CustomBindingExample;

public class CustomBoundParameter : IBindableFromHttpContext<CustomBoundParameter>
{
    public string Value { get; init; } = default!;

    public static ValueTask<CustomBoundParameter?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Custom binding logic here
        // This example reads from a custom header
        var value = context.Request.Headers["X-Custom-Header"].ToString();
        
        // If no header was provided, you could fall back to a query parameter
        if (string.IsNullOrEmpty(value))
        {
            value = context.Request.Query["customValue"].ToString();
        }
        
        return ValueTask.FromResult<CustomBoundParameter?>(new CustomBoundParameter 
        {
            Value = value
        });
    }
}

Du kan också implementera validering i din anpassade bindningslogik:

app.MapGet("/validated", (ValidatedParameter param) =>
{
    if (string.IsNullOrEmpty(param.Value))
    {
        return Results.BadRequest("Value cannot be empty");
    }
    
    return Results.Ok($"Validated value: {param.Value}");
});

Visa eller ladda ned exempelkoden (ladda ned)

Bindningsfel

När bindningen misslyckas loggar ramverket ett felsökningsmeddelande och returnerar olika statuskoder till klienten beroende på felläget.

Felläge Parametertyp som kan vara null Bindningskälla Statuskod
{ParameterType}.TryParse returnerar false Ja rutt/förfrågan/huvud 400
{ParameterType}.BindAsync returnerar null Ja anpassad 400
{ParameterType}.BindAsync kastar spelar ingen roll anpassad 500
Det gick inte att deserialisera JSON-data spelar ingen roll kropp 400
Fel innehållstyp (inte application/json) spelar ingen roll kropp 415

Bindningsprioritet

Reglerna för att fastställa en bindningskälla från en parameter:

  1. Den explicita attribut som definieras på parametern (From*-attribut) i följande ordning:
    1. Routvärden: [FromRoute]
    2. Frågesträng: [FromQuery]
    3. Rubrik: [FromHeader]
    4. Brödtext: [FromBody]
    5. Tjänst: [FromServices]
    6. Parametervärden: [AsParameters]
  2. Särskilda typer
    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. Parametertypen har en giltig statisk BindAsync-metod.
  4. Parametertypen är en sträng eller har en giltig statisk TryParse-metod.
    1. Om parameternamnet finns i vägmallen. På app.Map("/todo/{id}", (int id) => {});är id bunden från rutten.
    2. Bunden till frågesträngen.
  5. Om parametertypen är en tjänst som tillhandahålls av beroendeinmatning använder den tjänsten som källa.
  6. Parametern kommer från kroppen.

Konfigurera JSON-deserialiseringsalternativ för brödtextbindning

Kroppsbindningskällan använder System.Text.Json för deserialisering. Det är inte möjligt att ändra den här standardinställningen, men JSON-serialiserings- och deserialiseringsalternativ kan konfigureras.

Konfigurera JSON-deserialiseringsalternativ globalt

Alternativ som gäller globalt för en app kan konfigureras genom att anropa ConfigureHttpJsonOptions. Följande exempel innehåller offentliga fält och formaterar JSON-utdata.

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

Eftersom exempelkoden konfigurerar både serialisering och deserialisering kan den läsa NameField och inkludera NameField i utdata-JSON.

Konfigurera JSON-deserialiseringsalternativ för en slutpunkt

ReadFromJsonAsync har överbelastningar som accepterar ett JsonSerializerOptions-objekt. Följande exempel innehåller offentliga fält och formaterar JSON-utdata.

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

Eftersom föregående kod endast tillämpar de anpassade alternativen för deserialisering utesluter utdata-JSON NameField.

Läs begärandetexten

Läs begärandetexten direkt med hjälp av en HttpContext- eller HttpRequest-parameter:

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

Föregående kod: