Typed results (Results<>) против IResult против IActionResult в ASP.NET Core 11
В ASP.NET Core 11 возвращайте Results<T1, TN> с TypedResults для minimal API и ActionResult<T> для контроллеров. Относитесь к голым IResult и IActionResult как к аварийным выходам: они компилируются для любого ответа, но ничего не сообщают OpenAPI, так что за них вы расплачиваетесь написанными вручную атрибутами ProducesResponseType.
Если у вашей конечной точки есть один возможный ответ, объявите этот один конкретный тип и двигайтесь дальше. Если их несколько, точный ответ в ASP.NET Core 11 таков: возвращайте Results<TResult1, TResultN> с TypedResults из minimal API и ActionResult<T> из контроллера. Оба варианта дают проверку на этапе компиляции того, что обработчик возвращает только то, что объявляет, и оба бесплатно передают генератору OpenAPI метаданные ответа. Два интерфейсных типа, голый IResult и голый IActionResult, являются аварийными выходами: они компилируются независимо от того, что вы возвращаете, и именно поэтому они ничего не сообщают фреймворку и вынуждают вас писать вручную [ProducesResponseType] или .Produces, чтобы получить точную спецификацию. Всё, что описано ниже, ориентировано на .NET 11 с Microsoft.NET.Sdk.Web и C# 14; типы HttpResults ведут себя одинаково начиная с .NET 7, так что тот же код без изменений работает на .NET 10 GA.
Три претендента из заголовка очереди относятся к двум разным мирам. IActionResult — это мир контроллеров MVC. IResult и его типизированное объединение Results<> — это мир minimal API, построенный на пространстве имён Microsoft.AspNetCore.Http.HttpResults. Нюанс, ради которого стоит писать это сравнение, в том, что начиная с .NET 7 типы HttpResults работают и в контроллерах, так что в действии контроллера у вас теперь есть настоящий выбор между типами результатов MVC и типами minimal API. Выбрать правильно — значит понимать, что каждый тип несёт, а что нет.
Матрица возможностей
| Возможность | IActionResult | ActionResult<T> | IResult (голый) | Results<T1, TN> |
|---|---|---|---|---|
| Основной дом | Контроллеры | Контроллеры | Minimal API + контроллеры | Minimal API + контроллеры |
| Самоописание для OpenAPI | Нет | Частично (выводит T) | Нет | Да |
Нужен [ProducesResponseType] / .Produces | Да, обильно | Для статус-кодов, отличных от T | Да | Нет |
| Проверка возвращаемого типа при компиляции | Нет | Нет | Нет | Да |
| Согласование содержимого / форматтеры | Да | Да | Нет | Нет |
| Неявное приведение из типа полезной нагрузки | Нет (интерфейс) | Да (T в ActionResult<T>) | Нет | Да (каждый аргумент объединения) |
| Результат, напрямую пригодный для модульного тестирования | Требуется приведение | Требуется приведение | Требуется приведение | Конкретный .Result |
Прочитайте матрицу сверху вниз, и закономерность станет ясна. Две интерфейсные строки дают “Нет” в каждом столбце про метаданные и безопасность. Две типизированные строки оправдывают свою многословность, превращая “Нет” в “Да”. Единственный столбец, где интерфейсы и ActionResult<T> выигрывают у типов HttpResults, — это согласование содержимого, и именно эта строка иногда делает выбор за вас. Подробнее об этом ниже.
Когда выбирать Results<> (и TypedResults)
Тянитесь к объединению всякий раз, когда конечная точка minimal API может ответить более чем одной формой.
- Конечная точка minimal API с
200и404, в .NET 11. ОбъявитеResults<Ok<Todo>, NotFound>, возвращайтеTypedResults.Ok(todo)иTypedResults.NotFound()и удалите каждый вызов.Produces. Теперь метаданные несёт объединение. - Любая конечная точка, где спецификация должна оставаться честной. Поскольку возвращаемый тип и есть контракт, добавление ветки
400без добавленияBadRequestв объединение является ошибкой компиляции, а не молчаливо устаревшей страницей Swagger. - Контроллеры, где вам нужно то же самоописывающее поведение. Типы
HttpResultsдопустимы в действии контроллера.public Results<NotFound, Ok<Product>> GetById(int id)компилируется и убирает все ваши атрибуты[ProducesResponseType]ровно так же, как это было бы в minimal API.
Вот каноническая форма minimal API:
// .NET 11, C# 14 -- Program.cs
using Microsoft.AspNetCore.Http.HttpResults;
app.MapGet("/todos/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
return todo is null
? TypedResults.NotFound()
: TypedResults.Ok(todo);
});
Никаких .Produces, а сгенерированный документ OpenAPI перечисляет 200 со схемой Todo и 404 без тела, оба выведены из возвращаемого типа. Пошаговое преобразование, потолок в шесть типов и выигрыш при тестировании подробно рассмотрены в как вернуть типизированное объединение Results из конечной точки minimal API; этот пост о том, когда выбирать его вместо альтернатив, а не как его подключить.
Когда выбирать ActionResult
Тянитесь к ActionResult<T>, когда вы пишете действие контроллера с основной полезной нагрузкой успеха и одной или несколькими ветками ошибок.
GETконтроллера, который возвращаетProductили404.ActionResult<Product>позволяет вам напрямую написатьreturn product;(неявное приведение оборачивает его вObjectResult) иreturn NotFound();при промахе.- Вы хотите, чтобы тип успеха выводился в спецификацию без его повторения. С
ActionResult<T>атрибуту[ProducesResponseType(200)]больше не нуженType = typeof(Product); фреймворк читаетT. Документация говорит об этом прямо: “Ожидаемый возвращаемый тип действия выводится изTвActionResult<T>.” - Вам нужно согласование содержимого. Типы результатов MVC проходят через настроенные форматтеры, так что клиент, отправляющий
Accept: application/xml, получает XML, если у вас зарегистрирован форматтер. ТипыHttpResultsэтого не делают вообще.
// .NET 11, C# 14 -- ProductsController.cs
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById(int id)
{
var product = _db.Products.Find(id);
return product is null ? NotFound() : product; // implicit cast T -> ActionResult<T>
}
Причина, по которой ActionResult<T> существует, а IActionResult не может его заменить, — это правило C#, а не решение фреймворка: C# не допускает операторы неявного приведения на интерфейсах. ActionResult<T> — конкретный обобщённый тип, поэтому он может определить неявное преобразование из T, которое позволяет вам написать return product;. IActionResult — интерфейс, поэтому он никогда не сможет. В этом и заключается весь эргономический разрыв между этими двумя.
Когда голый IActionResult или IResult на самом деле правильный выбор
Ни один из интерфейсов не является неправильным, они просто узкие. Используйте их обдуманно, а не по умолчанию.
IActionResult, когда действие действительно возвращает несвязанные типы результатов и вы согласны писать[ProducesResponseType]для каждого. Он остаётся честным выбором для действия, которое может вернуть файл, редирект и тело JSON из трёх веток, где нет единогоT.IResult, когда у вас одноформенная ветка minimal API и вы не хотите расписывать однорукое объединение. Возвращать голыйIResultиз обработчика, который всегда выдаёт только один статус, нормально; вы просто добавляете.Produces, если вас волнует документ.- Совместное использование обработчика между minimal API и контроллером. Типы
HttpResults— единственное семейство результатов, которое компилируется в обеих моделях хостинга, так что общий статический метод, возвращающийIResultили объединениеResults<>, — это способ написать его один раз. Эта переносимость и есть документированная причина, по которой типы существуют за пределами minimal API.
Голая версия IResult в контроллере выглядит так, и обратите внимание, что атрибуты вернулись:
// .NET 11, C# 14 -- ProductsController.cs
[HttpGet("{id}")]
[ProducesResponseType<Product>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IResult GetById(int id)
{
var product = _db.Products.Find(id);
return product is null ? Results.NotFound() : Results.Ok(product);
}
Каждый помощник Results.* возвращает IResult, так что компилятор выводит IResult для обеих веток и никогда не жалуется, а ApiExplorer видит интерфейс, который ничего не говорит о статус-кодах. Вот почему две строки [ProducesResponseType] здесь обязательны и отсутствуют в версии Results<>: метаданным больше неоткуда взяться.
Загвоздка, которая делает выбор за вас: согласование содержимого
Если ваш API должен учитывать заголовки Accept и возвращать XML, CSV или любой формат, отличный от того, который жёстко задан в результате, семейство HttpResults отпадает, и это решение перекрывает всё сказанное выше. Документация прямо утверждает, что типы HttpResults “не задействуют настроенные форматтеры”, и разъясняет следствие: “Некоторые возможности, такие как Content negotiation, недоступны” и “Выдаваемый Content-Type определяется реализацией HttpResults.” TypedResults.Ok(product) будет сериализовать JSON независимо от того, что запросил клиент. Так что внутренний API только для JSON волен использовать Results<> в контроллере и пользоваться самоописывающими метаданными, но публичный API с зарегистрированным форматтером XML вынужден оставаться на ActionResult<T> / IActionResult для конечных точек, которые ведут согласование. Это стена возможностей, а не предпочтение, и именно поэтому она относится к вершине вашего решения, а не к его основанию.
Вторая вынуждающая функция — ваша модель хостинга. Если конечная точка живёт в minimal API, IActionResult и ActionResult<T> вам даже недоступны; это типы MVC, которые зависят от конвейера контроллеров. Выбор там всегда только между IResult и Results<>, и Results<> выигрывает для любой конечной точки с несколькими ответами. Полный компромисс между двумя моделями хостинга изложен в minimal API против контроллеров в ASP.NET Core 11.
Почему типизированные версии не компилируются случайно
Есть одна точка трения, с которой люди сталкиваются с Results<>, и её стоит назвать, чтобы она не читалась как баг. Вывод типов не построит объединение за вас. Это не компилируется:
// .NET 11, C# 14 -- does NOT compile
app.MapGet("/todos/{id}", async (int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
return todo is null
? TypedResults.NotFound() // NotFound
: TypedResults.Ok(todo); // Ok<Todo>
});
TypedResults.NotFound() и TypedResults.Ok(todo) — это разные конкретные типы, так что компилятор не может найти общий тип для тернарного оператора, и у лямбды нет выводимого возвращаемого типа. Голая версия IResult компилировалась только потому, что каждый помощник Results.* уже является IResult, что даёт веткам очевидный общий тип. С TypedResults вы расплачиваетесь за более богатые метаданные, объявляя возвращаемый тип сами: Results<Ok<Todo>, NotFound> для синхронного обработчика или Task<Results<Ok<Todo>, NotFound>> для асинхронного. Это объявление — не шаблонный код, который можно сократить. Это в точности та строка, которую фреймворк читает, чтобы построить спецификацию, в чём и весь смысл.
Та же логика объясняет, почему ActionResult<IEnumerable<Product>> работает, но ActionResult<T> не может обернуть интерфейс, который вы возвращаете напрямую: неявное приведение определено из T, а C# запрещает неявные приведения на интерфейсах, так что возврат экземпляра IEnumerable требует явной обёртки Ok(...). Небольшое правило, иногда удивляющее.
Рекомендация, переформулированная с полной картиной
- Новый minimal API, несколько ответов:
Results<T1, TN>сTypedResults. Проверка при компиляции плюс самоописывающая спецификация OpenAPI, без.Produces. Это выбор по умолчанию, и он должен стать вашим рефлексом. - Новый minimal API, один ответ: один конкретный тип, например
Task<Ok<Todo[]>>. Пропустите объединение, когда нечего различать. - Контроллер, только JSON, хотите метаданные бесплатно:
Results<T1, TN>в контроллере работает и убирает ваши атрибуты. В противном случаеActionResult<T>для классической эргономики контроллеров. - Любая конечная точка, которая должна вести согласование содержимого (XML, CSV, пользовательские типы медиа):
ActionResult<T>илиIActionResult. ТипыHttpResultsне умеют согласование содержимого, и точка. - Голый
IResult/ голыйIActionResult: только аварийные выходы. Тянитесь к ним для действительно разнородных ответов, одноформенных веток, которые вы не хотите расписывать, или кода, разделяемого между моделями хостинга, и принимайте написанные вручную метаданные, которые к ним прилагаются.
Ментальная модель, которую стоит держать: интерфейсный возвращаемый тип принимает что угодно и ничего не документирует, так что фреймворк заставляет вас заново утверждать контракт в атрибутах. Типизированный возвращаемый тип, Results<> или ActionResult<T>, и есть контракт, так что компилятор его обеспечивает, а генератор OpenAPI его читает. Выбирайте типизированный, если только конкретная возможность, почти всегда согласование содержимого, не вынуждает к интерфейсу. Для веток, которые возвращают отказ валидации, подача ProblemHttpResult в объединение сохраняет форму согласованной со встроенным конвейером, описанным в как настроить ответы об ошибках валидации minimal API с помощью IProblemDetailsService.
Похожие материалы
- Как вернуть типизированное объединение Results из конечной точки minimal API в ASP.NET Core 11 о пошаговом преобразовании, потолке в шесть типов и тестировании.
- Minimal API против контроллеров в ASP.NET Core 11 о выборе модели хостинга, который ограничивает, какие возвращаемые типы вам вообще доступны.
- Как выставить OpenAPI без Swashbuckle в ASP.NET Core 11 о встроенном генераторе, который читает эти метаданные.
- Как настроить ответы об ошибках валидации minimal API с помощью IProblemDetailsService в ASP.NET Core 11 о
ProblemHttpResult, который часто присоединяется к объединению. - Как валидировать тела запросов в minimal API без контроллеров в ASP.NET Core 11 о том, куда вписывается
ValidationProblemв наборе ответов.
Источники
- Microsoft Learn, Controller action return types in ASP.NET Core web API (
IActionResult,ActionResult<T>и преимущества его неявного приведения, ограничение неявного приведения на интерфейсах и типыHttpResultsв контроллерах, включая оговорку о согласовании содержимого). - Microsoft Learn, Create responses in Minimal API applications (
TypedResultsпротивResults, объединениеResults<TResult1, TResultN>, операторы неявного приведения, проверка при компиляции и самоописывающие метаданные). - Microsoft Learn, Microsoft.AspNetCore.Http.HttpResults namespace (
Ok<T>,NotFound,BadRequestи перегрузкиResults<>). - dotnet/aspnetcore, Introduce way for route handler delegates to return union results (issue #40672) (оригинальный дизайн объединения
Results<>).
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.