Megosztás a következőn keresztül:


Paraméterkötés minimális API-alkalmazásokban

Megjegyzés:

Ez nem a cikk legújabb verziója. Az aktuális kiadásról a cikk .NET 10-es verziójában olvashat.

Figyelmeztetés

A ASP.NET Core ezen verziója már nem támogatott. További információt a .NET és a .NET Core támogatási szabályzatában talál. A jelen cikk .NET 9-es verzióját lásd az aktuális kiadásért .

A paraméterkötés a kérelemadatok erősen gépelt paraméterekké alakításának folyamata, amelyeket az útvonalkezelők fejeznek ki. A kötési forrás határozza meg, hogy a paraméterek honnan vannak kötve. A kötési források lehetnek explicitek vagy következtethetők a HTTP-módszer és a paramétertípus alapján.

Támogatott kötési források:

  • Útvonalértékek
  • Lekérdezési karakterlánc
  • Fejléc
  • Törzs (mint JSON)
  • Űrlapértékek
  • Függőséginjektálás által biztosított szolgáltatások
  • Személyre szabott

Az alábbi GET útvonalkezelő az alábbi paraméterkötési források némelyikét használja:

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

Az alábbi táblázat az előző példában használt paraméterek és a kapcsolódó kötési források közötti kapcsolatot mutatja be.

Paraméter Kötés forrása
id útvonal értéke
page lekérdezési karakterlánc
customHeader fejléc
service Függőséginjektálás által biztosított

A HTTP-metódusok GET, HEAD, OPTIONSés DELETE nem kötődnek implicit módon a törzsből. A HTTP-metódusok törzséből (JSON-ként) való kötéshez kifejezetten[FromBody] vagy a HttpRequestolvasásához.

Az alábbi példában a POST útvonalkezelő egy kötési törzsforrást (JSON-ként) használ a person paraméterhez:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Az előző példákban szereplő paraméterek automatikusan kötődnek a kérelemadatokhoz. A paraméterkötés által biztosított kényelem bemutatásához az alábbi útvonalkezelők bemutatják, hogyan olvashatók be közvetlenül a kérelem adatai a kérelemből:

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 paraméterkötés

Az attribútumokkal explicit módon deklarálható, hogy a paraméterek honnan vannak kötve.

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);
Paraméter Kötés forrása
id útvonalérték id
page lekérdezési karakterlánc "p" nevű
service Függőséginjektálás által biztosított
contentType fejléc a "Content-Type" névvel

Explicit kötés űrlapértékekből

A [FromForm] attribútum az űrlapértékeket köti össze:

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.

Másik lehetőségként használhatja a [AsParameters] attribútumot olyan egyéni típussal, amely [FromForm]felirattal ellátott tulajdonságokkal rendelkezik. Az alábbi kód például az űrlapértékek és a NewTodoRequest rekordstruktúra tulajdonságaihoz kötődik:

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

További információért lásd a AsParameters részt a cikk későbbi részében.

A teljes mintakód az AspNetCore.Docs.Samples adattárban található.

Biztonságos kötés az IFormFile-ból és az IFormFileCollectionből

Az összetett űrlapkötés támogatott a IFormFile, a IFormFileCollection és a [FromForm]használatával.

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

A [FromForm] kéréshez kötött paraméterek közé tartozik egy álhamisítás elleni token. A kérés feldolgozásakor a rendszer ellenőrzi az antiforgery tokent. További információért lásd: Hamisítás elleni védelem minimális API-kkal.

További információért tekintse meg: Űrlapkötés minimális API-kban.

A teljes mintakód az AspNetCore.Docs.Samples adattárban található.

Paraméterkötés függőséginjektálással

A minimális API-k paraméterkötése függőséginjektálási köti a paramétereket, ha a típus szolgáltatásként van konfigurálva. Nem szükséges explicit módon alkalmazni a [FromServices] attribútumot egy paraméterre. A következő kódban mindkét művelet az időt adja vissza:

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

Választható paraméterek

