Skip to content

Add TypedResults static factory class for creating typed IResult objects #41161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0f424a4
Adding ResultsOfT
brunolins16 Apr 7, 2022
fa070bc
Updating unit tests
brunolins16 Apr 8, 2022
3989c83
Updating cache
brunolins16 Apr 8, 2022
95c7f93
Fixing unit tests
brunolins16 Apr 8, 2022
3d85d40
Fixing json result
brunolins16 Apr 8, 2022
a12c743
Fixing tests
brunolins16 Apr 8, 2022
8247fea
Updating sample
brunolins16 Apr 8, 2022
008b8e1
Updating sample
brunolins16 Apr 8, 2022
e1f6ccd
Api updates
brunolins16 Apr 9, 2022
877d093
Updating helper
brunolins16 Apr 10, 2022
1825177
Post-rebase fixes
DamianEdwards Apr 11, 2022
51a5623
Implement IEndpointMetadataProvider on result types
DamianEdwards Apr 12, 2022
5422e68
WIP
DamianEdwards Apr 12, 2022
0db5208
React to API review & misc clean-up
DamianEdwards Apr 13, 2022
0ca6682
Make Results factory methods call through to TypedResults
DamianEdwards Apr 13, 2022
40e5e2c
Fix docs & PublicAPI
DamianEdwards Apr 13, 2022
adba699
Fix build errors
DamianEdwards Apr 13, 2022
14dc834
More doc comment fixes
DamianEdwards Apr 13, 2022
c20d4c9
Update MinimalSample
DamianEdwards Apr 13, 2022
cbc2b34
Test that every Results.*** method returns expected IResult type
DamianEdwards Apr 13, 2022
a6637f1
Added tests for Results.Accepted & Results.AcceptedAtRotue
DamianEdwards Apr 13, 2022
13a1b65
Add more Results tests
DamianEdwards Apr 14, 2022
9c81ba7
More tests
DamianEdwards Apr 14, 2022
67a68cf
More tests
DamianEdwards Apr 14, 2022
dad3b43
More tests
DamianEdwards Apr 15, 2022
3aa81b8
Add tests for result types implementing IEndpointMetadataProvider
DamianEdwards Apr 15, 2022
fda085c
Added ValidationProblem tests
DamianEdwards Apr 15, 2022
b9b5dd3
Add more results tests
DamianEdwards Apr 16, 2022
2c4d687
Added tests for TypedResults
DamianEdwards Apr 16, 2022
36b40bc
Revert changes to MinimalSample
DamianEdwards Apr 16, 2022
38bead6
Remove MinimalSample from RequiresDelayedBuildProjects
DamianEdwards Apr 16, 2022
60943eb
Fix test on Unix
DamianEdwards Apr 16, 2022
f551d7b
Consistency fixes
DamianEdwards Apr 18, 2022
bed172d
Update googletest
DamianEdwards Apr 18, 2022
d0dc8c7
PR feedback
DamianEdwards Apr 18, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/Http/Http.Results/src/Accepted.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Http.HttpResults;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with status code Accepted (202) and Location header.
/// Targets a registered route.
/// </summary>
public sealed class Accepted : IResult, IEndpointMetadataProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
/// provided.
/// </summary>
/// <param name="location">The location at which the status of requested content can be monitored.</param>
internal Accepted(string? location)
{
Location = location;
}

/// <summary>
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
/// provided.
/// </summary>
/// <param name="locationUri">The location at which the status of requested content can be monitored.</param>
internal Accepted(Uri locationUri)
{
if (locationUri == null)
{
throw new ArgumentNullException(nameof(locationUri));
}

if (locationUri.IsAbsoluteUri)
{
Location = locationUri.AbsoluteUri;
}
else
{
Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
}
}

/// <summary>
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
/// </summary>
public int StatusCode => StatusCodes.Status202Accepted;

/// <summary>
/// Gets the location at which the status of the requested content can be monitored.
/// </summary>
public string? Location { get; }

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
{
// Creating the logger with a string to preserve the category after the refactoring.
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.AcceptedResult");

if (!string.IsNullOrEmpty(Location))
{
httpContext.Response.Headers.Location = Location;
}

HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return Task.CompletedTask;
}

/// <inheritdoc/>
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
{
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted));
}
}
89 changes: 89 additions & 0 deletions src/Http/Http.Results/src/AcceptedAtRoute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Http.HttpResults;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with status code Accepted (202) and Location header.
/// Targets a registered route.
/// </summary>
public sealed class AcceptedAtRoute : IResult, IEndpointMetadataProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
/// provided.
/// </summary>
/// <param name="routeValues">The route data to use for generating the URL.</param>
internal AcceptedAtRoute(object? routeValues)
: this(routeName: null, routeValues: routeValues)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
/// provided.
/// </summary>
/// <param name="routeName">The name of the route to use for generating the URL.</param>
/// <param name="routeValues">The route data to use for generating the URL.</param>
internal AcceptedAtRoute(
string? routeName,
object? routeValues)
{
RouteName = routeName;
RouteValues = new RouteValueDictionary(routeValues);
}

/// <summary>
/// Gets the name of the route to use for generating the URL.
/// </summary>
public string? RouteName { get; }

/// <summary>
/// Gets the route data to use for generating the URL.
/// </summary>
public RouteValueDictionary RouteValues { get; }

