Skip to content

Commit 64ccf82

Browse files
Add TypedResults static factory class for creating typed IResult objects (#41161)
- Use the `HttpResult` suffix on `IResult` types except for where the short name has recognized value, e.g. those types that implement `IEndpointMetadataProvider` and will be used in conjunction with `Results<T1, TN>`, e.g. `Results<Ok<Todo>, NotFound>` - Add `TypedResults.xxx` factory class - Added `HttpResults.ValidationProblem` type that represents 400 responses with validation errors (equiv to `BadRequest<HttpValidationProblemDetails>` - Moved `Results<TResult1, TResultN>` types into the `Microsoft.AspNetCore.Http.HttpResults` namespace to match where the other results types are - Changed `Results.xxx` methods to call through to `TypedResults.xxx` methods - Explicitly implemented `IEndpointMetadataProvider` on following `IResult` types: - `Accepted` - `Accepted<TValue>` - `AcceptedAtRoute` - `AcceptedAtRoute<TValue>` - `BadRequest` - `BadRequest<TValue>` - `Conflict` - `Conflict<TValue>` - `Created` - `Created<TValue>` - `CreatedAtRoute` - `CreatedAtRoute<TValue>` - `NoContent` - `NotFound` - `NotFound<TValue>` - `Ok` - `Ok<TValue>` - `UnprocessableEntity` - `UnprocessableEntity<TValue>` - Order `using` statements before `namespace` declarations - Added tests for `Microsoft.AspNetCore.Http.Results` and `Microsoft.AspNetCore.Http.TypedResults` Fixes #41009 Co-authored-by: Bruno Lins de Oliveira <[email protected]>
1 parent 544ff35 commit 64ccf82

File tree

93 files changed

+5430
-972
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+5430
-972
lines changed

src/Http/Http.Results/src/Accepted.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Metadata;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.AspNetCore.Http.HttpResults;
9+
10+
/// <summary>
11+
/// An <see cref="IResult"/> that on execution will write an object to the response
12+
/// with status code Accepted (202) and Location header.
13+
/// Targets a registered route.
14+
/// </summary>
15+
public sealed class Accepted : IResult, IEndpointMetadataProvider
16+
{
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
19+
/// provided.
20+
/// </summary>
21+
/// <param name="location">The location at which the status of requested content can be monitored.</param>
22+
internal Accepted(string? location)
23+
{
24+
Location = location;
25+
}
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
29+
/// provided.
30+
/// </summary>
31+
/// <param name="locationUri">The location at which the status of requested content can be monitored.</param>
32+
internal Accepted(Uri locationUri)
33+
{
34+
if (locationUri == null)
35+
{
36+
throw new ArgumentNullException(nameof(locationUri));
37+
}
38+
39+
if (locationUri.IsAbsoluteUri)
40+
{
41+
Location = locationUri.AbsoluteUri;
42+
}
43+
else
44+
{
45+
Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
46+
}
47+
}
48+
49+
/// <summary>
50+
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
51+
/// </summary>
52+
public int StatusCode => StatusCodes.Status202Accepted;
53+
54+
/// <summary>
55+
/// Gets the location at which the status of the requested content can be monitored.
56+
/// </summary>
57+
public string? Location { get; }
58+
59+
/// <inheritdoc/>
60+
public Task ExecuteAsync(HttpContext httpContext)
61+
{
62+
// Creating the logger with a string to preserve the category after the refactoring.
63+
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
64+
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.AcceptedResult");
65+
66+
if (!string.IsNullOrEmpty(Location))
67+
{
68+
httpContext.Response.Headers.Location = Location;
69+
}
70+
71+
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
72+
httpContext.Response.StatusCode = StatusCode;
73+
74+
return Task.CompletedTask;
75+
}
76+
77+
/// <inheritdoc/>
78+
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
79+
{
80+
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted));
81+
}
82+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Metadata;
5+
using Microsoft.AspNetCore.Routing;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Microsoft.AspNetCore.Http.HttpResults;
10+
11+
/// <summary>
12+
/// An <see cref="IResult"/> that on execution will write an object to the response
13+
/// with status code Accepted (202) and Location header.
14+
/// Targets a registered route.
15+
/// </summary>
16+
public sealed class AcceptedAtRoute : IResult, IEndpointMetadataProvider
17+
{
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
20+
/// provided.
21+
/// </summary>
22+
/// <param name="routeValues">The route data to use for generating the URL.</param>
23+
internal AcceptedAtRoute(object? routeValues)
24+
: this(routeName: null, routeValues: routeValues)
25+
{
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
30+
/// provided.
31+
/// </summary>
32+
/// <param name="routeName">The name of the route to use for generating the URL.</param>
33+
/// <param name="routeValues">The route data to use for generating the URL.</param>
34+
internal AcceptedAtRoute(
35+
string? routeName,
36+
object? routeValues)
37+
{
38+
RouteName = routeName;
39+
RouteValues = new RouteValueDictionary(routeValues);
40+
}
41+
42+
/// <summary>
43+
/// Gets the name of the route to use for generating the URL.
44+
/// </summary>
45+
public string? RouteName { get; }
46+
47+
/// <summary>
48+
/// Gets the route data to use for generating the URL.
49+
/// </summary>
50+
public RouteValueDictionary RouteValues { get; }
51+
52+
/// <summary>
53+
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
54+
/// </summary>
55+
public int StatusCode => StatusCodes.Status202Accepted;
56+
57+
/// <inheritdoc/>
58+
public Task ExecuteAsync(HttpContext httpContext)
59+
{
60+
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
61+
var url = linkGenerator.GetUriByAddress(
62+
httpContext,
63+
RouteName,
64+
RouteValues,
65+
fragment: FragmentString.Empty);
66+
67+
if (string.IsNullOrEmpty(url))
68+
{
69+
throw new InvalidOperationException("No route matches the supplied values.");
70+
}
71+
72+
// Creating the logger with a string to preserve the category after the refactoring.
73+
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
74+
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.AcceptedAtRouteResult");
75+
76+
httpContext.Response.Headers.Location = url;
77+
78+
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
79+
httpContext.Response.StatusCode = StatusCode;
80+
81+
return Task.CompletedTask;
82+
}
83+
84+
/// <inheritdoc/>
85+
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
86+
{
87+
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted));
88+
}
89+
}