Az útvonalkezelőkben deklarált paramétereket a rendszer szükség szerint kezeli:

  • Ha egy kérelem megfelel az útvonalnak, az útvonalkezelő csak akkor fut, ha a kérelemben minden szükséges paraméter meg van adva.
  • Ha nem adja meg az összes szükséges paramétert, az hibát eredményez.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
URI eredmény
/products?pageNumber=3 3 visszaküldött
/products BadHttpRequestException: A szükséges "int pageNumber" paraméter nem lett megadva a lekérdezési sztringből.
/products/1 HTTP 404-es hiba, nincs egyező útvonal

Ha a pageNumber nem kötelezővé szeretné tenni, adja meg a típust opcionálisként, vagy adjon meg egy alapértelmezett értéket:

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 eredmény
/products?pageNumber=3 3 visszaküldött
/products 1 visszaadott
/products2 1 visszaadott

Az előző null értékű és alapértelmezett érték az összes forrásra vonatkozik:

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

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

app.Run();

Az előző kód null értékű termékkel hívja meg a metódust, ha nem küldenek kéréstörzset.

MEGJEGYZÉS: Ha érvénytelen adatokat ad meg, és a paraméter nullázható, az útvonalkezelő nem kerül végrehajtásra.

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

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

app.Run();
URI eredmény
/products?pageNumber=3 3 visszaérkezett
/products 1 visszaérkezett
/products?pageNumber=two BadHttpRequestException: Nem sikerült megkötni a "Nullable<int> pageNumber" paramétert a "kettő" értékből.
/products/two HTTP 404-es hiba, nincs egyező útvonal

További információt a kötési hibák szakaszban talál.

Speciális típusok

A következő típusok explicit attribútumok nélkül vannak megkötve:

  • HttpContext: Az aktuális HTTP-kéréssel vagy -válaszsal kapcsolatos összes információt tartalmazó környezet:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest és HttpResponse: A HTTP-kérés és a HTTP-válasz:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Az aktuális HTTP-kéréshez társított törlési token:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: A kérelemhez társított felhasználó, HttpContext.User:

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

A kérelem törzsét kössük össze Stream vagy PipeReader-ként.

A kérelem törzse Stream vagy PipeReader-ként összekapcsolható, hogy hatékonyan támogassa azokat a forgatókönyveket, amelyekben a felhasználónak adatokat kell feldolgoznia, és:

  • Tárolja az adatokat blobtárolóba, vagy az adatokat egy üzenetsor-szolgáltatóhoz csatolja.
  • Feldolgozhatja a tárolt adatokat egy feldolgozói folyamattal vagy egy felhőfüggvénnyel.

Előfordulhat például, hogy az adatok Azure Queue Storage- vagy Azure Blob Storage-tárolóba kerülnek.

A következő kód egy háttér-üzenetsort implementál:

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

A következő kód hozzárendeli a kérelem törzsét egy Stream-hoz:

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

Az alábbi kód a teljes Program.cs fájlt jeleníti meg:

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();
  • Az adatok olvasásakor a Stream ugyanaz az objektum, mint HttpRequest.Body.
  • A kérelem törzse alapértelmezés szerint nincs pufferelve. A test elolvasása után nem lehet újraindítani. A stream nem olvasható többször.
  • A Stream és a PipeReader nem használhatók a minimális műveletkezelőn kívül, mivel a mögöttes puffereket a rendszer megsemmisíti vagy újra felhasználja.

Fájlfeltöltések az IFormFile és az IFormFileCollection használatával

A minimális API-k használatával IFormFile és IFormFileCollection használatával végzett fájlfeltöltések kódolást igényelnek multipart/form-data . Az útvonalkezelő paraméternevének meg kell egyeznie a kérelem űrlapmezőjének nevével. A minimális API-k nem támogatják a teljes kérelemtörzs közvetlen kötését egy IFormFile paraméterhez űrlapkódolás nélkül.

Ha a teljes kérelemtörzset meg kell kötnie, például JSON-, bináris adatok vagy más tartalomtípusok használatakor, tekintse meg a következőt:

A következő kód IFormFile és IFormFileCollection használ a fájl feltöltéséhez:

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