/// <summary>
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
/// </summary>
public int StatusCode => StatusCodes.Status202Accepted;

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
var url = linkGenerator.GetUriByAddress(
httpContext,
RouteName,
RouteValues,
fragment: FragmentString.Empty);

if (string.IsNullOrEmpty(url))
{
throw new InvalidOperationException("No route matches the supplied values.");
}

// Creating the logger with a string to preserve the category after the refactoring.
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.AcceptedAtRouteResult");

httpContext.Response.Headers.Location = url;

HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return Task.CompletedTask;
}

/// <inheritdoc/>
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
{
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted));
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http;

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Http.HttpResults;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with status code Accepted (202) and Location header.
/// Targets a registered route.
/// </summary>
public sealed class AcceptedAtRouteHttpResult : IResult
/// <typeparam name="TValue">The type of object that will be JSON serialized to the response body.</typeparam>
public sealed class AcceptedAtRoute<TValue> : IResult, IEndpointMetadataProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="AcceptedAtRouteHttpResult"/> class with the values
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
/// provided.
/// </summary>
/// <param name="routeValues">The route data to use for generating the URL.</param>
/// <param name="value">The value to format in the entity body.</param>
internal AcceptedAtRouteHttpResult(object? routeValues, object? value)
internal AcceptedAtRoute(object? routeValues, TValue? value)
: this(routeName: null, routeValues: routeValues, value: value)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AcceptedAtRouteHttpResult"/> class with the values
/// Initializes a new instance of the <see cref="AcceptedAtRoute"/> class with the values
/// provided.
/// </summary>
/// <param name="routeName">The name of the route to use for generating the URL.</param>
/// <param name="routeValues">The route data to use for generating the URL.</param>
/// <param name="value">The value to format in the entity body.</param>
internal AcceptedAtRouteHttpResult(
internal AcceptedAtRoute(
string? routeName,
object? routeValues,
object? value)
TValue? value)
{
Value = value;
RouteName = routeName;
Expand All @@ -47,7 +48,7 @@ internal AcceptedAtRouteHttpResult(
/// <summary>
/// Gets the object result.
/// </summary>
public object? Value { get; }
public TValue? Value { get; }

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

/// <summary>
/// Gets the HTTP status code.
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
/// </summary>
public int StatusCode => StatusCodes.Status202Accepted;

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

httpContext.Response.Headers.Location = url;
return HttpResultsHelper.WriteResultAsJsonAsync(httpContext, logger, Value, StatusCode);

HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return HttpResultsHelper.WriteResultAsJsonAsync(httpContext, logger, Value);
}

/// <inheritdoc/>
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
{
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json"));
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http;

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Http.HttpResults;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with status code Accepted (202) and Location header.
/// Targets a registered route.
/// </summary>
public sealed class AcceptedHttpResult : IResult
public sealed class Accepted<TValue> : IResult, IEndpointMetadataProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="AcceptedHttpResult"/> class with the values
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
/// provided.
/// </summary>
/// <param name="location">The location at which the status of requested content can be monitored.</param>
/// <param name="value">The value to format in the entity body.</param>
internal AcceptedHttpResult(string? location, object? value)
internal Accepted(string? location, TValue? value)
{
Value = value;
Location = location;
HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode);
}

/// <summary>
/// Initializes a new instance of the <see cref="AcceptedHttpResult"/> class with the values
/// Initializes a new instance of the <see cref="Accepted"/> class with the values
/// provided.
/// </summary>
/// <param name="locationUri">The location at which the status of requested content can be monitored.</param>
/// <param name="value">The value to format in the entity body.</param>
internal AcceptedHttpResult(Uri locationUri, object? value)
internal Accepted(Uri locationUri, TValue? value)
{
Value = value;
HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode);
Expand All @@ -56,10 +56,10 @@ internal AcceptedHttpResult(Uri locationUri, object? value)
/// <summary>
/// Gets the object result.
/// </summary>
public object? Value { get; }
public TValue? Value { get; }

/// <summary>
/// Gets the HTTP status code.
/// Gets the HTTP status code: <see cref="StatusCodes.Status202Accepted"/>
/// </summary>
public int StatusCode => StatusCodes.Status202Accepted;

Expand All @@ -79,10 +79,19 @@ public Task ExecuteAsync(HttpContext httpContext)
// Creating the logger with a string to preserve the category after the refactoring.
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.AcceptedResult");

HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return HttpResultsHelper.WriteResultAsJsonAsync(
httpContext,
logger,
Value,
StatusCode);
Value);
}

/// <inheritdoc/>
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
{
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json"));
}
}
47 changes: 47 additions & 0 deletions src/Http/Http.Results/src/BadRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Http.HttpResults;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with Bad Request (400) status code.
/// </summary>
public sealed class BadRequest : IResult, IEndpointMetadataProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="BadRequest"/> class with the values
/// provided.
/// </summary>
internal BadRequest()
{
}

/// <summary>
/// Gets the HTTP status code: <see cref="StatusCodes.Status400BadRequest"/>
/// </summary>
public int StatusCode => StatusCodes.Status400BadRequest;

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
{
// Creating the logger with a string to preserve the category after the refactoring.
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.BadRequestObjectResult");

HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return Task.CompletedTask;
}

/// <inheritdoc/>
static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)
{
context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest));
}
}
Loading