Bagikan melalui


Pengikatan Parameter di aplikasi API Minimal

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 10 dari artikel ini.

Peringatan

Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 9 dari artikel ini.

Pengikatan parameter adalah proses mengonversi data permintaan menjadi parameter yang diekspresikan dengan kuat yang dinyatakan oleh penangan rute. Sumber pengikatan menentukan dari mana parameter terikat. Sumber pengikatan dapat eksplisit atau disimpulkan berdasarkan metode HTTP dan jenis parameter.

Sumber pengikatan yang didukung:

  • Nilai rute
  • Untai kueri
  • Kepala Halaman
  • Isi (sebagai JSON)
  • Nilai formulir
  • Layanan yang disediakan oleh injeksi dependensi
  • Adat

Handler rute berikut GET menggunakan beberapa sumber pengikatan parameter ini:

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

Tabel berikut menunjukkan hubungan antara parameter yang digunakan dalam contoh sebelumnya dan sumber pengikatan terkait.

Pengaturan Sumber Pengikatan
id nilai rute
page string kueri
customHeader header
service Disediakan oleh injeksi dependensi

Metode HTTP , GET, HEAD, dan OPTIONS tidak secara implisit DELETEmengikat dari isi. Untuk mengikat dari isi (sebagai JSON) untuk metode HTTP ini, ikat secara eksplisit dengan [FromBody] atau baca dari HttpRequest.

Contoh handler rute POST berikut menggunakan sumber isi pengikatan (sebagai JSON) untuk person parameter :

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parameter dalam contoh sebelumnya semuanya terikat dari data permintaan secara otomatis. Untuk menunjukkan kenyamanan yang disediakan pengikatan parameter, penangan rute berikut menunjukkan cara membaca data permintaan langsung dari permintaan:

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

    // ...
});

Pengikatan Parameter Eksplisit

Atribut dapat digunakan untuk secara eksplisit menyatakan dari mana parameter terikat.

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);
Pengaturan Sumber Pengikatan
id nilai rute dengan nama id
page string kueri dengan nama "p"
service Disediakan oleh injeksi dependensi
contentType header dengan nama "Content-Type"

Pengikatan eksplisit dari nilai formulir

Atribut [FromForm] mengikat nilai formulir:

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.

Alternatifnya adalah menggunakan [AsParameters] atribut dengan jenis kustom yang memiliki properti yang dianotasi dengan [FromForm]. Misalnya, kode berikut mengikat dari nilai formulir ke properti NewTodoRequest struktur rekaman:

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

Untuk informasi selengkapnya, lihat bagian tentang AsParameters nanti di artikel ini.

Kode sampel lengkap ada di repositori AspNetCore.Docs.Samples.

Pengikatan aman dari IFormFile dan IFormFileCollection