A hitelesített fájlfeltöltési kérelmek egy Engedélyezési fejléc, ügyféltanúsítványvagy cookie fejléc használatával támogatottak.

Az űrlapokhoz való kötődés az IFormCollection, IFormFile és IFormFileCollection használatával

A IFormCollection, IFormFileés IFormFileCollection használatával végzett űrlapalapú paraméterek kötése támogatott. OpenAPI metaadata a űrlapparaméterekre következtetve támogatja a Swagger UIintegrációt.

Az alábbi kód a IFormFile típusból származó következtetéses kötéssel tölti fel a fájlokat:

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

Figyelmeztetés: Űrlapok implementálásakor az alkalmazás meg kell akadályozniahelyek közötti kérelemhamisítási (XSRF/CSRF) támadásokat. Az előző kódban a IAntiforgery szolgáltatás az XSRF-támadások megelőzésére szolgál egy antiforgery token létrehozásával és érvényesítésével:

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

További információ az XSRF-támadásokról: Antiforgery hamisítás elleni védelem minimális API-kkal

További információkért lásd: Űrlapkötés minimális API-kban.

Gyűjtemények és összetett típusok űrlapokból való összekötése

A kötés támogatott az alábbiaknál:

  • Gyűjtemények, például Lista és Szótár
  • Összetett típusok, például Todo vagy Project

Az alábbi kód a következőket jeleníti meg:

  • Egy minimális végpont, amely egy többrészes űrlap bemenetét egy összetett objektumhoz köti.
  • Az hamisítás elleni szolgáltatások használata a hamisítás elleni tokenek létrehozásának és érvényesítésének támogatására.
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));
}

Az előző kódban:

  • A célparamétert a JSON törzsből beolvasandó paraméterek egyértelműsítéséhez [FromForm] attribútummal kell ellátni.
  • Az összetett vagy gyűjteménytípusok kötése nem támogatott minimális API-k esetében, amelyek a kérelemdelegálási generátorral vannak lefordítva.
  • A korrektúra egy további rejtett bemenetet jelenít meg isCompleted nevével és falseértékkel. Ha a isCompleted jelölőnégyzet be van jelölve az űrlap elküldésekor, akkor a true és a false is értékként lesz elküldve. Ha a jelölőnégyzet nincs bejelölve, csak a rejtett bemeneti érték false lesz elküldve. Az ASP.NET Core modellkötési folyamata csak az első értéket olvassa be, amikor egy bool értékhez van kötés, ami true eredményez a bejelölt jelölőnégyzetek esetén és false a nem bejelölt jelölőnégyzeteknél.

Az előző végpontnak küldött űrlapadatok egy példája a következőképpen néz ki:

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

Tömbök és karakterláncértékek összekapcsolása fejlécekből és kérdésláncokból

Az alábbi kód bemutatja, hogyan lehet a lekérdezési karakterláncokat primitív típusokba, karakterlánctömbökbe és StringValuestömbbe kötni.

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

A lekérdezési sztringek vagy fejlécértékek összetett típusú tömbhöz való kötése akkor támogatott, ha a típus TryParse implementálva van. A következő kód egy sztringtömbhöz kötődik, és a megadott címkékkel rendelkező összes elemet visszaadja:

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

Az alábbi kód a modellt és a szükséges TryParse implementációt mutatja be:

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

A következő kód egy int tömbhöz kapcsolódik:

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

Az előző kód teszteléséhez adja hozzá a következő végpontot az adatbázis Todo elemekkel való feltöltéséhez:

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

    return Results.Ok(todos);
});

A következő adatok az előző végpontra való továbbításához használjon olyan eszközt, mint a HttpRepl:

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

A következő kód a fejléckulcshoz X-Todo-Id kapcsolódik, és visszaadja a Todo elemeket az egyező Id értékekkel:

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

Megjegyzés:

Ha egy string[] egy lekérdezési sztringből illeszt, az egyező lekérdezési sztringérték hiánya null érték helyett üres tömböt eredményez.

Paraméterkötés az [AsParameters] argumentumlistákhoz

AsParametersAttribute egyszerű paraméterkötést tesz lehetővé a típusokhoz, nem pedig összetett vagy rekurzív modellkötéshez.