src/Http/Http.Results/src/AcceptedAtRouteHttpResult.cs renamed to src/Http/Http.Results/src/AcceptedAtRouteOfT.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
namespace Microsoft.AspNetCore.Http;
5-
6-
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Http.Metadata;
75
using Microsoft.AspNetCore.Routing;
86
using Microsoft.Extensions.DependencyInjection;
97
using Microsoft.Extensions.Logging;
108

9+
namespace Microsoft.AspNetCore.Http.HttpResults;
10+
1111
/// <summary>
1212
/// An <see cref="IResult"/> that on execution will write an object to the response
1313
/// with status code Accepted (202) and Location header.
1414
/// Targets a registered route.
1515
/// </summary>
16-
public sealed class AcceptedAtRouteHttpResult : IResult
16+
/// <typeparam name="TValue">The type of object that will be JSON serialized to the response body.</typeparam>
17+
public sealed class AcceptedAtRoute<TValue> : IResult, IEndpointMetadataProvider
1718
{
1819
/// <summary>
19-
/// Initializes a new instance of the <see cref="AcceptedAtRouteHttpResult"/> class with the values
20+
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
2021
/// provided.
2122
/// </summary>
2223
/// <param name="routeValues">The route data to use for generating the URL.</param>
2324
/// <param name="value">The value to format in the entity body.</param>
24-
internal AcceptedAtRouteHttpResult(object? routeValues, object? value)
25+
internal AcceptedAtRoute(object? routeValues, TValue? value)
2526
: this(routeName: null, routeValues: routeValues, value: value)
2627
{
2728
}
2829

2930
/// <summary>
30-
/// Initializes a new instance of the <see cref="AcceptedAtRouteHttpResult"/> class with the values
31+
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
3132
/// provided.
3233
/// </summary>
3334
/// <param name="routeName">The name of the route to use for generating the URL.</param>
3435
/// <param name="routeValues">The route data to use for generating the URL.</param>
3536
/// <param name="value">The value to format in the entity body.</param>
36-
internal AcceptedAtRouteHttpResult(
37+
internal AcceptedAtRoute(
3738
string? routeName,
3839
object? routeValues,
39-
object? value)
40+
TValue? value)
4041
{
4142
Value = value;
4243
RouteName = routeName;
@@ -47,7 +48,7 @@ internal AcceptedAtRouteHttpResult(
4748
/// <summary>
4849
/// Gets the object result.
4950
/// </summary>
50-
public object? Value { get; }
51+
public TValue? Value { get; }
5152

5253
/// <summary>
5354
/// Gets the name of the route to use for generating the URL.
@@ -60,7 +61,7 @@ internal AcceptedAtRouteHttpResult(
6061
public RouteValueDictionary RouteValues { get; }
6162

6263
/// <summary>
63-
/// Gets the HTTP status code.
64+
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
6465
/// </summary>
6566
public int StatusCode => StatusCodes.Status202Accepted;
6667

@@ -84,6 +85,16 @@ public Task ExecuteAsync(HttpContext httpContext)
8485
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.AcceptedAtRouteResult");
8586

8687
httpContext.Response.Headers.Location = url;
87-
return HttpResultsHelper.WriteResultAsJsonAsync(httpContext, logger, Value, StatusCode);
88+
89+
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
90+
httpContext.Response.StatusCode = StatusCode;
91+
92+
return HttpResultsHelper.WriteResultAsJsonAsync(httpContext, logger, Value);
93+
}
94+
95+
/// <inheritdoc/>
96+
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
97+
{
98+
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json"));
8899
}
89100
}

src/Http/Http.Results/src/AcceptedHttpResult.cs renamed to src/Http/Http.Results/src/AcceptedOfT.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
namespace Microsoft.AspNetCore.Http;
5-
6-
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Http.Metadata;
75
using Microsoft.Extensions.DependencyInjection;
86
using Microsoft.Extensions.Logging;
97

8+
namespace Microsoft.AspNetCore.Http.HttpResults;
9+
1010
/// <summary>
1111
/// An <see cref="IResult"/> that on execution will write an object to the response
1212
/// with status code Accepted (202) and Location header.
1313
/// Targets a registered route.
1414
/// </summary>
15-
public sealed class AcceptedHttpResult : IResult
15+
public sealed class Accepted<TValue> : IResult, IEndpointMetadataProvider
1616
{
1717
/// <summary>
18-
/// Initializes a new instance of the <see cref="AcceptedHttpResult"/> class with the values
18+
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
1919
/// provided.
2020
/// </summary>
2121
/// <param name="location">The location at which the status of requested content can be monitored.</param>
2222
/// <param name="value">The value to format in the entity body.</param>
23-
internal AcceptedHttpResult(string? location, object? value)
23+
internal Accepted(string? location, TValue? value)
2424
{
2525
Value = value;
2626
Location = location;
2727
HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode);
2828
}
2929

3030
/// <summary>
31-
/// Initializes a new instance of the <see cref="AcceptedHttpResult"/> class with the values
31+
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
3232
/// provided.
3333
/// </summary>
3434
/// <param name="locationUri">The location at which the status of requested content can be monitored.</param>
3535
/// <param name="value">The value to format in the entity body.</param>
36-
internal AcceptedHttpResult(Uri locationUri, object? value)
36+
internal Accepted(Uri locationUri, TValue? value)
3737
{
3838
Value = value;
3939
HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode);
@@ -56,10 +56,10 @@ internal AcceptedHttpResult(Uri locationUri, object? value)
5656
/// <summary>
5757
/// Gets the object result.
5858
/// </summary>
59-
public object? Value { get; }
59+
public TValue? Value { get; }
6060

6161
/// <summary>
62-
/// Gets the HTTP status code.
62+
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
6363
/// </summary>
6464
public int StatusCode => StatusCodes.Status202Accepted;
6565

@@ -79,10 +79,19 @@ public Task ExecuteAsync(HttpContext httpContext)
7979
// Creating the logger with a string to preserve the category after the refactoring.
8080
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
8181
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.AcceptedResult");
82+
83+
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
84+
httpContext.Response.StatusCode = StatusCode;
85+
8286
return HttpResultsHelper.WriteResultAsJsonAsync(
8387
httpContext,
8488
logger,
85-
Value,
86-
StatusCode);
89+
Value);
90+
}
91+
92+
/// <inheritdoc/>
93+
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
94+
{
95+
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json"));
8796
}
8897
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Metadata;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.AspNetCore.Http.HttpResults;
9+
10+
/// <summary>
11+
/// An <see cref="IResult"/> that on execution will write an object to the response
12+
/// with Bad Request (400) status code.
13+
/// </summary>
14+
public sealed class BadRequest : IResult, IEndpointMetadataProvider
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="BadRequest"/> class with the values
18+
/// provided.
19+
/// </summary>
20+
internal BadRequest()
21+
{
22+
}
23+
24+
/// <summary>
25+
/// Gets the HTTP status code: <see cref="StatusCodes.Status400BadRequest"/>
26+
/// </summary>
27+
public int StatusCode => StatusCodes.Status400BadRequest;
28+
29+
/// <inheritdoc/>
30+
public Task ExecuteAsync(HttpContext httpContext)
31+
{
32+
// Creating the logger with a string to preserve the category after the refactoring.
33+
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
34+
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.BadRequestObjectResult");
35+
36+
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
37+
httpContext.Response.StatusCode = StatusCode;
38+
39+
return Task.CompletedTask;
40+
}
41+
42+
/// <inheritdoc/>
43+
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
44+
{
45+
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest));
46+
}
47+
}

0 commit comments

Comments
 (0)