Introduce Results.Typed factory methods for creating results whose types properly reflect the response type & shape
See original GitHub issueBackground
The existing Results.xxx()
static factory methods all return IResult
meaning the explicit type information is lost. Even though the in-box types themselves are now public, their constructors are not, meaning having a route handler delegate return an explicit result type requires the result be cast to its actual type, e.g.:
app.MapGet("/todos/{id}", async (int id, TodoDb db) =>
(OkObjectHttpResult)Results.Ok(await db.FindAsync(id));
The introduction of the Results<TResult1, TResultN>
union types presents a compelling reason to allow the creation of the in-box IResult
implementing types in a way that preserves their explicit types. Having to explicitly cast each call to a Results
factory method is somewhat unsightly:
app.MapGet("/todos/{id}", async Task<Results<OkObjectHttpResult, NotFoundObjectHttpResult>> (int id, TodoDb db) =>
await db.FindAsync(id) is Todo todo
? (OkObjectHttpResult)Results.Ok(todo)
: (NotFoundObjectHttpResult)Results.NotFound()
Additionally, the in-box result types today that allow setting an object to be serialized to the response body do not preserve the type information of those objects, including OkObjectHttpResult
and others, e.g.
public static IResult Ok(object? value = null) => new OkObjectHttpResult(value);
This means that even if the result type itself were preserved, the type of the underlying response body is not, and as such an OpenAPI schema cannot be inferred automatically, requiring the developer to manually annotate the endpoint with type information:
app.MapGet("/todos/{id}", async (int id, TodoDb db) =>
(OkObjectHttpResult)Results.Ok(await db.FindAsync(id))
.Produces<Todo>();
In order to enable result types to properly describe the HTTP result they represent, the type must encapsulate the status code, content type, and response body shape statically by the type shape (i.e. without any runtime knowledge).
Proposal
New results types
Introduce new result types that represent all the supported response status codes (within reason) and preserve the type details of the response body via generics. As these result types all serialize their body object to JSON (and no other format is currently supported by the in-box result types) the content type need not be represented in the type shape. An example of the result types being proposed can be found in the MinimalApis.Extensions library here.
Example of what a new generic result representing an OK response might look like:
namespace Microsoft.AspNetCore.Http;
public class OkHttpResult<TValue> : IResult
{
internal OkHttpResult(TValue value)
{
Value = value;
}
public TValue Value { get; init; }
public int StatusCode => StatusCodes.Status200OK;
public async Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.StatusCode = StatusCode;
if (Value is not null)
{
await httpContext.Response.WriteAsJsonAsync(Value);
}
}
}
❓ New result type names
As the type names for these new types will actually appear in code (rather than being inferred by the compiler) some extra thought should be given to their names. The existing result type names are somewhat unwieldly, e.g. OkObjectHttpResult
, NotFoundObjectHttpResult
, and as such don’t lend themselves well to the “minimal” approach. In the MinimalApis.Extensions library, the types are named with the assumption that they will be used in conjunction with the Results<TResult1, TResultN>
union types, and as such the names are very minimal, just using the response type itself, e.g. Ok
, NotFound
, etc.
This allows for fairly terse signatures like Task<Result<Ok<Todo>, NotFound>> (int id)
but this might be a bit too short to accept in the framework. Some other ideas we should consider:
OkHttpResult<T>
,NotFoundHttpResult
, etc.- Putting the new types in their own namespace,
Microsoft.AspNetCore.Http.TypedResults
and having shorter names, e.g.Microsoft.AspNetCore.Http.TypedResults.Ok<TValue>
,Microsoft.AspNetCore.Http.TypedResults.NotFound
- This would allow the types to be used either like
TypedResults.Ok<Todo>
,TypedResults.NotFound
or by importing the namespace explicitly likeOk<Todo>
,NotFound
, etc.
Results.Typed
factory methods
To preserve the existing pattern of creating result types via the static Results
class, we will introduce a new member on the Microsoft.AspNetCore.Http.Results
class called Typed
, upon which are factory methods that use the new result types and preserve the concrete type of the returned results:
namespace Microsoft.AspNetCore.Http;
public static class Results
{
+ public static ITypedResultExtensions Typed { get; } = new TypedResultExtensions();
}
+ internal class TypedResultExtensions : ITypedResultExtensions { }
+ public static class TypedResultExtensionsMethods
+ {
+ public static OkHttpResult<TResult> Ok<TResult>(this ITypedResultExtensions typedResults, TResult? value)
+ {
+ return new OkHttpResult<TResult>(value);
+ }
+
+ // Additional factory result methods
+ }
Example use
app.MapGet("/todos/{id}", async Task<Results<OkHttpResult<Todo>, NotFoundHttpResult>> (int id, TodoDb db) =>
await db.FindAsync(id) is Todo todo
? Results.Typed.Ok(todo)
: Results.Typed.NotFound());
Making the new results self-describe via metadata
These new result types would be updated once #40646 is implemented, such that the result types can self-describe their operation via metadata into ApiExplorer
and through to OpenAPI documents and Swagger UI, resulting in much more of an APIs details being described from just the method type information.
The following two API implementations represent the resulting two different approaches to describing the details of an API for OpenAPI/Swagger, the first using just type information from the route handler delegate, the second requiring explicitly adding type information via metadata. The first means there’s compile-time checking that the responses returned are actually declared in the method signature, whereas the second requires the developer to manually ensure the metadata added matches the actual method implementation:
// API parameters, responses, and response body shape described just by method type information
app.MapGet("/todos/{id}", async Task<Results<OkHttpResult<Todo>, NotFoundHttpResult>> (int id, TodoDb db) =>
await db.FindAsync(id) is Todo todo
? Results.Typed.Ok(todo)
: Results.Typed.NotFound());
// API parameters from method type information, but response details requiring manual metadata specification
app.MapGet("/todos/{id}", async (int id, TodoDb db) =>
await db.FindAsync(id) is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>()
.Produces(StatusCodes.Status404NotFound);
Issue Analytics
- State:
- Created a year ago
- Reactions:1
- Comments:22 (22 by maintainers)
Top GitHub Comments
Here is my proposal:
Typed Results static methods
They are almost the same available at https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Results/src/Results.cs
New IResult types with type parameter
Also, the proposal is to change the current types to have a shorter name, move to a new namespace and remove the
object Value
properties:After a quick look, might missed something, in addition to the example I mentioned we will only have conflict with:
VirtualFileHttpResult -> https://docs.microsoft.com/en-us/dotnet/api/system.web.hosting.virtualfile?view=netframework-4.8 Not in .NET Core JsonHttpResult -> https://docs.microsoft.com/en-us/dotnet/api/system.web.helpers.json?view=aspnet-webpages-3.2 Don’t know if is possible to use it
So, maybe if we decide a better name to
FileStreamHttpResult
other thanFileStream
we might be able to have the short names without problems.