Vegye figyelembe a következő kódot:

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.

Fontolja meg a következő GET végpontot:

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

Az alábbi struct helyettesítheti az előző kiemelt paramétereket:

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

Az átszervezett GET végpont az előző struct-et használja az AsParameters attribútummal.

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

Az alábbi kód további végpontokat jelenít meg az alkalmazásban:

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

A paraméterlisták újrabontásához a következő osztályok használhatók:

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

Az alábbi kód a AsParameters és az előtte lévő struct valamint osztályok használatával átszervezett végpontokat mutatja be.

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

Az alábbi record típusokkal helyettesítheti az előző paramétereket:

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

A structAsParameters-gyel való használata hatékonyabb lehet, mint egy record típus használata.

A teljes mintakód az AspNetCore.Docs.Samples adattárban.

Egyéni kötés

A paraméterkötés testreszabásának három módja van:

  1. Az útvonal-, lekérdezés- és fejléckötési forrásokhoz az egyéni típusok kötéséhez adjon hozzá egy statikus TryParse metódust a típushoz.
  2. A kötési folyamat szabályozása egy BindAsync metódus típuson való implementálásával.
  3. Speciális forgatókönyvek esetén implementálja a(z) IBindableFromHttpContext<TSelf> interfészt, hogy közvetlenül HttpContext egyéni kötési logikát biztosítson.

TryParse

TryParse két API-val rendelkezik:

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

Az alábbi kód a Point: 12.3, 10.1 kódot jeleníti meg az URI /map?Point=12.3,10.1-el.

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 a következő API-kkal rendelkezik:

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

Az alábbi kód a SortBy:xyz, SortDirection:Desc, CurrentPage:99 kódot jeleníti meg az URI /products?SortBy=xyz&SortDir=Desc&Page=99-el.

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
}

Egyéni paraméterkötés a IBindableFromHttpContext segítségével

ASP.NET Core támogatja az egyéni paraméterkötést minimális API-kban az IBindableFromHttpContext<TSelf> interfész használatával. Ez a C# 11 statikus absztrakt tagjaival bevezetett felület lehetővé teszi, hogy olyan típusokat hozzon létre, amelyek közvetlenül az útvonalkezelő paraméterekben köthetők http-környezetből.

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

Az IBindableFromHttpContext<TSelf> implementálásával egyéni típusokat hozhat létre, amelyek a saját kötési logikájukat kezelik a HttpContext-ból. Ha egy útvonalkezelő ilyen típusú paramétert tartalmaz, a keretrendszer automatikusan meghívja a statikus BindAsync metódust a példány létrehozásához:

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

Az alábbi példa egy HTTP-fejlécből kötődő egyéni paraméter implementációját mutatja be:

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

Az érvényesítést az egyéni kötési logikán belül is megvalósíthatja:

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

A mintakód megtekintése vagy letöltése (letöltés)

Kötési hibák

Ha a kötés sikertelen, a keretrendszer hibakeresési üzenetet naplóz, és a hibamódtól függően különböző állapotkódokat ad vissza az ügyfélnek.

Hibamód Null értékű paraméter típusa Kötés forrása Állapotkód
{ParameterType}.TryParse visszaad false igen útvonal/lekérdezés/fejléc 400
{ParameterType}.BindAsync visszaad null igen szokás 400
{ParameterType}.BindAsync dobások nem számít szokás ötszáz
A JSON-törzs deszerializálásának sikertelensége nem számít test 400
Helytelen tartalomtípus (nem application/json) nem számít test 415

Kötési elsőbbség