Pengikatan formulir kompleks didukung menggunakan IFormFile dan IFormFileCollection menggunakan [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();

Parameter yang terikat pada permintaan dengan [FromForm] menyertakan token antiforgery. Token antiforgery divalidasi ketika permintaan diproses. Untuk informasi selengkapnya, lihat Antiforgery dengan API Minimal.

Untuk informasi selengkapnya, lihat Pengikatan formulir di API minimal.

Kode sampel lengkap ada di repositori AspNetCore.Docs.Samples.

Pengikatan parameter dengan injeksi dependensi

Pengikatan parameter untuk API minimal mengikat parameter melalui injeksi dependensi saat jenis dikonfigurasi sebagai layanan. Tidak perlu menerapkan [FromServices] atribut secara eksplisit ke parameter. Dalam kode berikut, kedua tindakan mengembalikan waktu:

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

Parameter opsional

Parameter yang dideklarasikan dalam handler rute diperlakukan sesuai kebutuhan:

  • Jika permintaan cocok dengan rute, handler rute hanya berjalan jika semua parameter yang diperlukan disediakan dalam permintaan.
  • Kegagalan untuk memberikan semua parameter yang diperlukan menghasilkan kesalahan.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
URI hasil
/products?pageNumber=3 3 dikembalikan
/products BadHttpRequestException: Parameter yang diperlukan "int pageNumber" tidak disediakan dari string kueri.
/products/1 Kesalahan HTTP 404, tidak ada rute yang cocok

Untuk membuat pageNumber opsional, tentukan jenis sebagai opsional atau berikan nilai default:

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 hasil
/products?pageNumber=3 3 dikembalikan
/products 1 dikembalikan
/products2 1 dikembalikan

Nilai nullable dan default sebelumnya berlaku untuk semua sumber:

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

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

app.Run();

Kode sebelumnya memanggil metode dengan produk null jika tidak ada isi permintaan yang dikirim.

CATATAN: Jika data yang tidak valid disediakan dan parameter dapat diubah ke null, handler rute tidak dijalankan.

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

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

app.Run();
URI hasil
/products?pageNumber=3 3 Kembali
/products 1 Kembali
/products?pageNumber=two BadHttpRequestException: Gagal mengikat parameter "Nullable<int> pageNumber" dari "dua".
/products/two Kesalahan HTTP 404, tidak ada rute yang cocok

Lihat bagian Kegagalan Pengikatan untuk informasi selengkapnya.

Jenis khusus

Jenis berikut terikat tanpa atribut eksplisit:

  • HttpContext: Konteks yang menyimpan semua informasi tentang permintaan atau respons HTTP saat ini:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest dan HttpResponse: Permintaan HTTP dan respons HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Token pembatalan yang terkait dengan permintaan HTTP saat ini:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: Pengguna yang terkait dengan permintaan, terikat dari HttpContext.User:

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

Mengikat isi permintaan sebagai Stream atau PipeReader

Isi permintaan dapat mengikat sebagai Stream atau PipeReader untuk mendukung skenario secara efisien di mana pengguna harus memproses data dan:

  • Simpan data ke penyimpanan blob atau antrekan data ke penyedia antrean.
  • Proses data yang disimpan dengan proses pekerja atau fungsi cloud.

Misalnya, data mungkin diantrekan ke penyimpanan Azure Queue atau disimpan di penyimpanan Azure Blob.

Kode berikut mengimplementasikan antrean latar belakang:

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

Kode berikut mengikat isi permintaan ke 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);
});

Kode berikut menunjukkan file lengkap Program.cs :

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();
  • Saat membaca data, Stream adalah objek yang sama dengan HttpRequest.Body.
  • Isi permintaan tidak di-buffer secara default. Setelah tubuh dibaca, itu tidak dapat digulung balik. Aliran tidak dapat dibaca beberapa kali.
  • Stream dan PipeReader tidak dapat digunakan di luar handler tindakan minimal karena buffer yang mendasar akan dibuang atau digunakan kembali.

Unggahan file menggunakan IFormFile dan IFormFileCollection

Unggahan file menggunakan IFormFile dan IFormFileCollection dalam API minimal memerlukan multipart/form-data pengodean. Nama parameter dalam handler rute harus cocok dengan nama bidang formulir dalam permintaan. API minimal tidak mendukung pengikatan seluruh isi permintaan langsung ke IFormFile parameter tanpa pengodean formulir.

Jika Anda perlu mengikat seluruh isi permintaan, misalnya, saat bekerja dengan JSON, data biner, atau jenis konten lainnya, lihat:

Kode berikut menggunakan IFormFile dan IFormFileCollection untuk mengunggah file:

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

Permintaan unggahan file yang diautentikasi didukung menggunakan header Otorisasi, sertifikat klien, atau cookie header.

Pengikatan ke formulir dengan IFormCollection, IFormFile, dan IFormFileCollection

Pengikatan dari parameter berbasis formulir menggunakan IFormCollection, IFormFile, dan IFormFileCollection didukung. Metadata OpenAPI disimpulkan untuk parameter formulir untuk mendukung integrasi dengan antarmuka pengguna Swagger.

