Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Примечание.
Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.
Привязка параметров — это процесс преобразования данных запроса в строго типизированные параметры, выраженные обработчиками маршрутов. Источник привязки определяет, откуда будут привязаны параметры. Источники привязки могут быть явными или выведенными на основе метода HTTP и типа параметра.
Поддерживаемые источники привязки:
- значения маршрута;
- Строка запроса
- Заголовок
- текст (в формате JSON);
- Значения формы
- службы, предоставляемые через внедрение зависимостей
- Пользовательское
Примечание.
Типы объединения C# поддерживаются только как текст (как JSON). Источники вне тела запроса — значения маршрута, строка запроса, заголовки и значения формы — связывают строковые значения без разбора JSON, поэтому не могут сопоставляться с вариантом объединения.
В следующем GET обработчике маршрутов используются некоторые из этих источников привязки параметров:
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 { }
В следующей таблице показана связь между параметрами, используемыми в предыдущем примере, и связанными источниками привязки.
| Параметр | Источник привязки |
|---|---|
id |
Значение маршрута |
page |
строка запроса |
customHeader |
заголовок |
service |
Предоставляется путем внедрения зависимостей |
Методы HTTP GET, HEAD, OPTIONS и DELETE не выполняют неявную привязку из текста запроса. Чтобы выполнить привязку из тела (в формате JSON) для этих HTTP-методов, нужно явно указать привязку с помощью [FromBody] или прочитать данные из HttpRequest.
В следующем примере обработчик маршрута POST использует источник привязки — тело запроса (в формате JSON) — для параметра person:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Все параметры в предыдущих примерах автоматически привязываются из данных запроса. Чтобы продемонстрировать удобство привязки параметров, в следующих обработчиках маршрутов показано, как считывать данные запроса непосредственно из запроса:
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>();
// ...
});
Явная привязка параметров
Атрибуты можно использовать для явного объявления источника, из которого привязываются параметры.
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);
| Параметр | Источник привязки |
|---|---|
id |
Значение маршрута с именем id |
page |
Строка запроса с именем "p" |
service |
Предоставляется путем внедрения зависимостей |
contentType |
Заголовок с именем "Content-Type" |
Явная привязка к значениям формы
Атрибут [FromForm] привязывает значения формы:
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.
Альтернативой является использование [AsParameters] атрибута с пользовательским типом, который имеет свойства, аннотированные с [FromForm]. Например, следующий код выполняет привязку значений формы к свойствам record-структуры NewTodoRequest:
app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
var todo = new Todo
{
Name = request.Name,
Visibility = request.Visibility
};
if (request.Attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await request.Attachment.CopyToAsync(stream);
todo.Attachment = attachmentName;
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
[FromForm] Visibility Visibility, IFormFile? Attachment);
Дополнительные сведения см. в разделе AsParameters далее в этой статье.
Полный пример кода находится в репозитории AspNetCore.Docs.Samples .
Безопасная привязка из IFormFile и IFormFileCollection
Поддерживается сложная привязка форм с использованием IFormFile и IFormFileCollection при помощи [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) =>
{
// Server-side validation. The client-side pattern can be bypassed, so the
// name must be validated here too.
if (!MyUtils.IsValidFileName(fileUploadForm.Name))
{
return TypedResults.BadRequest("Invalid file name.");
}
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();
Параметры запроса, связанные с [FromForm], включают маркер защиты от подделки. Маркер защиты проверяется при обработке запроса. Дополнительные сведения см. в разделе Защита от подделки в минимальных API.
Дополнительные сведения см. в статье "Привязка формы" в минимальных API.
Полный пример кода находится в репозитории AspNetCore.Docs.Samples .
Привязка параметров при внедрении зависимостей
Привязка параметров для минимальных API привязывает параметры путем внедрения зависимостей при настройке типа в качестве службы. Явное применение атрибута [FromServices] для параметра не является обязательным. В следующем коде оба действия возвращают время:
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();
Необязательные параметры
Параметры, объявленные в обработчиках маршрутов, считаются обязательными:
- Если запрос соответствует маршруту, то обработчик маршрутов выполняется только в том случае, если в запросе есть все обязательные параметры.
- Отсутствие любого из обязательных параметров приводит к ошибке.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
| URI | результат |
|---|---|
/products?pageNumber=3 |
Возвращено: 3 |
/products |
BadHttpRequestException: обязательный параметр "int pageNumber" не был предоставлен из строки запроса. |
/products/1 |
Ошибка HTTP 404, нет соответствующего маршрута |
Чтобы сделать pageNumber необязательным, определите для него тип optional (необязательный) или укажите значение по умолчанию:
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 | результат |
|---|---|
/products?pageNumber=3 |
Возвращено: 3 |
/products |
Возвращено: 1 |
/products2 |
Возвращено: 1 |
Приведенные выше значения nullable и default применяется ко всем источникам:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
Предыдущий код вызывает метод с параметром product, равным null, если тело запроса не передано.
ПРИМЕЧАНИЕ: если предоставлены недопустимые данные и параметр допускает значение NULL, обработчик маршрутов не выполняется.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
| URI | результат |
|---|---|
/products?pageNumber=3 |
3 вернулся |
/products |
1 вернулся |
/products?pageNumber=two |
BadHttpRequestException: Не удалось выполнить привязку параметра "Nullable<int> pageNumber" для "two". |
/products/two |
Ошибка HTTP 404, нет соответствующего маршрута |
Дополнительные сведения см. в разделе Ошибки привязки.
Специальные типы
Следующие типы привязываются без явно заданных атрибутов:
HttpContext — контекст, содержащий все сведения о текущем HTTP-запросе или ответе:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));HttpRequest и HttpResponse — HTTP-запрос и ответ HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));CancellationToken — маркер отмены, связанный с текущим HTTP-запросом:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));ClaimsPrincipal — пользователь, связанный с запросом, привязанным из HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Свяжите тело запроса как Stream или PipeReader
Тело запроса может привязываться как Stream или PipeReader для эффективной поддержки сценариев, в которых пользователю необходимо обрабатывать данные и:
- Сохраните данные в хранилище BLOB-объектов или поместите данные в очередь в службе очередей.
- Обрабатывать хранимые данные с помощью рабочего процесса или облачной функции.
Например, данные могут быть помещены в очередь в Хранилище очередей Azure или храниться в Хранилище BLOB-объектов Azure.
Следующий код реализует фоновую очередь:
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;
}
Следующий код привязывает текст запроса к 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);
});
Следующий код демонстрирует полный файл 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();
- При чтении данных
Stream— это тот же объект, что иHttpRequest.Body. - Текст запроса по умолчанию не буферизуется. После чтения тело нельзя перемотать назад. Поток не может быть прочитан несколько раз.
-
StreamиPipeReaderнельзя использовать за пределами обработчика минимального действия, так как базовые буферы будут удалены или использованы повторно.
Отправка файлов с помощью IFormFile и IFormFileCollection
Для загрузки файлов с использованием IFormFile и IFormFileCollection в минимальных API требуется кодировка multipart/form-data. Имя параметра в обработчике маршрутов должно соответствовать имени поля формы в запросе. Минимальные API не поддерживают привязку всего текста запроса непосредственно к параметру IFormFile без кодирования формы.
Если необходимо привязать весь текст запроса, например при работе с JSON, двоичными данными или другими типами контента, см. следующее:
Следующий код использует IFormFile и IFormFileCollection, чтобы отправить файл:
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();
Поддерживаются запросы на отправку файлов с проверкой подлинности посредством заголовка авторизации, сертификата клиента или заголовка cookie.
Привязка к формам с помощью IFormCollection, IFormFile и IFormFileCollection
Поддерживается привязка с использованием параметров на основе форм с помощью IFormCollection, IFormFile и IFormFileCollection. Метаданные OpenAPI выводятся для параметров формы для поддержки интеграции с пользовательским интерфейсом Swagger.
Следующий код загружает файлы с использованием автоматически определяемой привязки типа 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();
Предупреждение: При реализации форм приложение должно предотвращать атаки межсайтовой подделки запросов (XSRF/CSRF). В приведенном выше коде IAntiforgery служба используется для предотвращения атак XSRF путем создания и проверки маркера защиты от подделки:
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();
Дополнительные сведения о атаках XSRF см. в статье "Антифоргерия с минимальными API"
Дополнительные сведения см. по теме "Связывание форм" в контексте минимальных API;
Поведение привязки коллекции IFormFile
В следующей таблице показано, как разные IFormFile типы параметров коллекции привязаны к минимальным API. Общее правило о том, что имя параметра в обработчике маршрута должно совпадать с именем поля формы, относится к IFormFile и поддерживаемым именованным коллекциям файлов, таким как IReadOnlyList<T>, но не к IFormFileCollection, который связывает все отправленные файлы.
| Тип параметра | Привязанное значение | Учитывает имя параметра? |
|---|---|---|
IFormFileCollection |
Все файлы в HttpContext.Request.Form.Files |
Нет |
IFormFile |
Один файл, имя поля формы которого соответствует имени параметра. | Yes |
IReadOnlyList<IFormFile> |
Все файлы, имя поля формы которых соответствует имени параметра | Yes |
Другие IFormFile типы коллекций (IEnumerable<IFormFile>, List<IFormFile>, IFormFile[]и т. д.) |
Не поддерживается— параметр не заполняется | N/A |
Используйте IFormFileCollection , если вам нужны все отправленные файлы независимо от имени поля формы. Используйте IReadOnlyList<T> , если вам нужны только файлы, имя поля формы которого соответствует имени параметра.
Примечание.
Правила привязки коллекций IFormFile, приведённые в предыдущей таблице, также применяются к свойствам типов [AsParameters] и сложных типов, сопоставленных с формой, поскольку слой сопоставления с формой является общим для Blazor.
Привязка к коллекциям и сложным типам из форм
Привязка поддерживается для:
- Коллекции, например list and Dictionary
- Сложные типы, например
TodoилиProject
В коде демонстрируется следующее.
- Минимальная конечная точка, которая привязывает многокомпонентные входные данные формы к сложному объекту.
- Как использовать службы антифоргерии для поддержки создания и проверки маркеров антифоргерии.
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));
}
В предыдущем коде:
- Целевой параметр должен быть аннотирован атрибутом
[FromForm], чтобы отличать его от параметров, которые следует считывать из тела JSON. - Привязка из сложных типов или типов коллекций не поддерживается для минимальных API, скомпилированных с помощью генератора запросов делегатов.
- В разметке показаны дополнительные скрытые входные данные с именем
isCompletedи значениемfalse.isCompletedЕсли флажок установлен при отправке формы, оба значенияtrueиfalseотправляются в виде значений. Если флажок снят, отправляется только значение скрытого поля вводаfalse. Процесс привязки модели в ASP.NET Core считывает только первое значение при привязке к значениюbool, в результате чего для установленных флажков используетсяtrue, а для снятых —false.
Пример данных формы, отправленных в предыдущую конечную точку, выглядит следующим образом:
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
Привязка массивов и строковых значений из заголовков и строк запроса
Следующий код демонстрирует привязку строк запроса к массиву примитивных типов, массивам строк и 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]}");
Привязка строк запроса или значений заголовков к массиву сложных типов поддерживается, если для типа реализовать TryParse. Следующий код выполняет привязку к массиву строк и возвращает все элементы с указанными тегами.
// 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();
});
В следующем коде показана модель и требуемая реализация 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;
}
}
В следующем коде выполняется привязка к массиву int:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Чтобы протестировать предыдущий код, добавьте следующую конечную точку, чтобы заполнить базу данных элементами Todo.
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Используйте такой инструмент, как 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"
}
}
]
В следующем коде выполняется привязка к ключу заголовка X-Todo-Id и возвращаются элементы Todo с соответствующими значениями Id.
// 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();
});
Примечание.
При привязке string[] из строки запроса отсутствие соответствующего значения строки запроса приведет к пустому массиву вместо значения NULL.
Привязка параметров для списков аргументов с помощью [AsParameters]
AsParametersAttribute обеспечивает простую привязку параметров к типам, а не сложную или рекурсивную привязку модели.
Рассмотрим следующий код:
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.
Рассмотрим следующую конечную точку 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());
Для замены указанных выше выделенных параметров можно использовать следующую структуру (struct):
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
Оптимизированная конечная точка GET использует приведенную выше структуру (struct) с атрибутом 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());
В приведенном ниже коде показаны дополнительные конечные точки в приложении:
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();
});
Для рефакторинга списков параметров используются следующие классы:
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!;
}
В следующем фрагменте кода показаны эндпоинты, переработанные с помощью AsParameters, а также предшествующие им 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();
});
Для замены предыдущих параметров можно использовать следующие типы record:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
Использование struct с AsParameters может обеспечить лучшую производительность, чем использование типа record.
Полный пример кода приведен в репозитории AspNetCore.Docs.Samples.
Настраиваемая привязка
Существует три способа настройки привязки параметров:
- Если в качестве источника привязки маршрутов используется маршрут, запрос или заголовок, привяжите пользовательские типы путем добавления статического метода
TryParseдля нужного типа. - Управление процессом привязки осуществляется путем реализации метода
BindAsyncдля этого типа. - Для расширенных сценариев реализуйте интерфейс IBindableFromHttpContext<TSelf>, чтобы предоставить пользовательскую логику привязки непосредственно из
HttpContext.
TryParse
TryParse имеет два API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Следующий код отображает Point: 12.3, 10.1 с 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 имеет следующие API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Следующий код отображает SortBy:xyz, SortDirection:Desc, CurrentPage:99 с 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
}
Настраиваемая привязка параметров с помощью IBindableFromHttpContext
ASP.NET Core обеспечивает поддержку пользовательской привязки параметров в минимальных API с помощью IBindableFromHttpContext<TSelf> интерфейса. Этот интерфейс, представленный статическими абстрактными элементами C# 11, позволяет создавать типы, которые могут быть привязаны из контекста HTTP непосредственно в параметрах обработчика маршрутов.
public interface IBindableFromHttpContext<TSelf>
where TSelf : class, IBindableFromHttpContext<TSelf>
{
static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}
Реализуя IBindableFromHttpContext<TSelf>, вы можете создать пользовательские типы, которые обрабатывают собственную логику привязки из HttpContext. Если обработчик маршрута включает параметр этого типа, платформа автоматически вызывает статический BindAsync метод для создания экземпляра:
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}";
});
Ниже приведен пример реализации пользовательского параметра, который привязывается из заголовка 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
});
}
}
Вы также можете реализовать проверку в пользовательской логике привязки:
app.MapGet("/validated", (ValidatedParameter param) =>
{
if (string.IsNullOrEmpty(param.Value))
{
return Results.BadRequest("Value cannot be empty");
}
return Results.Ok($"Validated value: {param.Value}");
});
Просмотр или скачивание примера кода (как скачать)
Ошибки привязки
Если привязка выполняется неудачно, платформа регистрирует сообщение отладки и возвращает клиенту коды состояния, которые могут быть разными в зависимости от условий сбоя.
| Режим сбоя | Тип параметра, допускающий значение NULL | Источник привязки | Код состояния |
|---|---|---|---|
{ParameterType}.TryParse возвращает false |
да | Маршрут, запрос или заголовок | 400 |
{ParameterType}.BindAsync возвращает null |
да | пользовательский | 400 |
{ParameterType}.BindAsync вызывает |
Всё равно | пользовательский | 500 |
| Не удалось десериализовать текст JSON | Всё равно | тело | 400 |
Неправильный тип содержимого (не application/json) |
Всё равно | тело | 415 |
Приоритет привязки
Правила для определения источника привязки на основе параметра:
- Явный атрибут, определенный для атрибутов параметра (From*) в следующем порядке:
- значения маршрута:
[FromRoute]; - Строка запроса:
[FromQuery] - заголовок:
[FromHeader]; - Текст:
[FromBody] - Форма:
[FromForm] - Служба:
[FromServices] - Значения параметров:
[AsParameters]
- значения маршрута:
- Специальные типы
HttpContext-
HttpRequest(HttpContext.Request) -
HttpResponse(HttpContext.Response) -
ClaimsPrincipal(HttpContext.User) -
CancellationToken(HttpContext.RequestAborted) -
IFormCollection(HttpContext.Request.Form) -
IFormFileCollection(HttpContext.Request.Form.Files) -
IFormFile(HttpContext.Request.Form.Files[paramName]) -
Stream(HttpContext.Request.Body) -
PipeReader(HttpContext.Request.BodyReader)
- У типа параметра есть корректный статический метод
BindAsync. - Тип параметра является строкой или имеет допустимый статический
TryParseметод.- Если имя параметра существует в шаблоне маршрута, например,
app.Map("/todo/{id}", (int id) => {});оно привязано к маршруту. - Привязывается из строки запроса.
- Если имя параметра существует в шаблоне маршрута, например,
- Если тип параметра является службой, предоставляемой путем внедрения зависимостей, то в качестве источника он использует эту службу.
- Параметр извлекается из текста запроса.
Настройка параметров десериализации JSON для привязки текста
Источник привязки тела используется System.Text.Json для десериализации. Изменить этот параметр по умолчанию невозможно, но можно настроить параметры сериализации и десериализации JSON.
Настройка параметров десериализации JSON глобально
Параметры, применяемые глобально для приложения, можно настроить, вызвав ConfigureHttpJsonOptions. В следующем примере содержатся общедоступные поля и форматы выходных данных 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
// }
Так как пример кода настраивает сериализацию и десериализацию, он может считывать NameField и включать NameField в выходной КОД JSON.
Настройка параметров десериализации JSON для конечной точки
ReadFromJsonAsync имеет перегрузки, принимающие объект JsonSerializerOptions. В следующем примере содержатся общедоступные поля и форматы выходных данных 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
// }
Поскольку приведенный выше код применяет пользовательские параметры только к десериализации, выходной JSON не включает NameField.
Прочитать тело запроса
Считайте текст запроса напрямую, используя параметр HttpContext или HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
Предыдущий код:
- Обращается к тексту запроса с помощью HttpRequest.BodyReader.
- Копирует текст запроса в локальный файл.
Привязка параметров — это процесс преобразования данных запроса в строго типизированные параметры, выраженные обработчиками маршрутов. Источник привязки определяет, откуда будут привязаны параметры. Источники привязки могут быть явными или выведенными на основе метода HTTP и типа параметра.
Поддерживаемые источники привязки:
- значения маршрута;
- Строка запроса
- Заголовок
- текст (в формате JSON);
- службы, предоставляемые через внедрение зависимостей
- Пользовательское
Привязка по значениям формы не поддерживается изначально в .NET 6 и 7.
В следующем GET обработчике маршрутов используются некоторые из этих источников привязки параметров:
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 { }
В следующей таблице показана связь между параметрами, используемыми в предыдущем примере, и связанными источниками привязки.
| Параметр | Источник привязки |
|---|---|
id |
Значение маршрута |
page |
строка запроса |
customHeader |
заголовок |
service |
Предоставляется путем внедрения зависимостей |
Методы HTTP GET, HEAD, OPTIONS и DELETE не выполняют неявную привязку из текста запроса. Чтобы выполнить привязку из тела (в формате JSON) для этих HTTP-методов, нужно явно указать привязку с помощью [FromBody] или прочитать данные из HttpRequest.
В следующем примере обработчик маршрута POST использует источник привязки — тело запроса (в формате JSON) — для параметра person:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Все параметры в предыдущих примерах автоматически привязываются из данных запроса. Чтобы продемонстрировать удобство привязки параметров, в следующих обработчиках маршрутов показано, как считывать данные запроса непосредственно из запроса:
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>();
// ...
});
Явная привязка параметров
Атрибуты можно использовать для явного объявления источника, из которого привязываются параметры.
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);
| Параметр | Источник привязки |
|---|---|
id |
Значение маршрута с именем id |
page |
Строка запроса с именем "p" |
service |
Предоставляется путем внедрения зависимостей |
contentType |
Заголовок с именем "Content-Type" |
Примечание.
Привязка по значениям формы не поддерживается изначально в .NET 6 и 7.
Привязка параметров при внедрении зависимостей
Привязка параметров для минимальных API привязывает параметры путем внедрения зависимостей при настройке типа в качестве службы. Явное применение атрибута [FromServices] для параметра не является обязательным. В следующем коде оба действия возвращают время:
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();
Необязательные параметры
Параметры, объявленные в обработчиках маршрутов, считаются обязательными:
- Если запрос соответствует маршруту, то обработчик маршрутов выполняется только в том случае, если в запросе есть все обязательные параметры.
- Отсутствие любого из обязательных параметров приводит к ошибке.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
| URI | результат |
|---|---|
/products?pageNumber=3 |
Возвращено: 3 |
/products |
BadHttpRequestException: Обязательный параметр "int pageNumber" не указан в строке запроса. |
/products/1 |
Ошибка HTTP 404, нет соответствующего маршрута |
Чтобы сделать pageNumber необязательным, определите для него тип optional (необязательный) или укажите значение по умолчанию:
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 | результат |
|---|---|
/products?pageNumber=3 |
Возвращено: 3 |
/products |
Возвращено: 1 |
/products2 |
Возвращено: 1 |
Приведенные выше значения nullable и default применяется ко всем источникам:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
Предыдущий код вызывает метод с параметром product, равным null, если тело запроса не передано.
ПРИМЕЧАНИЕ: если предоставлены недопустимые данные и параметр допускает значение NULL, обработчик маршрутов не выполняется.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
| URI | результат |
|---|---|
/products?pageNumber=3 |
3 вернулся |
/products |
1 вернулся |
/products?pageNumber=two |
BadHttpRequestException: Не удалось выполнить привязку параметра "Nullable<int> pageNumber" для "two". |
/products/two |
Ошибка HTTP 404, нет соответствующего маршрута |
Дополнительные сведения см. в разделе Ошибки привязки.
Специальные типы
Следующие типы привязываются без явно заданных атрибутов:
HttpContext — контекст, содержащий все сведения о текущем HTTP-запросе или ответе:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));HttpRequest и HttpResponse — HTTP-запрос и ответ HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));CancellationToken — маркер отмены, связанный с текущим HTTP-запросом:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));ClaimsPrincipal — пользователь, связанный с запросом, привязанным из HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Свяжите тело запроса как Stream или PipeReader
Тело запроса может привязываться как Stream или PipeReader для эффективной поддержки сценариев, в которых пользователю необходимо обрабатывать данные и:
- Сохраните данные в хранилище BLOB-объектов или поместите данные в очередь в службе очередей.
- Обрабатывать хранимые данные с помощью рабочего процесса или облачной функции.
Например, данные могут быть помещены в очередь в Хранилище очередей Azure или храниться в Хранилище BLOB-объектов Azure.
Следующий код реализует фоновую очередь:
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;
}
Следующий код привязывает текст запроса к 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);
});
Следующий код демонстрирует полный файл 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();
- При чтении данных
Stream— это тот же объект, что иHttpRequest.Body. - Текст запроса по умолчанию не буферизуется. После чтения тело нельзя перемотать назад. Поток не может быть прочитан несколько раз.
-
StreamиPipeReaderнельзя использовать за пределами обработчика минимального действия, так как базовые буферы будут удалены или использованы повторно.
Отправка файлов с помощью IFormFile и IFormFileCollection
Следующий код использует IFormFile и IFormFileCollection, чтобы отправить файл:
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();
Поддерживаются запросы на отправку файлов с проверкой подлинности посредством заголовка авторизации, сертификата клиента или заголовка cookie.
Отсутствует встроенная поддержка защиты от подделки в ASP.NET Core в .NET 7.
Антифальсификация доступна в ASP.NET Core в версии .NET 8 или более поздней версии. Однако ее можно реализовать с помощью службы IAntiforgery.
Привязка массивов и строковых значений из заголовков и строк запроса
Следующий код демонстрирует привязку строк запроса к массиву примитивных типов, массивам строк и 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]}");
Привязка строк запроса или значений заголовков к массиву сложных типов поддерживается, если для типа реализовать TryParse. Следующий код выполняет привязку к массиву строк и возвращает все элементы с указанными тегами.
// 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();
});
В следующем коде показана модель и требуемая реализация 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;
}
}
В следующем коде выполняется привязка к массиву int:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Чтобы протестировать предыдущий код, добавьте следующую конечную точку, чтобы заполнить базу данных элементами Todo.
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Используйте инструмент для тестирования API, например 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"
}
}
]
В следующем коде выполняется привязка к ключу заголовка X-Todo-Id и возвращаются элементы Todo с соответствующими значениями Id.
// 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();
});
Примечание.
При привязке string[] из строки запроса отсутствие соответствующего значения строки запроса приведет к пустому массиву вместо значения NULL.
Привязка параметров для списков аргументов с помощью [AsParameters]
AsParametersAttribute обеспечивает простую привязку параметров к типам, а не сложную или рекурсивную привязку модели.
Рассмотрим следующий код:
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.
Рассмотрим следующую конечную точку 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());
Для замены указанных выше выделенных параметров можно использовать следующую структуру (struct):
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
Оптимизированная конечная точка GET использует приведенную выше структуру (struct) с атрибутом 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());
В приведенном ниже коде показаны дополнительные конечные точки в приложении:
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();
});
Для рефакторинга списков параметров используются следующие классы:
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!;
}
В следующем фрагменте кода показаны эндпоинты, переработанные с помощью AsParameters, а также предшествующие им 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();
});
Для замены предыдущих параметров можно использовать следующие типы record:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
Использование struct с AsParameters может обеспечить лучшую производительность, чем использование типа record.
Полный пример кода приведен в репозитории AspNetCore.Docs.Samples.
Настраиваемая привязка
Существует три способа настройки привязки параметров:
- Если в качестве источника привязки маршрутов используется маршрут, запрос или заголовок, привяжите пользовательские типы путем добавления статического метода
TryParseдля нужного типа. - Управление процессом привязки осуществляется путем реализации метода
BindAsyncдля этого типа. - Для расширенных сценариев реализуйте интерфейс IBindableFromHttpContext<TSelf>, чтобы предоставить пользовательскую логику привязки непосредственно из
HttpContext.
TryParse
TryParse имеет два API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Следующий код отображает Point: 12.3, 10.1 с 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 имеет следующие API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Следующий код отображает SortBy:xyz, SortDirection:Desc, CurrentPage:99 с 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
}
Настраиваемая привязка параметров с помощью IBindableFromHttpContext
ASP.NET Core обеспечивает поддержку пользовательской привязки параметров в минимальных API с помощью IBindableFromHttpContext<TSelf> интерфейса. Этот интерфейс, представленный статическими абстрактными элементами C# 11, позволяет создавать типы, которые могут быть привязаны из контекста HTTP непосредственно в параметрах обработчика маршрутов.
public interface IBindableFromHttpContext<TSelf>
where TSelf : class, IBindableFromHttpContext<TSelf>
{
static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}
Реализуя IBindableFromHttpContext<TSelf> интерфейс, можно создавать пользовательские типы, обрабатывающие собственную логику привязки из HttpContext. Если обработчик маршрута включает параметр этого типа, платформа автоматически вызывает статический метод BindAsync для создания экземпляра:
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}";
});
Ниже приведен пример реализации пользовательского параметра, который привязывается из заголовка 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
});
}
}
Вы также можете реализовать проверку в пользовательской логике привязки:
app.MapGet("/validated", (ValidatedParameter param) =>
{
if (string.IsNullOrEmpty(param.Value))
{
return Results.BadRequest("Value cannot be empty");
}
return Results.Ok($"Validated value: {param.Value}");
});
Просмотр или скачивание примера кода (как скачать)
Ошибки привязки
Если привязка выполняется неудачно, платформа регистрирует сообщение отладки и возвращает клиенту коды состояния, которые могут быть разными в зависимости от условий сбоя.
| Режим сбоя | Тип параметра, допускающий значение NULL | Источник привязки | Код состояния |
|---|---|---|---|
{ParameterType}.TryParse возвращает false |
да | Маршрут, запрос или заголовок | 400 |
{ParameterType}.BindAsync возвращает null |
да | пользовательский | 400 |
{ParameterType}.BindAsync вызывает |
Не имеет значения | пользовательский | 500 |
| Не удалось десериализовать текст JSON | Не имеет значения | тело | 400 |
Неправильный тип содержимого (не application/json) |
Не имеет значения | тело | 415 |
Приоритет привязки
Правила для определения источника привязки на основе параметра:
- Явный атрибут, определенный для атрибутов параметра (From*) в следующем порядке:
- значения маршрута:
[FromRoute]; - Строка запроса:
[FromQuery] - заголовок:
[FromHeader]; - Текст:
[FromBody] - Служба:
[FromServices] - Значения параметров:
[AsParameters]
- значения маршрута:
- Специальные типы
HttpContext-
HttpRequest(HttpContext.Request) -
HttpResponse(HttpContext.Response) -
ClaimsPrincipal(HttpContext.User) -
CancellationToken(HttpContext.RequestAborted) -
IFormFileCollection(HttpContext.Request.Form.Files) -
IFormFile(HttpContext.Request.Form.Files[paramName]) -
Stream(HttpContext.Request.Body) -
PipeReader(HttpContext.Request.BodyReader)
- У типа параметра есть корректный статический метод
BindAsync. - Тип параметра является строкой или имеет допустимый статический
TryParseметод.- Если имя параметра существует в шаблоне маршрута. В
app.Map("/todo/{id}", (int id) => {});,idпривязана к маршруту. - Привязывается из строки запроса.
- Если имя параметра существует в шаблоне маршрута. В
- Если тип параметра является службой, предоставляемой путем внедрения зависимостей, то в качестве источника он использует эту службу.
- Параметр извлекается из текста запроса.
Настройка параметров десериализации JSON для привязки текста
Источник привязки тела используется System.Text.Json для десериализации. Изменить этот параметр по умолчанию невозможно, но можно настроить параметры сериализации и десериализации JSON.
Настройка параметров десериализации JSON глобально
Параметры, применяемые глобально для приложения, можно настроить, вызвав ConfigureHttpJsonOptions. В следующем примере содержатся общедоступные поля и форматы выходных данных 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
// }
Так как пример кода настраивает сериализацию и десериализацию, он может считывать NameField и включать NameField в выходной КОД JSON.
Настройка параметров десериализации JSON для конечной точки
ReadFromJsonAsync имеет перегрузки, принимающие объект JsonSerializerOptions. В следующем примере содержатся общедоступные поля и форматы выходных данных 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
// }
Поскольку приведенный выше код применяет пользовательские параметры только к десериализации, выходной JSON не включает NameField.
Прочитать тело запроса
Считайте текст запроса напрямую, используя параметр HttpContext или HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
Предыдущий код:
- Обращается к тексту запроса с помощью HttpRequest.BodyReader.
- Копирует текст запроса в локальный файл.
ASP.NET Core