A kötési forrás paraméterből való meghatározásának szabályai:

  1. A paraméteren (From* attribútumokon) definiált explicit attribútum a következő sorrendben:
    1. Útvonalértékek: [FromRoute]
    2. Lekérdezési karakterlánc: [FromQuery]
    3. Fejléc: [FromHeader]
    4. Törzs: [FromBody]
    5. Űrlap: [FromForm]
    6. Szolgáltatás: [FromServices]
    7. Paraméterértékek: [AsParameters]
  2. Speciális típusok
    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. A paramétertípus érvényes statikus BindAsync metódussal rendelkezik.
  4. A paraméter típusa sztring, vagy érvényes statikus TryParse metódussal rendelkezik.
    1. Ha a paraméter neve létezik például az útvonalsablonban, app.Map("/todo/{id}", (int id) => {});, akkor az útvonalhoz van kötve.
    2. Lekérdezési karakterláncból kötve.
  5. Ha a paramétertípus egy függőséginjektálás által biztosított szolgáltatás, akkor ezt a szolgáltatást használja forrásként.
  6. A paraméter a törzsből származik.

JSON deszerializálási beállítások konfigurálása a törzskötéshez

A testkötési forrás System.Text.Json-t használ a deszerializáláshoz. Ezt az alapértelmezett beállítást nem lehet módosítani, de a JSON szerializálási és deszerializálási beállításai konfigurálhatók.

JSON deszerializálási beállítások konfigurálása globálisan

Az alkalmazásokra globálisan érvényes beállítások ConfigureHttpJsonOptionsmeghívásával konfigurálhatók. Az alábbi példa a nyilvános mezőket és a JSON-kimenet formátumát tartalmazza.

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

Mivel a mintakód szerializálást és deszerializálást is konfigurál, képes olvasni NameField, és NameField belefoglalni a kimeneti JSON-fájlba.

JSON-deszerializálási beállítások konfigurálása végponthoz

ReadFromJsonAsync-nak vannak túlterhelései, amelyek JsonSerializerOptions objektumot fogadnak el. Az alábbi példa a nyilvános mezőket és a JSON-kimenet formátumát tartalmazza.

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

Mivel az előző kód csak a deszerializálásra alkalmazza a testre szabott beállításokat, a kimeneti JSON kizárja NameField.

Olvassa el a kérés törzsét

Olvassa el a kérelem törzsét közvetlenül egy HttpContext vagy HttpRequest paraméter használatával:

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

Az előző kód:

  • A kérelmi törzshöz a HttpRequest.BodyReaderhasználatával fér hozzá.
  • Másolja a kérelem törzsét egy helyi fájlba.

A paraméterkötés a kérelemadatok erősen gépelt paraméterekké alakításának folyamata, amelyeket az útvonalkezelők fejeznek ki. A kötési forrás határozza meg, hogy a paraméterek honnan vannak kötve. A kötési források lehetnek explicitek vagy következtethetők a HTTP-módszer és a paramétertípus alapján.

Támogatott kötési források:

  • Útvonalértékek
  • Lekérdezési karakterlánc
  • Fejléc
  • Törzs (mint JSON)
  • Függőséginjektálás által biztosított szolgáltatások
  • Személyre szabott

Az űrlapértékek kötése nem natívan támogatott .NET 6 és 7 rendszerben.

Az alábbi GET útvonalkezelő az alábbi paraméterkötési források némelyikét használja:

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

Az alábbi táblázat az előző példában használt paraméterek és a kapcsolódó kötési források közötti kapcsolatot mutatja be.

Paraméter Kötés forrása
id útvonal értéke
page lekérdezési karakterlánc
customHeader fejléc
service Függőséginjektálás által biztosított

A HTTP-metódusok GET, HEAD, OPTIONSés DELETE nem kötődnek implicit módon a törzsből. A HTTP-metódusok törzséből (JSON-ként) való kötéshez kifejezetten[FromBody] vagy a HttpRequestolvasásához.

Az alábbi példában a POST útvonalkezelő egy kötési törzsforrást (JSON-ként) használ a person paraméterhez:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Az előző példákban szereplő paraméterek automatikusan kötődnek a kérelemadatokhoz. A paraméterkötés által biztosított kényelem bemutatásához az alábbi útvonalkezelők bemutatják, hogyan olvashatók be közvetlenül a kérelem adatai a kérelemből:

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 paraméterkötés

Az attribútumokkal explicit módon deklarálható, hogy a paraméterek honnan vannak kötve.

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);
Paraméter Kötés forrása
id útvonalérték id
page lekérdezési karakterlánc "p" nevű
service Függőséginjektálás által biztosított
contentType fejléc a "Content-Type" névvel