Kode berikut mengunggah file menggunakan pengikatan yang disimpulkan dari jenis :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();

Peringatan: Saat menerapkan formulir, aplikasi harus mencegahserangan Pemalsuan Permintaan Lintas Situs (XSRF/CSRF). Dalam kode sebelumnya, IAntiforgery layanan digunakan untuk mencegah serangan XSRF dengan menghasilkan dan memvalidasi token antiforgeri:

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

Untuk informasi selengkapnya tentang serangan XSRF, lihat Antiforgery dengan API Minimal

Untuk informasi selengkapnya, lihat Pengikatan formulir dalam API minimal;

Ikat ke koleksi dan jenis kompleks dari formulir

Pengikatan didukung untuk:

  • Koleksi, misalnya Daftar dan Kamus
  • Jenis kompleks, misalnya, Todo atau Project

Kode berikut menunjukkan:

  • Titik akhir minimal yang mengikat input formulir multi-bagian ke objek kompleks.
  • Cara menggunakan layanan antiforgery untuk mendukung pembuatan dan validasi token antiforgery.
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));
}

Dalam kode sebelumnya:

  • Parameter target harus diannotasi dengan [FromForm] atribut untuk membedakan dari parameter yang harus dibaca dari isi JSON.
  • Pengikatan dari jenis kompleks atau koleksi tidak didukung untuk API minimal yang dikompilasi dengan Generator Delegasi Permintaan.
  • Markup menunjukkan input tersembunyi tambahan dengan nama isCompleted dan nilai false. Jika kotak isCompleted centang dicentang saat formulir dikirimkan, kedua nilai true dan false dikirimkan sebagai nilai. Jika kotak centang tidak dicentang, hanya nilai false input tersembunyi yang dikirimkan. Proses pengikatan model ASP.NET Core hanya membaca nilai pertama saat mengikat bool nilai, yang menghasilkan kotak centang yang dicentang true dan false untuk kotak centang yang tidak dicentang.

Contoh data formulir yang dikirimkan ke titik akhir sebelumnya terlihat sebagai berikut:

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

Mengikat array dan nilai string dari header dan string kueri

Kode berikut menunjukkan pengikatan string kueri ke array jenis primitif, array string, dan 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]}");

Mengikat string kueri atau nilai header ke array jenis kompleks didukung ketika jenis telah TryParse diimplementasikan. Kode berikut mengikat ke array string dan mengembalikan semua item dengan tag yang ditentukan:

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

Kode berikut menunjukkan model dan implementasi yang diperlukan TryParse :

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

Kode berikut mengikat ke int array:

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

Untuk menguji kode sebelumnya, tambahkan titik akhir berikut untuk mengisi database dengan Todo item:

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

    return Results.Ok(todos);
});

Gunakan alat seperti HttpRepl untuk meneruskan data berikut ke titik akhir sebelumnya:

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

Kode berikut mengikat ke kunci X-Todo-Id header dan mengembalikan Todo item dengan nilai yang Id cocok:

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

Catatan

Saat mengikat string[] dari string kueri, tidak adanya nilai string kueri yang cocok akan menghasilkan array kosong alih-alih nilai null.

Pengikatan parameter untuk daftar argumen dengan [AsParameters]

AsParametersAttribute memungkinkan pengikatan parameter sederhana ke jenis dan bukan pengikatan model yang kompleks atau rekursif.

Pertimbangkan gambar berikut:

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.

Pertimbangkan titik akhir berikut GET :

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

Berikut ini struct dapat digunakan untuk mengganti parameter yang disorot sebelumnya:

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

Titik akhir yang direfaktor GET menggunakan yang sebelumnya struct dengan atribut 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());

Kode berikut menunjukkan titik akhir tambahan di aplikasi:

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

Kelas berikut digunakan untuk merefaktor daftar parameter:

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

Kode berikut menunjukkan titik akhir yang direfaktor menggunakan AsParameters dan kelas dan sebelumnya struct :

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