Megjegyzés:

Az űrlapértékek kötése nem natívan támogatott .NET 6 és 7 rendszerben.

Paraméterkötés függőséginjektálással

A minimális API-k paraméterkötése függőséginjektálási köti a paramétereket, ha a típus szolgáltatásként van konfigurálva. Nem szükséges explicit módon alkalmazni a [FromServices] attribútumot egy paraméterre. A következő kódban mindkét művelet az időt adja vissza:

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

Választható paraméterek

Az útvonalkezelőkben deklarált paramétereket a rendszer szükség szerint kezeli:

  • Ha egy kérelem megfelel az útvonalnak, az útvonalkezelő csak akkor fut, ha a kérelemben minden szükséges paraméter meg van adva.
  • Ha nem adja meg az összes szükséges paramétert, az hibát eredményez.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
URI eredmény
/products?pageNumber=3 3 visszaküldött
/products BadHttpRequestException: A szükséges "int pageNumber" paraméter nem lett megadva a lekérdezési sztringből.
/products/1 HTTP 404-es hiba, nincs egyező útvonal

Ha a pageNumber nem kötelezővé szeretné tenni, adja meg a típust opcionálisként, vagy adjon meg egy alapértelmezett értéket:

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 eredmény
/products?pageNumber=3 3 visszaküldött
/products 1 visszaadott
/products2 1 visszaadott

Az előző null értékű és alapértelmezett érték az összes forrásra vonatkozik:

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

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

app.Run();

Az előző kód null értékű termékkel hívja meg a metódust, ha nem küldenek kéréstörzset.

MEGJEGYZÉS: Ha érvénytelen adatokat ad meg, és a paraméter nullázható, az útvonalkezelő nem kerül végrehajtásra.

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

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

app.Run();
URI eredmény
/products?pageNumber=3 3 visszaérkezett
/products 1 visszaérkezett
/products?pageNumber=two BadHttpRequestException: Nem sikerült megkötni a "Nullable<int> pageNumber" paramétert a "kettő" értékből.
/products/two HTTP 404-es hiba, nincs egyező útvonal

További információt a kötési hibák szakaszban talál.

Speciális típusok

A következő típusok explicit attribútumok nélkül vannak megkötve:

  • HttpContext: Az aktuális HTTP-kéréssel vagy -válaszsal kapcsolatos összes információt tartalmazó környezet:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest és HttpResponse: A HTTP-kérés és a HTTP-válasz:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Az aktuális HTTP-kéréshez társított törlési token:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: A kérelemhez társított felhasználó, HttpContext.User:

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

A kérelem törzsét kössük össze Stream vagy PipeReader-ként.

A kérelem törzse Stream vagy PipeReader-ként összekapcsolható, hogy hatékonyan támogassa azokat a forgatókönyveket, amelyekben a felhasználónak adatokat kell feldolgoznia, és:

  • Tárolja az adatokat blobtárolóba, vagy az adatokat egy üzenetsor-szolgáltatóhoz csatolja.
  • Feldolgozhatja a tárolt adatokat egy feldolgozói folyamattal vagy egy felhőfüggvénnyel.

Előfordulhat például, hogy az adatok Azure Queue Storage- vagy Azure Blob Storage-tárolóba kerülnek.

A következő kód egy háttér-üzenetsort implementál:

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

A következő kód hozzárendeli a kérelem törzsét egy Stream-hoz:

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

Az alábbi kód a teljes Program.cs fájlt jeleníti meg:

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();
  • Az adatok olvasásakor a Stream ugyanaz az objektum, mint HttpRequest.Body.
  • A kérelem törzse alapértelmezés szerint nincs pufferelve. A test elolvasása után nem lehet újraindítani. A stream nem olvasható többször.
  • A Stream és a PipeReader nem használhatók a minimális műveletkezelőn kívül, mivel a mögöttes puffereket a rendszer megsemmisíti vagy újra felhasználja.

Fájlfeltöltések az IFormFile és az IFormFileCollection használatával