Jenis berikut record dapat digunakan untuk mengganti parameter sebelumnya:

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

struct Menggunakan dengan AsParameters bisa lebih berkinerja daripada menggunakan record jenis.

Kode sampel lengkap di repositori AspNetCore.Docs.Samples.

Pengikatan Kustom

Ada tiga cara untuk menyesuaikan pengikatan parameter:

  1. Untuk sumber pengikatan rute, kueri, dan header, ikat jenis kustom dengan menambahkan metode statis TryParse untuk jenis tersebut.
  2. Kontrol proses pengikatan dengan menerapkan BindAsync metode pada jenis.
  3. Untuk skenario tingkat lanjut, terapkan IBindableFromHttpContext<TSelf> antarmuka untuk menyediakan logika pengikatan kustom langsung dari HttpContext.

TryParse

TryParse memiliki dua API:

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

Kode berikut ditampilkan Point: 12.3, 10.1 dengan 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 memiliki API berikut:

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

Kode berikut ditampilkan SortBy:xyz, SortDirection:Desc, CurrentPage:99 dengan 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
}

Pengikatan parameter kustom dengan IBindableFromHttpContext

ASP.NET Core menyediakan dukungan untuk pengikatan parameter kustom di API Minimal menggunakan IBindableFromHttpContext<TSelf> antarmuka . Antarmuka ini, diperkenalkan dengan anggota abstrak statis C# 11, memungkinkan Anda membuat jenis yang dapat terikat dari konteks HTTP langsung dalam parameter handler rute.

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

Dengan menerapkan IBindableFromHttpContext<TSelf>, Anda dapat membuat tipe khusus yang menangani logika pengikatan mereka sendiri dari HttpContext. Saat handler rute menyertakan parameter jenis ini, kerangka kerja secara otomatis memanggil metode statis BindAsync untuk membuat instans:

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

Berikut ini adalah contoh implementasi parameter kustom yang mengikat dari header HTTP:

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

Anda juga dapat menerapkan validasi dalam logika pengikatan kustom Anda:

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

Melihat atau mengunduh kode sampel (cara mengunduh)

Kegagalan pengikatan

Saat pengikatan gagal, kerangka kerja mencatat pesan debug dan mengembalikan berbagai kode status ke klien tergantung pada mode kegagalan.

Mode kegagalan Jenis Parameter Nullable Sumber Pengikatan Kode status
{ParameterType}.TryParse menghasilkan false Ya rute/kueri/tajuk 400
{ParameterType}.BindAsync menghasilkan null Ya adat 400
{ParameterType}.BindAsync Melempar tidak penting adat 500
Kegagalan untuk mendeserialisasi isi JSON tidak penting tubuh 400
Tipe isi salah (bukan application/json) tidak penting tubuh 415

Prioritas Pengikatan

Aturan untuk menentukan sumber pengikatan dari parameter:

  1. Atribut eksplisit yang ditentukan pada parameter (Dari* atribut) dalam urutan berikut:
    1. Nilai rute: [FromRoute]
    2. String kueri: [FromQuery]
    3. Judul: [FromHeader]
    4. Badan: [FromBody]
    5. Bentuk: [FromForm]
    6. Layanan: [FromServices]
    7. Nilai parameter: [AsParameters]
  2. Jenis khusus
    1. HttpContext
    2. HttpRequestAku akan menemuinya.HttpContext.Request
    3. HttpResponseAku akan menemuinya.HttpContext.Response
    4. ClaimsPrincipalAku akan menemuinya.HttpContext.User
    5. CancellationTokenAku akan menemuinya.HttpContext.RequestAborted
    6. IFormCollectionAku akan menemuinya.HttpContext.Request.Form
    7. IFormFileCollectionAku akan menemuinya.HttpContext.Request.Form.Files
    8. IFormFileAku akan menemuinya.HttpContext.Request.Form.Files[paramName]
    9. StreamAku akan menemuinya.HttpContext.Request.Body
    10. PipeReaderAku akan menemuinya.HttpContext.Request.BodyReader
  3. Jenis parameter memiliki metode statis BindAsync yang valid.
  4. Jenis parameter adalah string atau memiliki metode statis TryParse yang valid.
    1. Jika nama parameter ada di templat rute misalnya, app.Map("/todo/{id}", (int id) => {});, maka terikat dari rute.
    2. Terikat dari string kueri.
  5. Jika jenis parameter adalah layanan yang disediakan oleh injeksi dependensi, ia menggunakan layanan tersebut sebagai sumbernya.
  6. Parameternya adalah dari isinya.