A következő kód IFormFile és IFormFileCollection használ a fájl feltöltéséhez:

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

A hitelesített fájlfeltöltési kérelmek egy Engedélyezési fejléc, ügyféltanúsítványvagy cookie fejléc használatával támogatottak.

A .NET 7-ben a ASP.NET Core nem támogatja az antiforgery beépített támogatását. Az Antiforgery a ASP.NET Core-ban érhető el a .NET 8-ban vagy újabb verzióiban . Azonban a IAntiforgery szolgáltatáshasználatával implementálható.

Tömbök és karakterláncértékek összekapcsolása fejlécekből és kérdésláncokból

Az alábbi kód bemutatja, hogyan lehet a lekérdezési karakterláncokat primitív típusokba, karakterlánctömbökbe és StringValuestömbbe kötni.

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

A lekérdezési sztringek vagy fejlécértékek összetett típusú tömbhöz való kötése akkor támogatott, ha a típus TryParse implementálva van. A következő kód egy sztringtömbhöz kötődik, és a megadott címkékkel rendelkező összes elemet visszaadja:

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

Az alábbi kód a modellt és a szükséges TryParse implementációt mutatja be:

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

A következő kód egy int tömbhöz kapcsolódik:

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

Az előző kód teszteléséhez adja hozzá a következő végpontot az adatbázis Todo elemekkel való feltöltéséhez:

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

    return Results.Ok(todos);
});

Használjon egy API-tesztelési eszközt, például HttpRepl a következő adatok átadásához az előző végpontnak:

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

A következő kód a fejléckulcshoz X-Todo-Id kapcsolódik, és visszaadja a Todo elemeket az egyező Id értékekkel:

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

Megjegyzés:

Ha egy string[] egy lekérdezési sztringből illeszt, az egyező lekérdezési sztringérték hiánya null érték helyett üres tömböt eredményez.

Paraméterkötés az [AsParameters] argumentumlistákhoz

AsParametersAttribute egyszerű paraméterkötést tesz lehetővé a típusokhoz, nem pedig összetett vagy rekurzív modellkötéshez.

Vegye figyelembe a következő kódot:

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.

Fontolja meg a következő GET végpontot:

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

Az alábbi struct helyettesítheti az előző kiemelt paramétereket:

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

Az átszervezett GET végpont az előző struct-et használja az AsParameters attribútummal.

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

Az alábbi kód további végpontokat jelenít meg az alkalmazásban:

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

A paraméterlisták újrabontásához a következő osztályok használhatók:

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

Az alábbi kód a AsParameters és az előtte lévő struct valamint osztályok használatával átszervezett végpontokat mutatja be.

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

Az alábbi record típusokkal helyettesítheti az előző paramétereket:

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

A structAsParameters-gyel való használata hatékonyabb lehet, mint egy record típus használata.

A teljes mintakód az AspNetCore.Docs.Samples adattárban.

Egyéni kötés

A paraméterkötés testreszabásának három módja van:

  1. Az útvonal-, lekérdezés- és fejléckötési forrásokhoz az egyéni típusok kötéséhez adjon hozzá egy statikus TryParse metódust a típushoz.
  2. A kötési folyamat szabályozása egy BindAsync metódus típuson való implementálásával.
  3. Speciális forgatókönyvek esetén implementálja a(z) IBindableFromHttpContext<TSelf> interfészt, hogy közvetlenül HttpContext egyéni kötési logikát biztosítson.

TryParse

TryParse két API-val rendelkezik:

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

Az alábbi kód a Point: 12.3, 10.1 kódot jeleníti meg az URI /map?Point=12.3,10.1-el.

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 a következő API-kkal rendelkezik:

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

Az alábbi kód a SortBy:xyz, SortDirection:Desc, CurrentPage:99 kódot jeleníti meg az URI /products?SortBy=xyz&SortDir=Desc&Page=99-el.

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
}

Egyéni paraméterkötés a IBindableFromHttpContext segítségével