Mengonfigurasi opsi deserialisasi JSON untuk pengikatan isi

Sumber pengikatan isi menggunakan System.Text.Json untuk deserialisasi. Tidak dimungkinkan untuk mengubah default ini, tetapi opsi serialisasi dan deserialisasi JSON dapat dikonfigurasi.

Mengonfigurasi opsi deserialisasi JSON secara global

Opsi yang berlaku secara global untuk aplikasi dapat dikonfigurasi dengan memanggil ConfigureHttpJsonOptions. Contoh berikut mencakup bidang publik dan format output JSON.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

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

Karena kode sampel mengonfigurasi serialisasi dan deserialisasi, kode tersebut dapat membaca NameField dan menyertakan NameField dalam output JSON.

Mengonfigurasi opsi deserialisasi JSON untuk titik akhir

ReadFromJsonAsync memiliki kelebihan beban yang menerima JsonSerializerOptions objek. Contoh berikut mencakup bidang publik dan format output JSON.

using System.Text.Json;

var app = WebApplication.Create();

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

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

app.Run();

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

Karena kode sebelumnya menerapkan opsi yang disesuaikan hanya untuk deserialisasi, output JSON mengecualikan NameField.

Membaca isi permintaan

Baca isi permintaan secara langsung menggunakan HttpContext parameter atau HttpRequest :

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

Kode sebelumnya:

Pengikatan parameter adalah proses mengonversi data permintaan menjadi parameter yang diekspresikan dengan kuat yang dinyatakan oleh penangan rute. Sumber pengikatan menentukan dari mana parameter terikat. Sumber pengikatan dapat eksplisit atau disimpulkan berdasarkan metode HTTP dan jenis parameter.

Sumber pengikatan yang didukung:

  • Nilai rute
  • Untai kueri
  • Kepala Halaman
  • Isi (sebagai JSON)
  • Layanan yang disediakan oleh injeksi dependensi
  • Adat

Pengikatan dari nilai formulir tidak didukung secara asli di .NET 6 dan 7.

Handler rute berikut GET menggunakan beberapa sumber pengikatan parameter ini:

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

Tabel berikut menunjukkan hubungan antara parameter yang digunakan dalam contoh sebelumnya dan sumber pengikatan terkait.

Pengaturan Sumber Pengikatan
id nilai rute
page string kueri
customHeader header
service Disediakan oleh injeksi dependensi

Metode HTTP , GET, HEAD, dan OPTIONS tidak secara implisit DELETEmengikat dari isi. Untuk mengikat dari isi (sebagai JSON) untuk metode HTTP ini, ikat secara eksplisit dengan [FromBody] atau baca dari HttpRequest.

Contoh handler rute POST berikut menggunakan sumber isi pengikatan (sebagai JSON) untuk person parameter :

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

record Person(string Name, int Age);

Parameter dalam contoh sebelumnya semuanya terikat dari data permintaan secara otomatis. Untuk menunjukkan kenyamanan yang disediakan pengikatan parameter, penangan rute berikut menunjukkan cara membaca data permintaan langsung dari permintaan:

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

    // ...
});

Pengikatan Parameter Eksplisit

Atribut dapat digunakan untuk secara eksplisit menyatakan dari mana parameter terikat.

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);
Pengaturan Sumber Pengikatan
id nilai rute dengan nama id
page string kueri dengan nama "p"
service Disediakan oleh injeksi dependensi
contentType header dengan nama "Content-Type"