ASP.NET Core támogatja az egyéni paraméterkötést minimális API-kban az IBindableFromHttpContext<TSelf> interfész használatával. Ez a C# 11 statikus absztrakt tagjaival bevezetett felület lehetővé teszi, hogy olyan típusokat hozzon létre, amelyek közvetlenül az útvonalkezelő paraméterekben köthetők http-környezetből.

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

Az IBindableFromHttpContext<TSelf> interfész implementálásával egyéni típusokat hozhat létre, amelyek a saját kötési logikájukat a HttpContext-ből kezelik. Ha egy útvonalkezelő tartalmaz egy ilyen típusú paramétert, a keretrendszer automatikusan meghívja a statikus BindAsync metódust a példány létrehozásához:

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

Az alábbi példa egy HTTP-fejlécből kötődő egyéni paraméter implementációját mutatja be:

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

Az érvényesítést az egyéni kötési logikán belül is megvalósíthatja:

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

A mintakód megtekintése vagy letöltése (letöltés)

Kötési hibák

Ha a kötés sikertelen, a keretrendszer hibakeresési üzenetet naplóz, és a hibamódtól függően különböző állapotkódokat ad vissza az ügyfélnek.

Hibamód Null értékű paraméter típusa Kötés forrása Állapotkód
{ParameterType}.TryParse visszaad false igen útvonal/lekérdezés/fejléc 400
{ParameterType}.BindAsync visszaad null igen szokás 400
{ParameterType}.BindAsync dobások nem számít szokás ötszáz
A JSON-törzs deszerializálásának sikertelensége nem számít test 400
Helytelen tartalomtípus (nem application/json) nem számít test 415

Kötési elsőbbség

A kötési forrás paraméterből való meghatározásának szabályai:

  1. A paraméteren (From* attribútumokon) definiált explicit attribútum a következő sorrendben:
    1. Útvonalértékek: [FromRoute]
    2. Lekérdezési karakterlánc: [FromQuery]
    3. Fejléc: [FromHeader]
    4. Törzs: [FromBody]
    5. Szolgáltatás: [FromServices]
    6. Paraméterértékek: [AsParameters]
  2. Speciális típusok
    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. A paramétertípus érvényes statikus BindAsync metódussal rendelkezik.
  4. A paraméter típusa sztring, vagy érvényes statikus TryParse metódussal rendelkezik.
    1. Ha a paraméter neve megtalálható az útvonalsablonban. A app.Map("/todo/{id}", (int id) => {});id az útvonalhoz van kötve.
    2. Lekérdezési karakterláncból kötve.
  5. Ha a paramétertípus egy függőséginjektálás által biztosított szolgáltatás, akkor ezt a szolgáltatást használja forrásként.
  6. A paraméter a törzsből származik.

JSON deszerializálási beállítások konfigurálása a törzskötéshez

A testkötési forrás System.Text.Json-t használ a deszerializáláshoz. Ezt az alapértelmezett beállítást nem lehet módosítani, de a JSON szerializálási és deszerializálási beállításai konfigurálhatók.

JSON deszerializálási beállítások konfigurálása globálisan

Az alkalmazásokra globálisan érvényes beállítások ConfigureHttpJsonOptionsmeghívásával konfigurálhatók. Az alábbi példa a nyilvános mezőket és a JSON-kimenet formátumát tartalmazza.

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

Mivel a mintakód szerializálást és deszerializálást is konfigurál, képes olvasni NameField, és NameField belefoglalni a kimeneti JSON-fájlba.

JSON-deszerializálási beállítások konfigurálása végponthoz

ReadFromJsonAsync-nak vannak túlterhelései, amelyek JsonSerializerOptions objektumot fogadnak el. Az alábbi példa a nyilvános mezőket és a JSON-kimenet formátumát tartalmazza.

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

Mivel az előző kód csak a deszerializálásra alkalmazza a testre szabott beállításokat, a kimeneti JSON kizárja NameField.

Olvassa el a kérés törzsét

Olvassa el a kérelem törzsét közvetlenül egy HttpContext vagy HttpRequest paraméter használatával:

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

Az előző kód:

  • A kérelmi törzshöz a HttpRequest.BodyReaderhasználatával fér hozzá.
  • Másolja a kérelem törzsét egy helyi fájlba.