Catatan

Pengikatan dari nilai formulir tidak didukung secara asli di .NET 6 dan 7.

Pengikatan parameter dengan injeksi dependensi

Pengikatan parameter untuk API minimal mengikat parameter melalui injeksi dependensi saat jenis dikonfigurasi sebagai layanan. Tidak perlu menerapkan [FromServices] atribut secara eksplisit ke parameter. Dalam kode berikut, kedua tindakan mengembalikan waktu:

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

Parameter opsional

Parameter yang dideklarasikan dalam handler rute diperlakukan sesuai kebutuhan:

  • Jika permintaan cocok dengan rute, handler rute hanya berjalan jika semua parameter yang diperlukan disediakan dalam permintaan.
  • Kegagalan untuk memberikan semua parameter yang diperlukan menghasilkan kesalahan.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();
URI hasil
/products?pageNumber=3 3 dikembalikan
/products BadHttpRequestException: Parameter yang diperlukan "int pageNumber" tidak disediakan dari string kueri.
/products/1 Kesalahan HTTP 404, tidak ada rute yang cocok

Untuk membuat pageNumber opsional, tentukan jenis sebagai opsional atau berikan nilai default:

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 hasil
/products?pageNumber=3 3 dikembalikan
/products 1 dikembalikan
/products2 1 dikembalikan

Nilai nullable dan default sebelumnya berlaku untuk semua sumber:

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

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

app.Run();

Kode sebelumnya memanggil metode dengan produk null jika tidak ada isi permintaan yang dikirim.

CATATAN: Jika data yang tidak valid disediakan dan parameter dapat diubah ke null, handler rute tidak dijalankan.

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

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

app.Run();
URI hasil
/products?pageNumber=3 3 Kembali
/products 1 Kembali
/products?pageNumber=two BadHttpRequestException: Gagal mengikat parameter "Nullable<int> pageNumber" dari "dua".
/products/two Kesalahan HTTP 404, tidak ada rute yang cocok

Lihat bagian Kegagalan Pengikatan untuk informasi selengkapnya.

Jenis khusus

Jenis berikut terikat tanpa atribut eksplisit:

  • HttpContext: Konteks yang menyimpan semua informasi tentang permintaan atau respons HTTP saat ini:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest dan HttpResponse: Permintaan HTTP dan respons HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: Token pembatalan yang terkait dengan permintaan HTTP saat ini:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: Pengguna yang terkait dengan permintaan, terikat dari HttpContext.User:

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

Mengikat isi permintaan sebagai Stream atau PipeReader

Isi permintaan dapat mengikat sebagai Stream atau PipeReader untuk mendukung skenario secara efisien di mana pengguna harus memproses data dan:

  • Simpan data ke penyimpanan blob atau antrekan data ke penyedia antrean.
  • Proses data yang disimpan dengan proses pekerja atau fungsi cloud.

Misalnya, data mungkin diantrekan ke penyimpanan Azure Queue atau disimpan di penyimpanan Azure Blob.

Kode berikut mengimplementasikan antrean latar belakang:

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

Kode berikut mengikat isi permintaan ke 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);
});

Kode berikut menunjukkan file lengkap Program.cs :

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();
  • Saat membaca data, Stream adalah objek yang sama dengan HttpRequest.Body.
  • Isi permintaan tidak di-buffer secara default. Setelah tubuh dibaca, itu tidak dapat digulung balik. Aliran tidak dapat dibaca beberapa kali.
  • Stream dan PipeReader tidak dapat digunakan di luar handler tindakan minimal karena buffer yang mendasar akan dibuang atau digunakan kembali.

Unggahan file menggunakan IFormFile dan IFormFileCollection

Kode berikut menggunakan IFormFile dan IFormFileCollection untuk mengunggah file:

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

Permintaan unggahan file yang diautentikasi didukung menggunakan header Otorisasi, sertifikat klien, atau cookie header.

Tidak ada dukungan bawaan untuk antiforgery di ASP.NET Core di .NET 7. Antiforgery tersedia di ASP.NET Core di .NET 8 atau versi yang lebih baru. Namun, dapat diimplementasikan menggunakan IAntiforgery layanan .

Mengikat array dan nilai string dari header dan string kueri

Kode berikut menunjukkan pengikatan string kueri ke array jenis primitif, array string, dan 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]}");

Mengikat string kueri atau nilai header ke array jenis kompleks didukung ketika jenis telah TryParse diimplementasikan. Kode berikut mengikat ke array string dan mengembalikan semua item dengan tag yang ditentukan:

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

Kode berikut menunjukkan model dan implementasi yang diperlukan TryParse :

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

Kode berikut mengikat ke int array:

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

Untuk menguji kode sebelumnya, tambahkan titik akhir berikut untuk mengisi database dengan Todo item:

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

    return Results.Ok(todos);
});

Gunakan alat pengujian API seperti HttpRepl untuk meneruskan data berikut ke titik akhir sebelumnya:

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

Kode berikut mengikat ke kunci X-Todo-Id header dan mengembalikan Todo item dengan nilai yang Id cocok:

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

Catatan

Saat mengikat string[] dari string kueri, tidak adanya nilai string kueri yang cocok akan menghasilkan array kosong alih-alih nilai null.

Pengikatan parameter untuk daftar argumen dengan [AsParameters]

AsParametersAttribute memungkinkan pengikatan parameter sederhana ke jenis dan bukan pengikatan model yang kompleks atau rekursif.

Pertimbangkan gambar berikut:

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.

Pertimbangkan titik akhir berikut GET :

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

Berikut ini struct dapat digunakan untuk mengganti parameter yang disorot sebelumnya:

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

Titik akhir yang direfaktor GET menggunakan yang sebelumnya struct dengan atribut 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());

Kode berikut menunjukkan titik akhir tambahan di aplikasi:

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

Kelas berikut digunakan untuk merefaktor daftar parameter:

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

Kode berikut menunjukkan titik akhir yang direfaktor menggunakan AsParameters dan kelas dan sebelumnya struct :

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

Jenis berikut record dapat digunakan untuk mengganti parameter sebelumnya:

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

struct Menggunakan dengan AsParameters bisa lebih berkinerja daripada menggunakan record jenis.

Kode sampel lengkap di repositori AspNetCore.Docs.Samples.

Pengikatan Kustom

Ada tiga cara untuk menyesuaikan pengikatan parameter:

  1. Untuk sumber pengikatan rute, kueri, dan header, ikat jenis kustom dengan menambahkan metode statis TryParse untuk jenis tersebut.
  2. Kontrol proses pengikatan dengan menerapkan BindAsync metode pada jenis.
  3. Untuk skenario tingkat lanjut, terapkan IBindableFromHttpContext<TSelf> antarmuka untuk menyediakan logika pengikatan kustom langsung dari HttpContext.

TryParse

TryParse memiliki dua API:

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

Kode berikut ditampilkan Point: 12.3, 10.1 dengan 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 memiliki API berikut:

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

Kode berikut ditampilkan SortBy:xyz, SortDirection:Desc, CurrentPage:99 dengan 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
}

Pengikatan parameter kustom dengan IBindableFromHttpContext

ASP.NET Core menyediakan dukungan untuk pengikatan parameter kustom di API Minimal menggunakan IBindableFromHttpContext<TSelf> antarmuka . Antarmuka ini, diperkenalkan dengan anggota abstrak statis C# 11, memungkinkan Anda membuat jenis yang dapat terikat dari konteks HTTP langsung dalam parameter handler rute.

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

Dengan menerapkan antarmuka IBindableFromHttpContext<TSelf>, Anda dapat membuat tipe kustom yang menangani logika pengikatan mereka sendiri dari HttpContext. Ketika handler rute menyertakan parameter jenis ini, kerangka kerja secara otomatis memanggil metode BindAsync statis untuk membuat instans:

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

Berikut ini adalah contoh implementasi parameter kustom yang mengikat dari header HTTP:

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

Anda juga dapat menerapkan validasi dalam logika pengikatan kustom Anda:

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

Melihat atau mengunduh kode sampel (cara mengunduh)

Kegagalan pengikatan

Saat pengikatan gagal, kerangka kerja mencatat pesan debug dan mengembalikan berbagai kode status ke klien tergantung pada mode kegagalan.

Mode kegagalan Jenis Parameter Nullable Sumber Pengikatan Kode status
{ParameterType}.TryParse menghasilkan false Ya rute/kueri/tajuk 400
{ParameterType}.BindAsync menghasilkan null Ya adat 400
{ParameterType}.BindAsync Melempar tidak masalah adat 500
Kegagalan untuk mendeserialisasi isi JSON tidak masalah tubuh 400
Tipe isi salah (bukan application/json) tidak masalah tubuh 415

Prioritas Pengikatan

Aturan untuk menentukan sumber pengikatan dari parameter:

  1. Atribut eksplisit yang ditentukan pada parameter (Dari* atribut) dalam urutan berikut:
    1. Nilai rute: [FromRoute]
    2. String kueri: [FromQuery]
    3. Judul: [FromHeader]
    4. Badan: [FromBody]
    5. Layanan: [FromServices]
    6. Nilai parameter: [AsParameters]
  2. Jenis khusus
    1. HttpContext
    2. HttpRequestAku akan menemuinya.HttpContext.Request
    3. HttpResponseAku akan menemuinya.HttpContext.Response
    4. ClaimsPrincipalAku akan menemuinya.HttpContext.User
    5. CancellationTokenAku akan menemuinya.HttpContext.RequestAborted
    6. IFormFileCollectionAku akan menemuinya.HttpContext.Request.Form.Files
    7. IFormFileAku akan menemuinya.HttpContext.Request.Form.Files[paramName]
    8. StreamAku akan menemuinya.HttpContext.Request.Body
    9. PipeReaderAku akan menemuinya.HttpContext.Request.BodyReader
  3. Jenis parameter memiliki metode statis BindAsync yang valid.
  4. Jenis parameter adalah string atau memiliki metode statis TryParse yang valid.
    1. Jika nama parameter ada di templat rute. Dalam app.Map("/todo/{id}", (int id) => {});, id terikat dari rute.
    2. Terikat dari string kueri.
  5. Jika jenis parameter adalah layanan yang disediakan oleh injeksi dependensi, ia menggunakan layanan tersebut sebagai sumbernya.
  6. Parameternya adalah dari isinya.

Mengonfigurasi opsi deserialisasi JSON untuk pengikatan isi

Sumber pengikatan isi menggunakan System.Text.Json untuk deserialisasi. Tidak dimungkinkan untuk mengubah default ini, tetapi opsi serialisasi dan deserialisasi JSON dapat dikonfigurasi.

Mengonfigurasi opsi deserialisasi JSON secara global

Opsi yang berlaku secara global untuk aplikasi dapat dikonfigurasi dengan memanggil ConfigureHttpJsonOptions. Contoh berikut mencakup bidang publik dan format output JSON.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

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

Karena kode sampel mengonfigurasi serialisasi dan deserialisasi, kode tersebut dapat membaca NameField dan menyertakan NameField dalam output JSON.

Mengonfigurasi opsi deserialisasi JSON untuk titik akhir

ReadFromJsonAsync memiliki kelebihan beban yang menerima JsonSerializerOptions objek. Contoh berikut mencakup bidang publik dan format output JSON.

using System.Text.Json;

var app = WebApplication.Create();

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

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

app.Run();

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

Karena kode sebelumnya menerapkan opsi yang disesuaikan hanya untuk deserialisasi, output JSON mengecualikan NameField.

Membaca isi permintaan

Baca isi permintaan secara langsung menggunakan HttpContext parameter atau HttpRequest :

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

Kode sebelumnya: