diff --git a/src/Http/Http.Results/src/Accepted.cs b/src/Http/Http.Results/src/Accepted.cs new file mode 100644 index 000000000000..fed67811d4dc --- /dev/null +++ b/src/Http/Http.Results/src/Accepted.cs @@ -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; + +/// +/// An that on execution will write an object to the response +/// with status code Accepted (202) and Location header. +/// Targets a registered route. +/// +public sealed class Accepted : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the status of requested content can be monitored. + internal Accepted(string? location) + { + Location = location; + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the status of requested content can be monitored. + 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); + } + } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status202Accepted; + + /// + /// Gets the location at which the status of the requested content can be monitored. + /// + public string? Location { get; } + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + // Creating the logger with a string to preserve the category after the refactoring. + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + 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; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); + } +} diff --git a/src/Http/Http.Results/src/AcceptedAtRoute.cs b/src/Http/Http.Results/src/AcceptedAtRoute.cs new file mode 100644 index 000000000000..96bf99aab6b9 --- /dev/null +++ b/src/Http/Http.Results/src/AcceptedAtRoute.cs @@ -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; + +/// +/// An that on execution will write an object to the response +/// with status code Accepted (202) and Location header. +/// Targets a registered route. +/// +public sealed class AcceptedAtRoute : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The route data to use for generating the URL. + internal AcceptedAtRoute(object? routeValues) + : this(routeName: null, routeValues: routeValues) + { + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + internal AcceptedAtRoute( + string? routeName, + object? routeValues) + { + RouteName = routeName; + RouteValues = new RouteValueDictionary(routeValues); + } + + /// + /// Gets the name of the route to use for generating the URL. + /// + public string? RouteName { get; } + + /// + /// Gets the route data to use for generating the URL. + /// + public RouteValueDictionary RouteValues { get; } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status202Accepted; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + 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(); + 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; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); + } +} diff --git a/src/Http/Http.Results/src/AcceptedAtRouteHttpResult.cs b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs similarity index 74% rename from src/Http/Http.Results/src/AcceptedAtRouteHttpResult.cs rename to src/Http/Http.Results/src/AcceptedAtRouteOfT.cs index 0e45e84164ce..4855b6e2c89a 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteHttpResult.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs @@ -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; + /// /// An that on execution will write an object to the response /// with status code Accepted (202) and Location header. /// Targets a registered route. /// -public sealed class AcceptedAtRouteHttpResult : IResult +/// The type of object that will be JSON serialized to the response body. +public sealed class AcceptedAtRoute : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The route data to use for generating the URL. /// The value to format in the entity body. - internal AcceptedAtRouteHttpResult(object? routeValues, object? value) + internal AcceptedAtRoute(object? routeValues, TValue? value) : this(routeName: null, routeValues: routeValues, value: value) { } /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// The value to format in the entity body. - internal AcceptedAtRouteHttpResult( + internal AcceptedAtRoute( string? routeName, object? routeValues, - object? value) + TValue? value) { Value = value; RouteName = routeName; @@ -47,7 +48,7 @@ internal AcceptedAtRouteHttpResult( /// /// Gets the object result. /// - public object? Value { get; } + public TValue? Value { get; } /// /// Gets the name of the route to use for generating the URL. @@ -60,7 +61,7 @@ internal AcceptedAtRouteHttpResult( public RouteValueDictionary RouteValues { get; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status202Accepted; @@ -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); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json")); } } diff --git a/src/Http/Http.Results/src/AcceptedHttpResult.cs b/src/Http/Http.Results/src/AcceptedOfT.cs similarity index 71% rename from src/Http/Http.Results/src/AcceptedHttpResult.cs rename to src/Http/Http.Results/src/AcceptedOfT.cs index d0b6026e0c56..ebe84dbfe6f4 100644 --- a/src/Http/Http.Results/src/AcceptedHttpResult.cs +++ b/src/Http/Http.Results/src/AcceptedOfT.cs @@ -1,26 +1,26 @@ // 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; + /// /// An that on execution will write an object to the response /// with status code Accepted (202) and Location header. /// Targets a registered route. /// -public sealed class AcceptedHttpResult : IResult +public sealed class Accepted : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The location at which the status of requested content can be monitored. /// The value to format in the entity body. - internal AcceptedHttpResult(string? location, object? value) + internal Accepted(string? location, TValue? value) { Value = value; Location = location; @@ -28,12 +28,12 @@ internal AcceptedHttpResult(string? location, object? value) } /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The location at which the status of requested content can be monitored. /// The value to format in the entity body. - internal AcceptedHttpResult(Uri locationUri, object? value) + internal Accepted(Uri locationUri, TValue? value) { Value = value; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); @@ -56,10 +56,10 @@ internal AcceptedHttpResult(Uri locationUri, object? value) /// /// Gets the object result. /// - public object? Value { get; } + public TValue? Value { get; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status202Accepted; @@ -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(); 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); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json")); } } diff --git a/src/Http/Http.Results/src/BadRequest.cs b/src/Http/Http.Results/src/BadRequest.cs new file mode 100644 index 000000000000..9dd3b161f111 --- /dev/null +++ b/src/Http/Http.Results/src/BadRequest.cs @@ -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; + +/// +/// An that on execution will write an object to the response +/// with Bad Request (400) status code. +/// +public sealed class BadRequest : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + internal BadRequest() + { + } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status400BadRequest; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + // Creating the logger with a string to preserve the category after the refactoring. + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.BadRequestObjectResult"); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest)); + } +} diff --git a/src/Http/Http.Results/src/BadRequestObjectHttpResult.cs b/src/Http/Http.Results/src/BadRequestOfT.cs similarity index 57% rename from src/Http/Http.Results/src/BadRequestObjectHttpResult.cs rename to src/Http/Http.Results/src/BadRequestOfT.cs index dc9ad6e4fb2f..097e3c8793c2 100644 --- a/src/Http/Http.Results/src/BadRequestObjectHttpResult.cs +++ b/src/Http/Http.Results/src/BadRequestOfT.cs @@ -1,23 +1,25 @@ // 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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that on execution will write an object to the response /// with Bad Request (400) status code. /// -public sealed class BadRequestObjectHttpResult : IResult +/// The type of error object that will be JSON serialized to the response body. +public sealed class BadRequest : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The error content to format in the entity body. - internal BadRequestObjectHttpResult(object? error) + internal BadRequest(TValue? error) { Value = error; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); @@ -26,10 +28,10 @@ internal BadRequestObjectHttpResult(object? error) /// /// Gets the object result. /// - public object? Value { get; internal init; } + public TValue? Value { get; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status400BadRequest; @@ -40,10 +42,18 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.BadRequestObjectResult"); + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger: logger, - Value, - StatusCode); + Value); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status400BadRequest, "application/json")); } } diff --git a/src/Http/Http.Results/src/ChallengeHttpResult.cs b/src/Http/Http.Results/src/ChallengeHttpResult.cs index cff6c06937dd..1772c6f0d379 100644 --- a/src/Http/Http.Results/src/ChallengeHttpResult.cs +++ b/src/Http/Http.Results/src/ChallengeHttpResult.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that on execution invokes . diff --git a/src/Http/Http.Results/src/Conflict.cs b/src/Http/Http.Results/src/Conflict.cs new file mode 100644 index 000000000000..12a639bba0e5 --- /dev/null +++ b/src/Http/Http.Results/src/Conflict.cs @@ -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; + +/// +/// An that on execution will write an object to the response +/// with Conflict (409) status code. +/// +public sealed class Conflict : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + internal Conflict() + { + } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status409Conflict; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + // Creating the logger with a string to preserve the category after the refactoring. + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.ConflictObjectResult"); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict)); + } +} diff --git a/src/Http/Http.Results/src/ConflictObjectHttpResult.cs b/src/Http/Http.Results/src/ConflictOfT.cs similarity index 58% rename from src/Http/Http.Results/src/ConflictObjectHttpResult.cs rename to src/Http/Http.Results/src/ConflictOfT.cs index 782775395bfc..f5fb98e193d6 100644 --- a/src/Http/Http.Results/src/ConflictObjectHttpResult.cs +++ b/src/Http/Http.Results/src/ConflictOfT.cs @@ -1,23 +1,25 @@ // 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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that on execution will write an object to the response /// with Conflict (409) status code. /// -public sealed class ConflictObjectHttpResult : IResult +/// The type of object that will be JSON serialized to the response body. +public sealed class Conflict : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The error content to format in the entity body. - internal ConflictObjectHttpResult(object? error) + internal Conflict(TValue? error) { Value = error; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); @@ -26,10 +28,10 @@ internal ConflictObjectHttpResult(object? error) /// /// Gets the object result. /// - public object? Value { get; internal init; } + public TValue? Value { get; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status409Conflict; @@ -40,10 +42,18 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.ConflictObjectResult"); + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger: logger, - Value, - StatusCode); + Value); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status409Conflict, "application/json")); } } diff --git a/src/Http/Http.Results/src/ContentHttpResult.cs b/src/Http/Http.Results/src/ContentHttpResult.cs index a50675408e2d..fa765a137ee0 100644 --- a/src/Http/Http.Results/src/ContentHttpResult.cs +++ b/src/Http/Http.Results/src/ContentHttpResult.cs @@ -1,11 +1,11 @@ // 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 Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that when executed /// will produce a response with content. @@ -30,18 +30,18 @@ internal ContentHttpResult(string? content, string? contentType) /// The Content-Type header for the response internal ContentHttpResult(string? content, string? contentType, int? statusCode) { - Content = content; + ResponseContent = content; StatusCode = statusCode; ContentType = contentType; } /// - /// Gets or set the content representing the body of the response. + /// Gets the content representing the body of the response. /// - public string? Content { get; internal init; } + public string? ResponseContent { get; internal init; } /// - /// Gets or sets the Content-Type header for the response. + /// Gets the Content-Type header for the response. /// public string? ContentType { get; internal init; } @@ -61,6 +61,16 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.ContentResult"); - return HttpResultsHelper.WriteResultAsContentAsync(httpContext, logger, Content, StatusCode, ContentType); + if (StatusCode is { } statusCode) + { + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, statusCode); + httpContext.Response.StatusCode = statusCode; + } + + return HttpResultsHelper.WriteResultAsContentAsync( + httpContext, + logger, + ResponseContent, + ContentType); } } diff --git a/src/Http/Http.Results/src/Created.cs b/src/Http/Http.Results/src/Created.cs new file mode 100644 index 000000000000..0f94c05771d8 --- /dev/null +++ b/src/Http/Http.Results/src/Created.cs @@ -0,0 +1,79 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with status code Created (201) and Location header. +/// +public sealed class Created : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the content has been created. + internal Created(string location) + { + Location = location; + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the content has been created. + internal Created(Uri locationUri) + { + if (locationUri == null) + { + throw new ArgumentNullException(nameof(locationUri)); + } + + if (locationUri.IsAbsoluteUri) + { + Location = locationUri.AbsoluteUri; + } + else + { + Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + } + } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status201Created; + + /// + public string? Location { get; } + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + // Creating the logger with a string to preserve the category after the refactoring. + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.CreatedResult"); + + if (!string.IsNullOrEmpty(Location)) + { + httpContext.Response.Headers.Location = Location; + } + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created)); + } +} diff --git a/src/Http/Http.Results/src/CreatedAtRoute.cs b/src/Http/Http.Results/src/CreatedAtRoute.cs new file mode 100644 index 000000000000..7cb9b31f0b53 --- /dev/null +++ b/src/Http/Http.Results/src/CreatedAtRoute.cs @@ -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; + +/// +/// An that on execution will write an object to the response +/// with status code Created (201) and Location header. +/// Targets a registered route. +/// +public sealed class CreatedAtRoute : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The route data to use for generating the URL. + internal CreatedAtRoute(object? routeValues) + : this(routeName: null, routeValues: routeValues) + { + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + internal CreatedAtRoute( + string? routeName, + object? routeValues) + { + RouteName = routeName; + RouteValues = new RouteValueDictionary(routeValues); + } + + /// + /// Gets the name of the route to use for generating the URL. + /// + public string? RouteName { get; } + + /// + /// Gets the route data to use for generating the URL. + /// + public RouteValueDictionary RouteValues { get; } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status201Created; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + var url = linkGenerator.GetUriByRouteValues( + 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(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.CreatedAtRouteResult"); + + httpContext.Response.Headers.Location = url; + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created)); + } +} diff --git a/src/Http/Http.Results/src/CreatedAtRouteHttpResult.cs b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs similarity index 73% rename from src/Http/Http.Results/src/CreatedAtRouteHttpResult.cs rename to src/Http/Http.Results/src/CreatedAtRouteOfT.cs index f59593be8505..c4260b27f0a9 100644 --- a/src/Http/Http.Results/src/CreatedAtRouteHttpResult.cs +++ b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs @@ -1,41 +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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that on execution will write an object to the response /// with status code Created (201) and Location header. /// Targets a registered route. /// -public sealed class CreatedAtRouteHttpResult : IResult +/// The type of object that will be JSON serialized to the response body. +public sealed class CreatedAtRoute : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The route data to use for generating the URL. /// The value to format in the entity body. - internal CreatedAtRouteHttpResult(object? routeValues, object? value) + internal CreatedAtRoute(object? routeValues, TValue? value) : this(routeName: null, routeValues: routeValues, value: value) { } /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// The value to format in the entity body. - internal CreatedAtRouteHttpResult( + internal CreatedAtRoute( string? routeName, object? routeValues, - object? value) + TValue? value) { Value = value; RouteName = routeName; @@ -46,7 +48,7 @@ internal CreatedAtRouteHttpResult( /// /// Gets the object result. /// - public object? Value { get; } + public TValue? Value { get; } /// /// Gets the name of the route to use for generating the URL. @@ -56,10 +58,10 @@ internal CreatedAtRouteHttpResult( /// /// Gets the route data to use for generating the URL. /// - public RouteValueDictionary? RouteValues { get; } + public RouteValueDictionary RouteValues { get; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status201Created; @@ -83,10 +85,19 @@ public Task ExecuteAsync(HttpContext httpContext) var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.CreatedAtRouteResult"); httpContext.Response.Headers.Location = url; + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger, - Value, - StatusCode); + Value); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status201Created, "application/json")); } } diff --git a/src/Http/Http.Results/src/CreatedHttpResult.cs b/src/Http/Http.Results/src/CreatedOfT.cs similarity index 68% rename from src/Http/Http.Results/src/CreatedHttpResult.cs rename to src/Http/Http.Results/src/CreatedOfT.cs index 4106cacd7fec..5523c5f87a7c 100644 --- a/src/Http/Http.Results/src/CreatedHttpResult.cs +++ b/src/Http/Http.Results/src/CreatedOfT.cs @@ -1,24 +1,26 @@ // 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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that on execution will write an object to the response /// with status code Created (201) and Location header. /// -public sealed class CreatedHttpResult : IResult +/// The type of object that will be JSON serialized to the response body. +public sealed class Created : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The location at which the content has been created. /// The value to format in the entity body. - internal CreatedHttpResult(string location, object? value) + internal Created(string location, TValue? value) { Value = value; Location = location; @@ -26,12 +28,12 @@ internal CreatedHttpResult(string location, object? value) } /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// /// The location at which the content has been created. /// The value to format in the entity body. - internal CreatedHttpResult(Uri locationUri, object? value) + internal Created(Uri locationUri, TValue? value) { Value = value; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); @@ -54,10 +56,10 @@ internal CreatedHttpResult(Uri locationUri, object? value) /// /// Gets the object result. /// - public object? Value { get; } + public TValue? Value { get; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status201Created; @@ -75,10 +77,19 @@ public Task ExecuteAsync(HttpContext httpContext) // Creating the logger with a string to preserve the category after the refactoring. var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.CreatedResult"); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger, - Value, - StatusCode); + Value); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status201Created, "application/json")); } } diff --git a/src/Http/Http.Results/src/EmptyHttpResult.cs b/src/Http/Http.Results/src/EmptyHttpResult.cs index 9e9b25c166dd..ccdc3bd05984 100644 --- a/src/Http/Http.Results/src/EmptyHttpResult.cs +++ b/src/Http/Http.Results/src/EmptyHttpResult.cs @@ -1,7 +1,7 @@ // 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; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// Represents an that when executed will diff --git a/src/Http/Http.Results/src/FileContentHttpResult.cs b/src/Http/Http.Results/src/FileContentHttpResult.cs index 56cd17d13944..960836b99479 100644 --- a/src/Http/Http.Results/src/FileContentHttpResult.cs +++ b/src/Http/Http.Results/src/FileContentHttpResult.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// Represents an that when executed will diff --git a/src/Http/Http.Results/src/FileStreamHttpResult.cs b/src/Http/Http.Results/src/FileStreamHttpResult.cs index 4a2273c535f0..eff283d20d2d 100644 --- a/src/Http/Http.Results/src/FileStreamHttpResult.cs +++ b/src/Http/Http.Results/src/FileStreamHttpResult.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// Represents an that when executed will diff --git a/src/Http/Http.Results/src/ForbidHttpResult.cs b/src/Http/Http.Results/src/ForbidHttpResult.cs index f9d8d9c27897..5afdb53b85d2 100644 --- a/src/Http/Http.Results/src/ForbidHttpResult.cs +++ b/src/Http/Http.Results/src/ForbidHttpResult.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that on execution invokes . diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index f447e1d9539e..b899467b1d80 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -10,34 +10,28 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Http; + internal static partial class HttpResultsHelper { private const string DefaultContentType = "text/plain; charset=utf-8"; private static readonly Encoding DefaultEncoding = Encoding.UTF8; - public static Task WriteResultAsJsonAsync( + public static Task WriteResultAsJsonAsync( HttpContext httpContext, ILogger logger, - object? value, - int? statusCode, + T? value, string? contentType = null, JsonSerializerOptions? jsonSerializerOptions = null) { - Log.WritingResultAsJson(logger, value, statusCode); - - if (statusCode is { } code) - { - httpContext.Response.StatusCode = code; - } - if (value is null) { return Task.CompletedTask; } + Log.WritingResultAsJson(logger, typeof(T).Name); + return httpContext.Response.WriteAsJsonAsync( value, - value.GetType(), options: jsonSerializerOptions, contentType: contentType); } @@ -46,7 +40,6 @@ public static Task WriteResultAsContentAsync( HttpContext httpContext, ILogger logger, string? content, - int? statusCode, string? contentType = null) { var response = httpContext.Response; @@ -60,11 +53,6 @@ public static Task WriteResultAsContentAsync( response.ContentType = resolvedContentType; - if (statusCode is { } code) - { - response.StatusCode = code; - } - Log.WritingResultAsContent(logger, resolvedContentType); if (content != null) @@ -163,29 +151,6 @@ public static void ApplyProblemDetailsDefaults(ProblemDetails problemDetails, in internal static partial class Log { - public static void WritingResultAsJson(ILogger logger, object? value, int? statusCode) - { - if (logger.IsEnabled(LogLevel.Information)) - { - if (value is null) - { - WritingResultAsJsonWithoutValue(logger, statusCode ?? StatusCodes.Status200OK); - } - else - { - var valueType = value.GetType().FullName!; - WritingResultAsJson(logger, type: valueType, statusCode ?? StatusCodes.Status200OK); - } - } - } - public static void WritingResultAsFile(ILogger logger, string fileDownloadName) - { - if (logger.IsEnabled(LogLevel.Information)) - { - WritingResultAsFileWithNoFileName(logger, fileDownloadName: fileDownloadName); - } - } - [LoggerMessage(1, LogLevel.Information, "Setting HTTP status code {StatusCode}.", EventName = "WritingResultAsStatusCode")] @@ -196,20 +161,13 @@ public static void WritingResultAsFile(ILogger logger, string fileDownloadName) EventName = "WritingResultAsContent")] public static partial void WritingResultAsContent(ILogger logger, string contentType); - [LoggerMessage(3, LogLevel.Information, "Writing value of type '{Type}' as Json with status code '{StatusCode}'.", - EventName = "WritingResultAsJson", - SkipEnabledCheck = true)] - private static partial void WritingResultAsJson(ILogger logger, string type, int statusCode); - - [LoggerMessage(4, LogLevel.Information, "Setting the status code '{StatusCode}' without value.", - EventName = "WritingResultAsJsonWithoutValue", - SkipEnabledCheck = true)] - private static partial void WritingResultAsJsonWithoutValue(ILogger logger, int statusCode); + [LoggerMessage(3, LogLevel.Information, "Writing value of type '{Type}' as Json.", + EventName = "WritingResultAsJson")] + public static partial void WritingResultAsJson(ILogger logger, string type); [LoggerMessage(5, LogLevel.Information, "Sending file with download name '{FileDownloadName}'.", - EventName = "WritingResultAsFileWithNoFileName", - SkipEnabledCheck = true)] - private static partial void WritingResultAsFileWithNoFileName(ILogger logger, string fileDownloadName); + EventName = "WritingResultAsFileWithNoFileName")] + public static partial void WritingResultAsFile(ILogger logger, string fileDownloadName); } } diff --git a/src/Http/Http.Results/src/JsonHttpResult.cs b/src/Http/Http.Results/src/JsonHttpResultOfT.cs similarity index 72% rename from src/Http/Http.Results/src/JsonHttpResult.cs rename to src/Http/Http.Results/src/JsonHttpResultOfT.cs index 1e7800a32bdb..dec845ac206a 100644 --- a/src/Http/Http.Results/src/JsonHttpResult.cs +++ b/src/Http/Http.Results/src/JsonHttpResultOfT.cs @@ -2,63 +2,70 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An action result which formats the given object as JSON. /// -public sealed class JsonHttpResult : IResult +public sealed partial class JsonHttpResult : IResult { /// - /// Initializes a new instance of the class with the values. + /// Initializes a new instance of the class with the values. /// /// The value to format in the entity body. /// The serializer settings. - internal JsonHttpResult(object? value, JsonSerializerOptions? jsonSerializerOptions) + internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOptions) : this(value, statusCode: null, contentType: null, jsonSerializerOptions: jsonSerializerOptions) { } /// - /// Initializes a new instance of the class with the values. + /// Initializes a new instance of the class with the values. /// /// The value to format in the entity body. /// The HTTP status code of the response. /// The serializer settings. - internal JsonHttpResult(object? value, int? statusCode, JsonSerializerOptions? jsonSerializerOptions) + internal JsonHttpResult(TValue? value, int? statusCode, JsonSerializerOptions? jsonSerializerOptions) : this(value, statusCode: statusCode, contentType: null, jsonSerializerOptions: jsonSerializerOptions) { } /// - /// Initializes a new instance of the class with the values. + /// Initializes a new instance of the class with the values. /// /// The value to format in the entity body. /// The value for the Content-Type header /// The serializer settings. - internal JsonHttpResult(object? value, string? contentType, JsonSerializerOptions? jsonSerializerOptions) + internal JsonHttpResult(TValue? value, string? contentType, JsonSerializerOptions? jsonSerializerOptions) : this(value, statusCode: null, contentType: contentType, jsonSerializerOptions: jsonSerializerOptions) { } /// - /// Initializes a new instance of the class with the values. + /// Initializes a new instance of the class with the values. /// /// The value to format in the entity body. /// The HTTP status code of the response. /// The serializer settings. /// The value for the Content-Type header - internal JsonHttpResult(object? value, int? statusCode, string? contentType, JsonSerializerOptions? jsonSerializerOptions) + internal JsonHttpResult(TValue? value, int? statusCode, string? contentType, JsonSerializerOptions? jsonSerializerOptions) { Value = value; - StatusCode = statusCode; JsonSerializerOptions = jsonSerializerOptions; ContentType = contentType; - HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + + if (value is ProblemDetails problemDetails) + { + HttpResultsHelper.ApplyProblemDetailsDefaults(problemDetails, statusCode); + statusCode ??= problemDetails.Status; + } + + StatusCode = statusCode; } /// @@ -69,7 +76,7 @@ internal JsonHttpResult(object? value, int? statusCode, string? contentType, Jso /// /// Gets the object result. /// - public object? Value { get; } + public TValue? Value { get; } /// /// Gets the value for the Content-Type header. @@ -88,11 +95,16 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.JsonResult"); + if (StatusCode is { } statusCode) + { + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, statusCode); + httpContext.Response.StatusCode = statusCode; + } + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger, Value, - StatusCode, ContentType, JsonSerializerOptions); } diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index 0d9845104cc4..de0934e22002 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -17,6 +17,8 @@ + + @@ -33,6 +35,14 @@ + + + True + True + ResultsCache.StatusCodes.tt + + + TextTemplatingFileGenerator diff --git a/src/Http/Http.Results/src/NoContentHttpResult.cs b/src/Http/Http.Results/src/NoContent.cs similarity index 65% rename from src/Http/Http.Results/src/NoContentHttpResult.cs rename to src/Http/Http.Results/src/NoContent.cs index 0339cc3f1f38..68920021f36c 100644 --- a/src/Http/Http.Results/src/NoContentHttpResult.cs +++ b/src/Http/Http.Results/src/NoContent.cs @@ -1,26 +1,27 @@ // 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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// Represents an that when executed will /// produce an HTTP response with the No Content (204) status code. /// -public class NoContentHttpResult : IResult +public class NoContent : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - internal NoContentHttpResult() + internal NoContent() { } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status204NoContent; @@ -37,4 +38,10 @@ public Task ExecuteAsync(HttpContext httpContext) return Task.CompletedTask; } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status204NoContent)); + } } diff --git a/src/Http/Http.Results/src/NotFound.cs b/src/Http/Http.Results/src/NotFound.cs new file mode 100644 index 000000000000..9b0dbe7848d1 --- /dev/null +++ b/src/Http/Http.Results/src/NotFound.cs @@ -0,0 +1,46 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with Not Found (404) status code. +/// +public sealed class NotFound : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values. + /// + internal NotFound() + { + } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status404NotFound; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + // Creating the logger with a string to preserve the category after the refactoring. + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.NotFoundObjectResult"); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound)); + } +} diff --git a/src/Http/Http.Results/src/NotFoundObjectHttpResult.cs b/src/Http/Http.Results/src/NotFoundOfT.cs similarity index 57% rename from src/Http/Http.Results/src/NotFoundObjectHttpResult.cs rename to src/Http/Http.Results/src/NotFoundOfT.cs index 45220b7d0483..8e1bacf533ee 100644 --- a/src/Http/Http.Results/src/NotFoundObjectHttpResult.cs +++ b/src/Http/Http.Results/src/NotFoundOfT.cs @@ -1,22 +1,24 @@ // 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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that on execution will write an object to the response /// with Not Found (404) status code. /// -public sealed class NotFoundObjectHttpResult : IResult +/// The type of object that will be JSON serialized to the response body. +public sealed class NotFound : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values. + /// Initializes a new instance of the class with the values. /// /// The value to format in the entity body. - internal NotFoundObjectHttpResult(object? value) + internal NotFound(TValue? value) { Value = value; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); @@ -25,10 +27,10 @@ internal NotFoundObjectHttpResult(object? value) /// /// Gets the object result. /// - public object? Value { get; internal init; } + public TValue? Value { get; internal init; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status404NotFound; @@ -39,10 +41,18 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.NotFoundObjectResult"); + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger: logger, - Value, - StatusCode); + Value); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status404NotFound, "application/json")); } } diff --git a/src/Http/Http.Results/src/ObjectHttpResult.cs b/src/Http/Http.Results/src/ObjectHttpResult.cs deleted file mode 100644 index 252ab122a1da..000000000000 --- a/src/Http/Http.Results/src/ObjectHttpResult.cs +++ /dev/null @@ -1,80 +0,0 @@ -// 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 Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -/// -/// An that on execution will write an object to the response. -/// -internal sealed class ObjectHttpResult : IResult -{ - /// - /// Creates a new instance - /// with the provided . - /// - internal ObjectHttpResult(object? value) - : this(value, null) - { - } - - /// - /// Creates a new instance with the provided - /// , . - /// - internal ObjectHttpResult(object? value, int? statusCode) - : this(value, statusCode, contentType: null) - { - } - - /// - /// Creates a new instance with the provided - /// , and . - /// - internal ObjectHttpResult(object? value, int? statusCode, string? contentType) - { - Value = value; - - if (value is ProblemDetails problemDetails) - { - HttpResultsHelper.ApplyProblemDetailsDefaults(problemDetails, statusCode); - statusCode ??= problemDetails.Status; - } - - StatusCode = statusCode; - ContentType = contentType; - } - - /// - /// Gets the object result. - /// - public object? Value { get; internal init; } - - /// - /// Gets or sets the value for the Content-Type header. - /// - public string? ContentType { get; internal init; } - - /// - /// Gets the HTTP status code. - /// - public int? StatusCode { get; internal init; } - - /// - public Task ExecuteAsync(HttpContext httpContext) - { - // Creating the logger with a string to preserve the category after the refactoring. - var loggerFactory = httpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.ObjectResult"); - - return HttpResultsHelper.WriteResultAsJsonAsync( - httpContext, - logger: logger, - Value, - StatusCode, - ContentType); - } -} diff --git a/src/Http/Http.Results/src/Ok.cs b/src/Http/Http.Results/src/Ok.cs new file mode 100644 index 000000000000..8d2197d0e458 --- /dev/null +++ b/src/Http/Http.Results/src/Ok.cs @@ -0,0 +1,46 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with Ok (200) status code. +/// +public sealed class Ok : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values. + /// + internal Ok() + { + } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status200OK; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + // Creating the logger with a string to preserve the category after the refactoring. + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.OkObjectResult"); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK)); + } +} diff --git a/src/Http/Http.Results/src/OkObjectHttpResult.cs b/src/Http/Http.Results/src/OkOfT.cs similarity index 58% rename from src/Http/Http.Results/src/OkObjectHttpResult.cs rename to src/Http/Http.Results/src/OkOfT.cs index 2dedc139d7ab..ccbef8a73b79 100644 --- a/src/Http/Http.Results/src/OkObjectHttpResult.cs +++ b/src/Http/Http.Results/src/OkOfT.cs @@ -1,23 +1,24 @@ // 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; + /// /// An that on execution will write an object to the response /// with Ok (200) status code. /// -public sealed class OkObjectHttpResult : IResult +/// The type of object that will be JSON serialized to the response body. +public sealed class Ok : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values. + /// Initializes a new instance of the class with the values. /// /// The value to format in the entity body. - internal OkObjectHttpResult(object? value) + internal Ok(TValue? value) { Value = value; HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); @@ -26,10 +27,10 @@ internal OkObjectHttpResult(object? value) /// /// Gets the object result. /// - public object? Value { get; internal init; } + public TValue? Value { get; } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status200OK; @@ -40,10 +41,18 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.OkObjectResult"); + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger: logger, - Value, - StatusCode); + Value); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status200OK, "application/json")); } } diff --git a/src/Http/Http.Results/src/PhysicalFileHttpResult.cs b/src/Http/Http.Results/src/PhysicalFileHttpResult.cs index 97f8e01dca38..4e69b71a88f6 100644 --- a/src/Http/Http.Results/src/PhysicalFileHttpResult.cs +++ b/src/Http/Http.Results/src/PhysicalFileHttpResult.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// A on execution will write a file from disk to the response diff --git a/src/Http/Http.Results/src/ProblemHttpResult.cs b/src/Http/Http.Results/src/ProblemHttpResult.cs index 0a5c52a0fcca..a0861eaf567b 100644 --- a/src/Http/Http.Results/src/ProblemHttpResult.cs +++ b/src/Http/Http.Results/src/ProblemHttpResult.cs @@ -1,12 +1,12 @@ // 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 Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that on execution will write Problem Details /// HTTP API responses based on https://tools.ietf.org/html/rfc7807 @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Http; public sealed class ProblemHttpResult : IResult { /// - /// Creates a new instance with + /// Creates a new instance with /// the provided . /// /// The instance to format in the entity body. @@ -30,7 +30,7 @@ internal ProblemHttpResult(ProblemDetails problemDetails) public ProblemDetails ProblemDetails { get; } /// - /// Gets or sets the value for the Content-Type header. + /// Gets the value for the Content-Type header: application/problem+json /// public string ContentType => "application/problem+json"; @@ -45,11 +45,16 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger(typeof(ProblemHttpResult)); + if (StatusCode is { } code) + { + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, code); + httpContext.Response.StatusCode = code; + } + return HttpResultsHelper.WriteResultAsJsonAsync( httpContext, logger, value: ProblemDetails, - StatusCode, ContentType); } } diff --git a/src/Http/Http.Results/src/PublicAPI.Unshipped.txt b/src/Http/Http.Results/src/PublicAPI.Unshipped.txt index 162a8ca3601f..e87a32cfd7d1 100644 --- a/src/Http/Http.Results/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Results/src/PublicAPI.Unshipped.txt @@ -1,184 +1,272 @@ #nullable enable -Microsoft.AspNetCore.Http.AcceptedAtRouteHttpResult -Microsoft.AspNetCore.Http.AcceptedAtRouteHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.AcceptedAtRouteHttpResult.RouteName.get -> string? -Microsoft.AspNetCore.Http.AcceptedAtRouteHttpResult.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary! -Microsoft.AspNetCore.Http.AcceptedAtRouteHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.AcceptedAtRouteHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.AcceptedHttpResult -Microsoft.AspNetCore.Http.AcceptedHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.AcceptedHttpResult.Location.get -> string? -Microsoft.AspNetCore.Http.AcceptedHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.AcceptedHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.BadRequestObjectHttpResult -Microsoft.AspNetCore.Http.BadRequestObjectHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.BadRequestObjectHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.BadRequestObjectHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.ChallengeHttpResult -Microsoft.AspNetCore.Http.ChallengeHttpResult.AuthenticationSchemes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.Http.ChallengeHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.ChallengeHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? -Microsoft.AspNetCore.Http.ConflictObjectHttpResult -Microsoft.AspNetCore.Http.ConflictObjectHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.ConflictObjectHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.ConflictObjectHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.ContentHttpResult -Microsoft.AspNetCore.Http.ContentHttpResult.Content.get -> string? -Microsoft.AspNetCore.Http.ContentHttpResult.ContentType.get -> string? -Microsoft.AspNetCore.Http.ContentHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.ContentHttpResult.StatusCode.get -> int? -Microsoft.AspNetCore.Http.CreatedAtRouteHttpResult -Microsoft.AspNetCore.Http.CreatedAtRouteHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.CreatedAtRouteHttpResult.RouteName.get -> string? -Microsoft.AspNetCore.Http.CreatedAtRouteHttpResult.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? -Microsoft.AspNetCore.Http.CreatedAtRouteHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.CreatedAtRouteHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.CreatedHttpResult -Microsoft.AspNetCore.Http.CreatedHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.CreatedHttpResult.Location.get -> string? -Microsoft.AspNetCore.Http.CreatedHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.CreatedHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.EmptyHttpResult -Microsoft.AspNetCore.Http.EmptyHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.FileContentHttpResult -Microsoft.AspNetCore.Http.FileContentHttpResult.ContentType.get -> string! -Microsoft.AspNetCore.Http.FileContentHttpResult.EnableRangeProcessing.get -> bool -Microsoft.AspNetCore.Http.FileContentHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? -Microsoft.AspNetCore.Http.FileContentHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.FileContentHttpResult.FileContents.get -> System.ReadOnlyMemory -Microsoft.AspNetCore.Http.FileContentHttpResult.FileDownloadName.get -> string? -Microsoft.AspNetCore.Http.FileContentHttpResult.FileLength.get -> long? -Microsoft.AspNetCore.Http.FileContentHttpResult.LastModified.get -> System.DateTimeOffset? -Microsoft.AspNetCore.Http.FileStreamHttpResult -Microsoft.AspNetCore.Http.FileStreamHttpResult.ContentType.get -> string! -Microsoft.AspNetCore.Http.FileStreamHttpResult.EnableRangeProcessing.get -> bool -Microsoft.AspNetCore.Http.FileStreamHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? -Microsoft.AspNetCore.Http.FileStreamHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.FileStreamHttpResult.FileDownloadName.get -> string? -Microsoft.AspNetCore.Http.FileStreamHttpResult.FileLength.get -> long? -Microsoft.AspNetCore.Http.FileStreamHttpResult.FileStream.get -> System.IO.Stream! -Microsoft.AspNetCore.Http.FileStreamHttpResult.LastModified.get -> System.DateTimeOffset? -Microsoft.AspNetCore.Http.ForbidHttpResult -Microsoft.AspNetCore.Http.ForbidHttpResult.AuthenticationSchemes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.Http.ForbidHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.ForbidHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? -Microsoft.AspNetCore.Http.JsonHttpResult -Microsoft.AspNetCore.Http.JsonHttpResult.ContentType.get -> string? -Microsoft.AspNetCore.Http.JsonHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.JsonHttpResult.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions? -Microsoft.AspNetCore.Http.JsonHttpResult.StatusCode.get -> int? -Microsoft.AspNetCore.Http.JsonHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.NoContentHttpResult -Microsoft.AspNetCore.Http.NoContentHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.NoContentHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.NotFoundObjectHttpResult -Microsoft.AspNetCore.Http.NotFoundObjectHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.NotFoundObjectHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.NotFoundObjectHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.OkObjectHttpResult -Microsoft.AspNetCore.Http.OkObjectHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.OkObjectHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.OkObjectHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.PhysicalFileHttpResult -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.ContentType.get -> string! -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.EnableRangeProcessing.get -> bool -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.FileDownloadName.get -> string? -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.FileLength.get -> long? -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.FileName.get -> string! -Microsoft.AspNetCore.Http.PhysicalFileHttpResult.LastModified.get -> System.DateTimeOffset? -Microsoft.AspNetCore.Http.ProblemHttpResult -Microsoft.AspNetCore.Http.ProblemHttpResult.ContentType.get -> string! -Microsoft.AspNetCore.Http.ProblemHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.ProblemHttpResult.ProblemDetails.get -> Microsoft.AspNetCore.Mvc.ProblemDetails! -Microsoft.AspNetCore.Http.ProblemHttpResult.StatusCode.get -> int? -Microsoft.AspNetCore.Http.PushStreamHttpResult -Microsoft.AspNetCore.Http.PushStreamHttpResult.ContentType.get -> string! -Microsoft.AspNetCore.Http.PushStreamHttpResult.EnableRangeProcessing.get -> bool -Microsoft.AspNetCore.Http.PushStreamHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? -Microsoft.AspNetCore.Http.PushStreamHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.PushStreamHttpResult.FileDownloadName.get -> string? -Microsoft.AspNetCore.Http.PushStreamHttpResult.FileLength.get -> long? -Microsoft.AspNetCore.Http.PushStreamHttpResult.LastModified.get -> System.DateTimeOffset? -Microsoft.AspNetCore.Http.RedirectHttpResult -Microsoft.AspNetCore.Http.RedirectHttpResult.AcceptLocalUrlOnly.get -> bool -Microsoft.AspNetCore.Http.RedirectHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.RedirectHttpResult.Permanent.get -> bool -Microsoft.AspNetCore.Http.RedirectHttpResult.PreserveMethod.get -> bool -Microsoft.AspNetCore.Http.RedirectHttpResult.Url.get -> string! -Microsoft.AspNetCore.Http.RedirectToRouteHttpResult -Microsoft.AspNetCore.Http.RedirectToRouteHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.RedirectToRouteHttpResult.Fragment.get -> string? -Microsoft.AspNetCore.Http.RedirectToRouteHttpResult.Permanent.get -> bool -Microsoft.AspNetCore.Http.RedirectToRouteHttpResult.PreserveMethod.get -> bool -Microsoft.AspNetCore.Http.RedirectToRouteHttpResult.RouteName.get -> string? -Microsoft.AspNetCore.Http.RedirectToRouteHttpResult.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? -Microsoft.AspNetCore.Http.Results -Microsoft.AspNetCore.Http.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! -Microsoft.AspNetCore.Http.Results -Microsoft.AspNetCore.Http.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! -Microsoft.AspNetCore.Http.Results -Microsoft.AspNetCore.Http.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! -Microsoft.AspNetCore.Http.Results -Microsoft.AspNetCore.Http.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! -Microsoft.AspNetCore.Http.Results -Microsoft.AspNetCore.Http.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult4 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult5 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult6 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult4 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult5 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult4 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.Results! -static Microsoft.AspNetCore.Http.Results.implicit operator Microsoft.AspNetCore.Http.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.Results! -Microsoft.AspNetCore.Http.SignInHttpResult -Microsoft.AspNetCore.Http.SignInHttpResult.AuthenticationScheme.get -> string? -Microsoft.AspNetCore.Http.SignInHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.SignInHttpResult.Principal.get -> System.Security.Claims.ClaimsPrincipal! -Microsoft.AspNetCore.Http.SignInHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? -Microsoft.AspNetCore.Http.SignOutHttpResult -Microsoft.AspNetCore.Http.SignOutHttpResult.AuthenticationSchemes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.Http.SignOutHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.SignOutHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? -Microsoft.AspNetCore.Http.StatusCodeHttpResult -Microsoft.AspNetCore.Http.StatusCodeHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.StatusCodeHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.UnauthorizedHttpResult -Microsoft.AspNetCore.Http.UnauthorizedHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.UnauthorizedHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.UnprocessableEntityObjectHttpResult -Microsoft.AspNetCore.Http.UnprocessableEntityObjectHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.UnprocessableEntityObjectHttpResult.StatusCode.get -> int -Microsoft.AspNetCore.Http.UnprocessableEntityObjectHttpResult.Value.get -> object? -Microsoft.AspNetCore.Http.VirtualFileHttpResult -Microsoft.AspNetCore.Http.VirtualFileHttpResult.ContentType.get -> string! -Microsoft.AspNetCore.Http.VirtualFileHttpResult.EnableRangeProcessing.get -> bool -Microsoft.AspNetCore.Http.VirtualFileHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? -Microsoft.AspNetCore.Http.VirtualFileHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.VirtualFileHttpResult.FileDownloadName.get -> string? -Microsoft.AspNetCore.Http.VirtualFileHttpResult.FileLength.get -> long? -Microsoft.AspNetCore.Http.VirtualFileHttpResult.FileName.get -> string! -Microsoft.AspNetCore.Http.VirtualFileHttpResult.LastModified.get -> System.DateTimeOffset? -static Microsoft.AspNetCore.Http.EmptyHttpResult.Instance.get -> Microsoft.AspNetCore.Http.EmptyHttpResult! +Microsoft.AspNetCore.Http.HttpResults.Accepted +Microsoft.AspNetCore.Http.HttpResults.Accepted.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Accepted.Location.get -> string? +Microsoft.AspNetCore.Http.HttpResults.Accepted.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Accepted +Microsoft.AspNetCore.Http.HttpResults.Accepted.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Accepted.Location.get -> string? +Microsoft.AspNetCore.Http.HttpResults.Accepted.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Accepted.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.RouteName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary! +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.RouteName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary! +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.BadRequest +Microsoft.AspNetCore.Http.HttpResults.BadRequest.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.BadRequest.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.BadRequest +Microsoft.AspNetCore.Http.HttpResults.BadRequest.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.BadRequest.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.BadRequest.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.ChallengeHttpResult +Microsoft.AspNetCore.Http.HttpResults.ChallengeHttpResult.AuthenticationSchemes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.HttpResults.ChallengeHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.ChallengeHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? +Microsoft.AspNetCore.Http.HttpResults.Conflict +Microsoft.AspNetCore.Http.HttpResults.Conflict.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Conflict.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Conflict +Microsoft.AspNetCore.Http.HttpResults.Conflict.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Conflict.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Conflict.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult +Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult.ContentType.get -> string? +Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult.ResponseContent.get -> string? +Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult.StatusCode.get -> int? +Microsoft.AspNetCore.Http.HttpResults.Created +Microsoft.AspNetCore.Http.HttpResults.Created.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Created.Location.get -> string? +Microsoft.AspNetCore.Http.HttpResults.Created.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Created +Microsoft.AspNetCore.Http.HttpResults.Created.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Created.Location.get -> string? +Microsoft.AspNetCore.Http.HttpResults.Created.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Created.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.RouteName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary! +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.RouteName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary! +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult +Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.ContentType.get -> string! +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.EnableRangeProcessing.get -> bool +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.FileContents.get -> System.ReadOnlyMemory +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.FileDownloadName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.FileLength.get -> long? +Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult.LastModified.get -> System.DateTimeOffset? +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.ContentType.get -> string! +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.EnableRangeProcessing.get -> bool +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.FileDownloadName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.FileLength.get -> long? +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.FileStream.get -> System.IO.Stream! +Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult.LastModified.get -> System.DateTimeOffset? +Microsoft.AspNetCore.Http.HttpResults.ForbidHttpResult +Microsoft.AspNetCore.Http.HttpResults.ForbidHttpResult.AuthenticationSchemes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.HttpResults.ForbidHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.ForbidHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? +Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult +Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult.ContentType.get -> string? +Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions? +Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult.StatusCode.get -> int? +Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.NoContent +Microsoft.AspNetCore.Http.HttpResults.NoContent.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.NoContent.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.NotFound +Microsoft.AspNetCore.Http.HttpResults.NotFound.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.NotFound.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.NotFound +Microsoft.AspNetCore.Http.HttpResults.NotFound.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.NotFound.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.NotFound.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.Ok +Microsoft.AspNetCore.Http.HttpResults.Ok.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Ok.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Ok +Microsoft.AspNetCore.Http.HttpResults.Ok.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Ok.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.Ok.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.ContentType.get -> string! +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.EnableRangeProcessing.get -> bool +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.FileDownloadName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.FileLength.get -> long? +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.FileName.get -> string! +Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult.LastModified.get -> System.DateTimeOffset? +Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult +Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult.ContentType.get -> string! +Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult.ProblemDetails.get -> Microsoft.AspNetCore.Mvc.ProblemDetails! +Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult.StatusCode.get -> int? +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult.ContentType.get -> string! +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult.EnableRangeProcessing.get -> bool +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult.FileDownloadName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult.FileLength.get -> long? +Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult.LastModified.get -> System.DateTimeOffset? +Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult +Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult.AcceptLocalUrlOnly.get -> bool +Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult.Permanent.get -> bool +Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult.PreserveMethod.get -> bool +Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult.Url.get -> string! +Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult +Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult.Fragment.get -> string? +Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult.Permanent.get -> bool +Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult.PreserveMethod.get -> bool +Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult.RouteName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? +Microsoft.AspNetCore.Http.HttpResults.Results +Microsoft.AspNetCore.Http.HttpResults.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! +Microsoft.AspNetCore.Http.HttpResults.Results +Microsoft.AspNetCore.Http.HttpResults.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! +Microsoft.AspNetCore.Http.HttpResults.Results +Microsoft.AspNetCore.Http.HttpResults.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! +Microsoft.AspNetCore.Http.HttpResults.Results +Microsoft.AspNetCore.Http.HttpResults.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! +Microsoft.AspNetCore.Http.HttpResults.Results +Microsoft.AspNetCore.Http.HttpResults.Results.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.Results.Result.get -> Microsoft.AspNetCore.Http.IResult! +Microsoft.AspNetCore.Http.HttpResults.SignInHttpResult +Microsoft.AspNetCore.Http.HttpResults.SignInHttpResult.AuthenticationScheme.get -> string? +Microsoft.AspNetCore.Http.HttpResults.SignInHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.SignInHttpResult.Principal.get -> System.Security.Claims.ClaimsPrincipal! +Microsoft.AspNetCore.Http.HttpResults.SignInHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? +Microsoft.AspNetCore.Http.HttpResults.SignOutHttpResult +Microsoft.AspNetCore.Http.HttpResults.SignOutHttpResult.AuthenticationSchemes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.HttpResults.SignOutHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.SignOutHttpResult.Properties.get -> Microsoft.AspNetCore.Authentication.AuthenticationProperties? +Microsoft.AspNetCore.Http.HttpResults.StatusCodeHttpResult +Microsoft.AspNetCore.Http.HttpResults.StatusCodeHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.StatusCodeHttpResult.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult +Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity +Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity +Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity.Value.get -> TValue? +Microsoft.AspNetCore.Http.HttpResults.ValidationProblem +Microsoft.AspNetCore.Http.HttpResults.ValidationProblem.ContentType.get -> string! +Microsoft.AspNetCore.Http.HttpResults.ValidationProblem.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.ValidationProblem.ProblemDetails.get -> Microsoft.AspNetCore.Http.HttpValidationProblemDetails! +Microsoft.AspNetCore.Http.HttpResults.ValidationProblem.StatusCode.get -> int +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.ContentType.get -> string! +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.EnableRangeProcessing.get -> bool +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.EntityTag.get -> Microsoft.Net.Http.Headers.EntityTagHeaderValue? +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.FileDownloadName.get -> string? +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.FileLength.get -> long? +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.FileName.get -> string! +Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult.LastModified.get -> System.DateTimeOffset? +Microsoft.AspNetCore.Http.TypedResults +static Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult.Instance.get -> Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult4 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult5 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult6 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult4 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult5 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult4 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult3 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult1 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! +static Microsoft.AspNetCore.Http.HttpResults.Results.implicit operator Microsoft.AspNetCore.Http.HttpResults.Results!(TResult2 result) -> Microsoft.AspNetCore.Http.HttpResults.Results! static Microsoft.AspNetCore.Http.Results.Bytes(System.ReadOnlyMemory contents, string? contentType = null, string? fileDownloadName = null, bool enableRangeProcessing = false, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null) -> Microsoft.AspNetCore.Http.IResult! static Microsoft.AspNetCore.Http.Results.Empty.get -> Microsoft.AspNetCore.Http.IResult! static Microsoft.AspNetCore.Http.Results.Stream(System.Func! streamWriterCallback, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null) -> Microsoft.AspNetCore.Http.IResult! static Microsoft.AspNetCore.Http.Results.Stream(System.IO.Pipelines.PipeReader! pipeReader, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) -> Microsoft.AspNetCore.Http.IResult! +static Microsoft.AspNetCore.Http.TypedResults.Accepted(System.Uri! uri) -> Microsoft.AspNetCore.Http.HttpResults.Accepted! +static Microsoft.AspNetCore.Http.TypedResults.Accepted(string? uri) -> Microsoft.AspNetCore.Http.HttpResults.Accepted! +static Microsoft.AspNetCore.Http.TypedResults.Accepted(System.Uri! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Accepted! +static Microsoft.AspNetCore.Http.TypedResults.Accepted(string? uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Accepted! +static Microsoft.AspNetCore.Http.TypedResults.AcceptedAtRoute(string? routeName = null, object? routeValues = null) -> Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute! +static Microsoft.AspNetCore.Http.TypedResults.AcceptedAtRoute(TValue? value, string? routeName = null, object? routeValues = null) -> Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute! +static Microsoft.AspNetCore.Http.TypedResults.BadRequest() -> Microsoft.AspNetCore.Http.HttpResults.BadRequest! +static Microsoft.AspNetCore.Http.TypedResults.BadRequest(TValue? error) -> Microsoft.AspNetCore.Http.HttpResults.BadRequest! +static Microsoft.AspNetCore.Http.TypedResults.Bytes(System.ReadOnlyMemory contents, string? contentType = null, string? fileDownloadName = null, bool enableRangeProcessing = false, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null) -> Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Bytes(byte[]! contents, string? contentType = null, string? fileDownloadName = null, bool enableRangeProcessing = false, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null) -> Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Challenge(Microsoft.AspNetCore.Authentication.AuthenticationProperties? properties = null, System.Collections.Generic.IList? authenticationSchemes = null) -> Microsoft.AspNetCore.Http.HttpResults.ChallengeHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Conflict() -> Microsoft.AspNetCore.Http.HttpResults.Conflict! +static Microsoft.AspNetCore.Http.TypedResults.Conflict(TValue? error) -> Microsoft.AspNetCore.Http.HttpResults.Conflict! +static Microsoft.AspNetCore.Http.TypedResults.Content(string! content, Microsoft.Net.Http.Headers.MediaTypeHeaderValue! contentType) -> Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Content(string! content, string? contentType = null, System.Text.Encoding? contentEncoding = null) -> Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Created(System.Uri! uri) -> Microsoft.AspNetCore.Http.HttpResults.Created! +static Microsoft.AspNetCore.Http.TypedResults.Created(string! uri) -> Microsoft.AspNetCore.Http.HttpResults.Created! +static Microsoft.AspNetCore.Http.TypedResults.Created(System.Uri! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created! +static Microsoft.AspNetCore.Http.TypedResults.Created(string! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created! +static Microsoft.AspNetCore.Http.TypedResults.CreatedAtRoute(string? routeName = null, object? routeValues = null) -> Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute! +static Microsoft.AspNetCore.Http.TypedResults.CreatedAtRoute(TValue? value, string? routeName = null, object? routeValues = null) -> Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute! +static Microsoft.AspNetCore.Http.TypedResults.Empty.get -> Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.File(System.IO.Stream! fileStream, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) -> Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.File(byte[]! fileContents, string? contentType = null, string? fileDownloadName = null, bool enableRangeProcessing = false, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null) -> Microsoft.AspNetCore.Http.HttpResults.FileContentHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Forbid(Microsoft.AspNetCore.Authentication.AuthenticationProperties? properties = null, System.Collections.Generic.IList? authenticationSchemes = null) -> Microsoft.AspNetCore.Http.HttpResults.ForbidHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Json(TValue? data, System.Text.Json.JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.LocalRedirect(string! localUrl, bool permanent = false, bool preserveMethod = false) -> Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.NoContent() -> Microsoft.AspNetCore.Http.HttpResults.NoContent! +static Microsoft.AspNetCore.Http.TypedResults.NotFound() -> Microsoft.AspNetCore.Http.HttpResults.NotFound! +static Microsoft.AspNetCore.Http.TypedResults.NotFound(TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.NotFound! +static Microsoft.AspNetCore.Http.TypedResults.Ok() -> Microsoft.AspNetCore.Http.HttpResults.Ok! +static Microsoft.AspNetCore.Http.TypedResults.Ok(TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Ok! +static Microsoft.AspNetCore.Http.TypedResults.PhysicalFile(string! path, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) -> Microsoft.AspNetCore.Http.HttpResults.PhysicalFileHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Problem(Microsoft.AspNetCore.Mvc.ProblemDetails! problemDetails) -> Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary? extensions = null) -> Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Redirect(string! url, bool permanent = false, bool preserveMethod = false) -> Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) -> Microsoft.AspNetCore.Http.HttpResults.RedirectToRouteHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.SignIn(System.Security.Claims.ClaimsPrincipal! principal, Microsoft.AspNetCore.Authentication.AuthenticationProperties? properties = null, string? authenticationScheme = null) -> Microsoft.AspNetCore.Http.HttpResults.SignInHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.SignOut(Microsoft.AspNetCore.Authentication.AuthenticationProperties? properties = null, System.Collections.Generic.IList? authenticationSchemes = null) -> Microsoft.AspNetCore.Http.HttpResults.SignOutHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.StatusCode(int statusCode) -> Microsoft.AspNetCore.Http.HttpResults.StatusCodeHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Stream(System.Func! streamWriterCallback, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null) -> Microsoft.AspNetCore.Http.HttpResults.PushStreamHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Stream(System.IO.Pipelines.PipeReader! pipeReader, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) -> Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Stream(System.IO.Stream! stream, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) -> Microsoft.AspNetCore.Http.HttpResults.FileStreamHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Text(string! content, string? contentType = null, System.Text.Encoding? contentEncoding = null) -> Microsoft.AspNetCore.Http.HttpResults.ContentHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.Unauthorized() -> Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult! +static Microsoft.AspNetCore.Http.TypedResults.UnprocessableEntity() -> Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity! +static Microsoft.AspNetCore.Http.TypedResults.UnprocessableEntity(TValue? error) -> Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity! +static Microsoft.AspNetCore.Http.TypedResults.ValidationProblem(System.Collections.Generic.IDictionary! errors, string? detail = null, string? instance = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary? extensions = null) -> Microsoft.AspNetCore.Http.HttpResults.ValidationProblem! +static Microsoft.AspNetCore.Http.TypedResults.VirtualFile(string! path, string? contentType = null, string? fileDownloadName = null, System.DateTimeOffset? lastModified = null, Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) -> Microsoft.AspNetCore.Http.HttpResults.VirtualFileHttpResult! diff --git a/src/Http/Http.Results/src/PushStreamHttpResult.cs b/src/Http/Http.Results/src/PushStreamHttpResult.cs index 71f454af2bf2..e7d3e852788b 100644 --- a/src/Http/Http.Results/src/PushStreamHttpResult.cs +++ b/src/Http/Http.Results/src/PushStreamHttpResult.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// Represents an that when executed will diff --git a/src/Http/Http.Results/src/RedirectHttpResult.cs b/src/Http/Http.Results/src/RedirectHttpResult.cs index 3c1bdc6ce911..e466cef62dcd 100644 --- a/src/Http/Http.Results/src/RedirectHttpResult.cs +++ b/src/Http/Http.Results/src/RedirectHttpResult.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), diff --git a/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs b/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs index 586694792e59..a846f56dd3c4 100644 --- a/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs +++ b/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), diff --git a/src/Http/Http.Results/src/Results.cs b/src/Http/Http.Results/src/Results.cs index 35187a07d435..005ea1385a63 100644 --- a/src/Http/Http.Results/src/Results.cs +++ b/src/Http/Http.Results/src/Results.cs @@ -7,6 +7,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Http; /// /// A factory for . /// -public static class Results +public static partial class Results { /// /// Creates an that on execution invokes . @@ -32,7 +33,7 @@ public static class Results public static IResult Challenge( AuthenticationProperties? properties = null, IList? authenticationSchemes = null) - => new ChallengeHttpResult(authenticationSchemes: authenticationSchemes ?? Array.Empty(), properties); + => TypedResults.Challenge(properties, authenticationSchemes); /// /// Creates a that on execution invokes . @@ -50,7 +51,7 @@ public static IResult Challenge( /// a redirect to show a login page. /// public static IResult Forbid(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) - => new ForbidHttpResult(authenticationSchemes: authenticationSchemes ?? Array.Empty(), properties); + => TypedResults.Forbid(properties, authenticationSchemes); /// /// Creates an that on execution invokes . @@ -63,7 +64,7 @@ public static IResult SignIn( ClaimsPrincipal principal, AuthenticationProperties? properties = null, string? authenticationScheme = null) - => new SignInHttpResult(principal, authenticationScheme, properties); + => TypedResults.SignIn(principal, properties, authenticationScheme); /// /// Creates an that on execution invokes . @@ -72,7 +73,7 @@ public static IResult SignIn( /// The authentication scheme to use for the sign-out operation. /// The created for the response. public static IResult SignOut(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) - => new SignOutHttpResult(authenticationSchemes ?? Array.Empty(), properties); + => TypedResults.SignOut(properties, authenticationSchemes); /// /// Writes the string to the HTTP response. @@ -89,7 +90,7 @@ public static IResult SignOut(AuthenticationProperties? properties = null, IList /// the parameter is chosen as the final encoding. /// public static IResult Content(string content, string? contentType = null, Encoding? contentEncoding = null) - => Text(content, contentType, contentEncoding); + => TypedResults.Content(content, contentType, contentEncoding); /// /// Writes the string to the HTTP response. @@ -106,16 +107,7 @@ public static IResult Content(string content, string? contentType = null, Encodi /// the parameter is chosen as the final encoding. /// public static IResult Text(string content, string? contentType = null, Encoding? contentEncoding = null) - { - MediaTypeHeaderValue? mediaTypeHeaderValue = null; - if (contentType is not null) - { - mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType); - mediaTypeHeaderValue.Encoding = contentEncoding ?? mediaTypeHeaderValue.Encoding; - } - - return new ContentHttpResult(content, mediaTypeHeaderValue?.ToString()); - } + => TypedResults.Text(content, contentType, contentEncoding); /// /// Writes the string to the HTTP response. @@ -124,7 +116,7 @@ public static IResult Text(string content, string? contentType = null, Encoding? /// The content type (MIME type). /// The created object for the response. public static IResult Content(string content, MediaTypeHeaderValue contentType) - => new ContentHttpResult(content, contentType.ToString()); + => TypedResults.Content(content, contentType); /// /// Creates a that serializes the specified object to JSON. @@ -133,15 +125,12 @@ public static IResult Content(string content, MediaTypeHeaderValue contentType) /// The serializer options to use when serializing the value. /// The content-type to set on the response. /// The status code to set on the response. - /// The created that serializes the specified + /// The created that serializes the specified /// as JSON format for the response. /// Callers should cache an instance of serializer settings to avoid /// recreating cached data with each call. public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) - => new JsonHttpResult(data, statusCode, options) - { - ContentType = contentType, - }; + => TypedResults.Json(data, options, contentType, statusCode); /// /// Writes the byte-array content to the response. @@ -153,28 +142,22 @@ public static IResult Json(object? data, JsonSerializerOptions? options = null, /// This API is an alias for . /// /// The file contents. - /// The Content-Type of the file. - /// The suggested file name. - /// Set to true to enable range requests processing. - /// The of when the file was last modified. - /// The associated with the file. + /// The Content-Type of the file. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The of when the file was last modified. + /// The associated with the file. /// The created for the response. #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static IResult File( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - byte[] fileContents, + byte[] fileContents, string? contentType = null, string? fileDownloadName = null, bool enableRangeProcessing = false, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) - => new FileContentHttpResult(fileContents, contentType) - { - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - LastModified = lastModified, - EntityTag = entityTag, - }; + => TypedResults.File(fileContents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag); /// /// Writes the byte-array content to the response. @@ -199,13 +182,7 @@ public static IResult Bytes( bool enableRangeProcessing = false, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) - => new FileContentHttpResult(contents, contentType) - { - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - LastModified = lastModified, - EntityTag = entityTag, - }; + => TypedResults.Bytes(contents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag); /// /// Writes the byte-array content to the response. @@ -230,13 +207,7 @@ public static IResult Bytes( bool enableRangeProcessing = false, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) - => new FileContentHttpResult(contents, contentType) - { - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - LastModified = lastModified, - EntityTag = entityTag, - }; + => TypedResults.Bytes(contents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag); /// /// Writes the specified to the response. @@ -263,21 +234,13 @@ public static IResult Bytes( #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static IResult File( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - Stream fileStream, + Stream fileStream, string? contentType = null, string? fileDownloadName = null, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) - { - return new FileStreamHttpResult(fileStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - }; - } + => TypedResults.File(fileStream, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing); /// /// Writes the specified to the response. @@ -308,15 +271,7 @@ public static IResult Stream( DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) - { - return new FileStreamHttpResult(stream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - }; - } + => TypedResults.Stream(stream, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing); /// /// Writes the contents of specified to the response. @@ -346,15 +301,7 @@ public static IResult Stream( DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) - { - return new FileStreamHttpResult(pipeReader.AsStream(), contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - }; - } + => TypedResults.Stream(pipeReader, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing); /// /// Allows writing directly to the response body. @@ -379,14 +326,7 @@ public static IResult Stream( string? fileDownloadName = null, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) - { - return new PushStreamHttpResult(streamWriterCallback, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - FileDownloadName = fileDownloadName, - }; - } + => TypedResults.Stream(streamWriterCallback, contentType, fileDownloadName, lastModified, entityTag); /// /// Writes the file at the specified to the response. @@ -405,34 +345,15 @@ public static IResult Stream( #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static IResult File( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - string path, + string path, string? contentType = null, string? fileDownloadName = null, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) - { - if (Path.IsPathRooted(path)) - { - return new PhysicalFileHttpResult(path, contentType) - { - FileDownloadName = fileDownloadName, - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = enableRangeProcessing, - }; - } - else - { - return new VirtualFileHttpResult(path, contentType) - { - FileDownloadName = fileDownloadName, - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = enableRangeProcessing, - }; - } - } + => Path.IsPathRooted(path) + ? TypedResults.PhysicalFile(path, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing) + : TypedResults.VirtualFile(path, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing); /// /// Redirects to the specified . @@ -448,7 +369,7 @@ public static IResult File( /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. /// The created for the response. public static IResult Redirect(string url, bool permanent = false, bool preserveMethod = false) - => new RedirectHttpResult(url, permanent, preserveMethod); + => TypedResults.Redirect(url, permanent, preserveMethod); /// /// Redirects to the specified . @@ -464,7 +385,7 @@ public static IResult Redirect(string url, bool permanent = false, bool preserve /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. /// The created for the response. public static IResult LocalRedirect(string localUrl, bool permanent = false, bool preserveMethod = false) - => new RedirectHttpResult(localUrl, acceptLocalUrlOnly: true, permanent, preserveMethod); + => TypedResults.LocalRedirect(localUrl, permanent, preserveMethod); /// /// Redirects to the specified route. @@ -482,20 +403,15 @@ public static IResult LocalRedirect(string localUrl, bool permanent = false, boo /// The fragment to add to the URL. /// The created for the response. public static IResult RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) - => new RedirectToRouteHttpResult( - routeName: routeName, - routeValues: routeValues, - permanent: permanent, - preserveMethod: preserveMethod, - fragment: fragment); + => TypedResults.RedirectToRoute(routeName, routeValues, permanent, preserveMethod, fragment); /// - /// Creates a object by specifying a . + /// Creates an object by specifying a . /// /// The status code to set on the response. - /// The created object for the response. + /// The created object for the response. public static IResult StatusCode(int statusCode) - => ResultsCache.StatusCode(statusCode); + => TypedResults.StatusCode(statusCode); /// /// Produces a response. @@ -503,14 +419,14 @@ public static IResult StatusCode(int statusCode) /// The value to be included in the HTTP response body. /// The created for the response. public static IResult NotFound(object? value = null) - => value is null ? ResultsCache.NotFound : new NotFoundObjectHttpResult(value); + => value is null ? TypedResults.NotFound() : TypedResults.NotFound(value); /// /// Produces a response. /// /// The created for the response. public static IResult Unauthorized() - => ResultsCache.Unauthorized; + => TypedResults.Unauthorized(); /// /// Produces a response. @@ -518,7 +434,7 @@ public static IResult Unauthorized() /// An error object to be included in the HTTP response body. /// The created for the response. public static IResult BadRequest(object? error = null) - => error is null ? ResultsCache.BadRequest : new BadRequestObjectHttpResult(error); + => error is null ? TypedResults.BadRequest() : TypedResults.BadRequest(error); /// /// Produces a response. @@ -526,14 +442,14 @@ public static IResult BadRequest(object? error = null) /// An error object to be included in the HTTP response body. /// The created for the response. public static IResult Conflict(object? error = null) - => error is null ? ResultsCache.Conflict : new ConflictObjectHttpResult(error); + => error is null ? TypedResults.Conflict() : TypedResults.Conflict(error); /// /// Produces a response. /// /// The created for the response. public static IResult NoContent() - => ResultsCache.NoContent; + => TypedResults.NoContent(); /// /// Produces a response. @@ -541,7 +457,7 @@ public static IResult NoContent() /// The value to be included in the HTTP response body. /// The created for the response. public static IResult Ok(object? value = null) - => value is null ? ResultsCache.Ok : new OkObjectHttpResult(value); + => value is null ? TypedResults.Ok() : TypedResults.Ok(value); /// /// Produces a response. @@ -549,7 +465,7 @@ public static IResult Ok(object? value = null) /// An error object to be included in the HTTP response body. /// The created for the response. public static IResult UnprocessableEntity(object? error = null) - => error is null ? ResultsCache.UnprocessableEntity : new UnprocessableEntityObjectHttpResult(error); + => error is null ? TypedResults.UnprocessableEntity() : TypedResults.UnprocessableEntity(error); /// /// Produces a response. @@ -568,26 +484,7 @@ public static IResult Problem( string? title = null, string? type = null, IDictionary? extensions = null) - { - var problemDetails = new ProblemDetails - { - Detail = detail, - Instance = instance, - Status = statusCode, - Title = title, - Type = type, - }; - - if (extensions is not null) - { - foreach (var extension in extensions) - { - problemDetails.Extensions.Add(extension); - } - } - - return new ProblemHttpResult(problemDetails); - } + => TypedResults.Problem(detail, instance, statusCode, title, type, extensions); /// /// Produces a response. @@ -595,9 +492,7 @@ public static IResult Problem( /// The object to produce a response from. /// The created for the response. public static IResult Problem(ProblemDetails problemDetails) - { - return new ProblemHttpResult(problemDetails); - } + => TypedResults.Problem(problemDetails); /// /// Produces a response @@ -620,6 +515,7 @@ public static IResult ValidationProblem( string? type = null, IDictionary? extensions = null) { + // TypedResults.ValidationProblem() does not allow setting the statusCode so we do this manually here var problemDetails = new HttpValidationProblemDetails(errors) { Detail = detail, @@ -638,7 +534,7 @@ public static IResult ValidationProblem( } } - return new ProblemHttpResult(problemDetails); + return TypedResults.Problem(problemDetails); } /// @@ -648,14 +544,7 @@ public static IResult ValidationProblem( /// The value to be included in the HTTP response body. /// The created for the response. public static IResult Created(string uri, object? value) - { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - return new CreatedHttpResult(uri, value); - } + => value is null ? TypedResults.Created(uri) : TypedResults.Created(uri, value); /// /// Produces a response. @@ -664,14 +553,7 @@ public static IResult Created(string uri, object? value) /// The value to be included in the HTTP response body. /// The created for the response. public static IResult Created(Uri uri, object? value) - { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - return new CreatedHttpResult(uri, value); - } + => value is null ? TypedResults.Created(uri) : TypedResults.Created(uri, value); /// /// Produces a response. @@ -681,7 +563,7 @@ public static IResult Created(Uri uri, object? value) /// The value to be included in the HTTP response body. /// The created for the response. public static IResult CreatedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) - => new CreatedAtRouteHttpResult(routeName, routeValues, value); + => value is null ? TypedResults.CreatedAtRoute(routeName, routeValues) : TypedResults.CreatedAtRoute(value, routeName, routeValues); /// /// Produces a response. @@ -690,7 +572,7 @@ public static IResult CreatedAtRoute(string? routeName = null, object? routeValu /// The optional content value to format in the response body. /// The created for the response. public static IResult Accepted(string? uri = null, object? value = null) - => new AcceptedHttpResult(uri, value); + => value is null ? TypedResults.Accepted(uri) : TypedResults.Accepted(uri, value); /// /// Produces a response. @@ -700,12 +582,12 @@ public static IResult Accepted(string? uri = null, object? value = null) /// The optional content value to format in the response body. /// The created for the response. public static IResult AcceptedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) - => new AcceptedAtRouteHttpResult(routeName, routeValues, value); + => value is null ? TypedResults.AcceptedAtRoute(routeName, routeValues) : TypedResults.AcceptedAtRoute(value, routeName, routeValues); /// /// Produces an empty result response, that when executed will do nothing. /// - public static IResult Empty { get; } = EmptyHttpResult.Instance; + public static IResult Empty { get; } = TypedResults.Empty; /// /// Provides a container for external libraries to extend diff --git a/src/Http/Http.Results/src/ResultsCache.StatusCodes.cs b/src/Http/Http.Results/src/ResultsCache.StatusCodes.cs index 4e255cd9449c..0468b7a565e0 100644 --- a/src/Http/Http.Results/src/ResultsCache.StatusCodes.cs +++ b/src/Http/Http.Results/src/ResultsCache.StatusCodes.cs @@ -5,6 +5,7 @@ #nullable enable using System.CodeDom.Compiler; +using Microsoft.AspNetCore.Http.HttpResults; namespace Microsoft.AspNetCore.Http; diff --git a/src/Http/Http.Results/src/ResultsCache.StatusCodes.tt b/src/Http/Http.Results/src/ResultsCache.StatusCodes.tt index 578deef36952..0c4450019f45 100644 --- a/src/Http/Http.Results/src/ResultsCache.StatusCodes.tt +++ b/src/Http/Http.Results/src/ResultsCache.StatusCodes.tt @@ -76,6 +76,7 @@ #nullable enable using System.CodeDom.Compiler; +using Microsoft.AspNetCore.Http.HttpResults; namespace Microsoft.AspNetCore.Http; diff --git a/src/Http/Http.Results/src/ResultsCache.cs b/src/Http/Http.Results/src/ResultsCache.cs index dcdcaf092fb0..aed0796bfc55 100644 --- a/src/Http/Http.Results/src/ResultsCache.cs +++ b/src/Http/Http.Results/src/ResultsCache.cs @@ -1,15 +1,17 @@ // 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.HttpResults; + namespace Microsoft.AspNetCore.Http; internal static partial class ResultsCache { - public static NotFoundObjectHttpResult NotFound { get; } = new(null); + public static NotFound NotFound { get; } = new(); public static UnauthorizedHttpResult Unauthorized { get; } = new(); - public static BadRequestObjectHttpResult BadRequest { get; } = new(null); - public static ConflictObjectHttpResult Conflict { get; } = new(null); - public static NoContentHttpResult NoContent { get; } = new(); - public static OkObjectHttpResult Ok { get; } = new(null); - public static UnprocessableEntityObjectHttpResult UnprocessableEntity { get; } = new(null); + public static BadRequest BadRequest { get; } = new(); + public static Conflict Conflict { get; } = new(); + public static NoContent NoContent { get; } = new(); + public static Ok Ok { get; } = new(); + public static UnprocessableEntity UnprocessableEntity { get; } = new(); } diff --git a/src/Http/Http.Results/src/ResultsOfT.cs b/src/Http/Http.Results/src/ResultsOfT.cs index d868e99b44b3..33559b6c6e98 100644 --- a/src/Http/Http.Results/src/ResultsOfT.cs +++ b/src/Http/Http.Results/src/ResultsOfT.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Http.Metadata; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that could be one of two different types. On execution will diff --git a/src/Http/Http.Results/src/SignInHttpResult.cs b/src/Http/Http.Results/src/SignInHttpResult.cs index a1b0d805e867..8ce3b7276732 100644 --- a/src/Http/Http.Results/src/SignInHttpResult.cs +++ b/src/Http/Http.Results/src/SignInHttpResult.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that on execution invokes . diff --git a/src/Http/Http.Results/src/SignOutHttpResult.cs b/src/Http/Http.Results/src/SignOutHttpResult.cs index 7d187fe52c15..515fb89e4183 100644 --- a/src/Http/Http.Results/src/SignOutHttpResult.cs +++ b/src/Http/Http.Results/src/SignOutHttpResult.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that on execution invokes . diff --git a/src/Http/Http.Results/src/StatusCodeHttpResult.cs b/src/Http/Http.Results/src/StatusCodeHttpResult.cs index 3e24b45a7767..ad951df8beb7 100644 --- a/src/Http/Http.Results/src/StatusCodeHttpResult.cs +++ b/src/Http/Http.Results/src/StatusCodeHttpResult.cs @@ -1,7 +1,7 @@ // 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; +namespace Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/Http/Http.Results/src/TypedResults.cs b/src/Http/Http.Results/src/TypedResults.cs new file mode 100644 index 000000000000..938400a4169b --- /dev/null +++ b/src/Http/Http.Results/src/TypedResults.cs @@ -0,0 +1,860 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipelines; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +/// +/// A typed factory for types in . +/// +public static class TypedResults +{ + /// + /// Creates a that on execution invokes . + /// + /// The behavior of this method depends on the in use. + /// and + /// are among likely status results. + /// + /// + /// used to perform the authentication + /// challenge. + /// The authentication schemes to challenge. + /// The created for the response. + public static ChallengeHttpResult Challenge( + AuthenticationProperties? properties = null, + IList? authenticationSchemes = null) + => new(authenticationSchemes: authenticationSchemes ?? Array.Empty(), properties); + + /// + /// Creates a that on execution invokes . + /// + /// By default, executing this result returns a . Some authentication schemes, such as cookies, + /// will convert to a redirect to show a login page. + /// + /// + /// + /// Some authentication schemes, such as cookies, will convert to + /// a redirect to show a login page. + /// + /// used to perform the authentication + /// challenge. + /// The authentication schemes to challenge. + /// The created for the response. + public static ForbidHttpResult Forbid(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) + => new(authenticationSchemes: authenticationSchemes ?? Array.Empty(), properties); + + /// + /// Creates a that on execution invokes . + /// + /// The containing the user claims. + /// used to perform the sign-in operation. + /// The authentication scheme to use for the sign-in operation. + /// The created for the response. + public static SignInHttpResult SignIn( + ClaimsPrincipal principal, + AuthenticationProperties? properties = null, + string? authenticationScheme = null) + => new(principal, authenticationScheme, properties); + + /// + /// Creates a that on execution invokes . + /// + /// used to perform the sign-out operation. + /// The authentication scheme to use for the sign-out operation. + /// The created for the response. + public static SignOutHttpResult SignOut(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) + => new(authenticationSchemes ?? Array.Empty(), properties); + + /// + /// Writes the string to the HTTP response. + /// + /// This is an alias for . + /// + /// + /// + /// If encoding is provided by both the 'charset' and the parameters, then + /// the parameter is chosen as the final encoding. + /// + /// The content to write to the response. + /// The content type (MIME type). + /// The content encoding. + /// The created object for the response. + public static ContentHttpResult Content(string content, string? contentType = null, Encoding? contentEncoding = null) + => Text(content, contentType, contentEncoding); + + /// + /// Writes the string to the HTTP response. + /// + /// This is an alias for . + /// + /// + /// + /// If encoding is provided by both the 'charset' and the parameters, then + /// the parameter is chosen as the final encoding. + /// + /// The content to write to the response. + /// The content type (MIME type). + /// The content encoding. + /// The created object for the response. + public static ContentHttpResult Text(string content, string? contentType = null, Encoding? contentEncoding = null) + { + MediaTypeHeaderValue? mediaTypeHeaderValue = null; + if (contentType is not null) + { + mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType); + mediaTypeHeaderValue.Encoding = contentEncoding ?? mediaTypeHeaderValue.Encoding; + } + + return new(content, mediaTypeHeaderValue?.ToString()); + } + + /// + /// Writes the string to the HTTP response. + /// + /// The content to write to the response. + /// The content type (MIME type). + /// The created object for the response. + public static ContentHttpResult Content(string content, MediaTypeHeaderValue contentType) + => new(content, contentType.ToString()); + + /// + /// Creates a that serializes the specified object to JSON. + /// + /// Callers should cache an instance of serializer settings to avoid + /// recreating cached data with each call. + /// The type of object that will be JSON serialized to the response body. + /// The object to write as JSON. + /// The serializer options to use when serializing the value. + /// The content-type to set on the response. + /// The status code to set on the response. + /// The created that serializes the specified + /// as JSON format for the response. + public static JsonHttpResult Json(TValue? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) + => new(data, statusCode, options) + { + ContentType = contentType, + }; + + /// + /// Writes the byte-array content to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// The file contents. + /// The Content-Type of the file. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The of when the file was last modified. + /// The associated with the file. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static FileContentHttpResult File( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + byte[] fileContents, + string? contentType = null, + string? fileDownloadName = null, + bool enableRangeProcessing = false, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + => new(fileContents, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + LastModified = lastModified, + EntityTag = entityTag, + }; + + /// + /// Writes the byte-array content to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// The file contents. + /// The Content-Type of the file. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The of when the file was last modified. + /// The associated with the file. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static FileContentHttpResult Bytes( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + byte[] contents, + string? contentType = null, + string? fileDownloadName = null, + bool enableRangeProcessing = false, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + => new(contents, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + LastModified = lastModified, + EntityTag = entityTag, + }; + + /// + /// Writes the byte-array content to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// The file contents. + /// The Content-Type of the file. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The of when the file was last modified. + /// The associated with the file. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static FileContentHttpResult Bytes( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + ReadOnlyMemory contents, + string? contentType = null, + string? fileDownloadName = null, + bool enableRangeProcessing = false, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + => new(contents, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + LastModified = lastModified, + EntityTag = entityTag, + }; + + /// + /// Writes the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// + /// + /// The parameter is disposed after the response is sent. + /// + /// The with the contents of the file. + /// The Content-Type of the file. + /// The the file name to be used in the Content-Disposition header. + /// The of when the file was last modified. + /// Used to configure the Last-Modified response header and perform conditional range requests. + /// The to be configure the ETag response header + /// and perform conditional requests. + /// Set to true to enable range requests processing. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static FileStreamHttpResult File( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + Stream fileStream, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + return new(fileStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + + /// + /// Writes the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// + /// + /// The parameter is disposed after the response is sent. + /// + /// The to write to the response. + /// The Content-Type of the response. Defaults to application/octet-stream. + /// The the file name to be used in the Content-Disposition header. + /// The of when the file was last modified. + /// Used to configure the Last-Modified response header and perform conditional range requests. + /// The to be configure the ETag response header + /// and perform conditional requests. + /// Set to true to enable range requests processing. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static FileStreamHttpResult Stream( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + Stream stream, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + return new(stream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + + /// + /// Writes the contents of the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// + /// The parameter is completed after the response is sent. + /// + /// The to write to the response. + /// The Content-Type of the response. Defaults to application/octet-stream. + /// The the file name to be used in the Content-Disposition header. + /// The of when the file was last modified. + /// Used to configure the Last-Modified response header and perform conditional range requests. + /// The to be configure the ETag response header + /// and perform conditional requests. + /// Set to true to enable range requests processing. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static FileStreamHttpResult Stream( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + PipeReader pipeReader, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + return new(pipeReader.AsStream(), contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + + /// + /// Allows writing directly to the response body. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// The callback that allows users to write directly to the response body. + /// The Content-Type of the response. Defaults to application/octet-stream. + /// The the file name to be used in the Content-Disposition header. + /// The of when the file was last modified. + /// Used to configure the Last-Modified response header and perform conditional range requests. + /// The to be configure the ETag response header + /// and perform conditional requests. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static PushStreamHttpResult Stream( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + Func streamWriterCallback, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + { + return new(streamWriterCallback, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + }; + } + + /// + /// Writes the file at the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// The path to the file. When not rooted, resolves the path relative to . + /// The Content-Type of the file. + /// The suggested file name. + /// The of when the file was last modified. + /// The associated with the file. + /// Set to true to enable range requests processing. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static PhysicalFileHttpResult PhysicalFile( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + string path, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + return new(path, contentType) + { + FileDownloadName = fileDownloadName, + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + }; + } + + /// + /// Writes the file at the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// The path to the file. When not rooted, resolves the path relative to . + /// The Content-Type of the file. + /// The suggested file name. + /// The of when the file was last modified. + /// The associated with the file. + /// Set to true to enable range requests processing. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static VirtualFileHttpResult VirtualFile( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + string path, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + return new(path, contentType) + { + FileDownloadName = fileDownloadName, + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + }; + } + + /// + /// Redirects to the specified . + /// + /// When and are set, sets the status code. + /// When is set, sets the status code. + /// When is set, sets the status code. + /// Otherwise, configures . + /// + /// + /// The URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// The created for the response. + public static RedirectHttpResult Redirect(string url, bool permanent = false, bool preserveMethod = false) + => new(url, permanent, preserveMethod); + + /// + /// Redirects to the specified . + /// + /// When and are set, sets the status code. + /// When is set, sets the status code. + /// When is set, sets the status code. + /// Otherwise, configures . + /// + /// + /// The local URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// The created for the response. + public static RedirectHttpResult LocalRedirect(string localUrl, bool permanent = false, bool preserveMethod = false) + => new(localUrl, acceptLocalUrlOnly: true, permanent, preserveMethod); + + /// + /// Redirects to the specified route. + /// + /// When and are set, sets the status code. + /// When is set, sets the status code. + /// When is set, sets the status code. + /// Otherwise, configures . + /// + /// + /// The name of the route. + /// The parameters for a route. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// The fragment to add to the URL. + /// The created for the response. + public static RedirectToRouteHttpResult RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) + => new( + routeName: routeName, + routeValues: routeValues, + permanent: permanent, + preserveMethod: preserveMethod, + fragment: fragment); + + /// + /// Creates a object by specifying a . + /// + /// The status code to set on the response. + /// The created object for the response. + public static StatusCodeHttpResult StatusCode(int statusCode) + => ResultsCache.StatusCode(statusCode); + + /// + /// Produces a response. + /// + /// The created for the response. + public static NotFound NotFound() => ResultsCache.NotFound; + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static NotFound NotFound(TValue? value) => new(value); + + /// + /// Produces a response. + /// + /// The created for the response. + public static UnauthorizedHttpResult Unauthorized() => ResultsCache.Unauthorized; + + /// + /// Produces a response. + /// + /// The created for the response. + public static BadRequest BadRequest() => ResultsCache.BadRequest; + + /// + /// Produces a response. + /// + /// The type of error object that will be JSON serialized to the response body. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static BadRequest BadRequest(TValue? error) => new(error); + + /// + /// Produces a response. + /// + /// The created for the response. + public static Conflict Conflict() => ResultsCache.Conflict; + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static Conflict Conflict(TValue? error) => new(error); + + /// + /// Produces a response. + /// + /// The created for the response. + public static NoContent NoContent() => ResultsCache.NoContent; + + /// + /// Produces a response. + /// + /// The created for the response. + public static Ok Ok() => ResultsCache.Ok; + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static Ok Ok(TValue? value) => new(value); + + /// + /// Produces a response. + /// + /// The created for the response. + public static UnprocessableEntity UnprocessableEntity() => ResultsCache.UnprocessableEntity; + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static UnprocessableEntity UnprocessableEntity(TValue? error) => new(error); + + /// + /// Produces a response. + /// + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The created for the response. + public static ProblemHttpResult Problem( + string? detail = null, + string? instance = null, + int? statusCode = null, + string? title = null, + string? type = null, + IDictionary? extensions = null) + { + var problemDetails = new ProblemDetails + { + Detail = detail, + Instance = instance, + Status = statusCode, + Title = title, + Type = type, + }; + + if (extensions is not null) + { + foreach (var extension in extensions) + { + problemDetails.Extensions.Add(extension); + } + } + + return new(problemDetails); + } + + /// + /// Produces a response. + /// + /// The object to produce a response from. + /// The created for the response. + public static ProblemHttpResult Problem(ProblemDetails problemDetails) + { + return new(problemDetails); + } + + /// + /// Produces a response with an value. + /// + /// One or more validation errors. + /// The value for . + /// The value for . + /// The value for . Defaults to "One or more validation errors occurred." + /// The value for . + /// The value for . + /// The created for the response. + public static ValidationProblem ValidationProblem( + IDictionary errors, + string? detail = null, + string? instance = null, + string? title = null, + string? type = null, + IDictionary? extensions = null) + { + var problemDetails = new HttpValidationProblemDetails(errors) + { + Detail = detail, + Instance = instance, + Type = type, + }; + + problemDetails.Title = title ?? problemDetails.Title; + + if (extensions is not null) + { + foreach (var extension in extensions) + { + problemDetails.Extensions.Add(extension); + } + } + + return new(problemDetails); + } + + /// + /// Produces a response. + /// + /// The URI at which the content has been created. + /// The created for the response. + public static Created Created(string uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new(uri); + } + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The URI at which the content has been created. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static Created Created(string uri, TValue? value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new(uri, value); + } + + /// + /// Produces a response. + /// + /// The URI at which the content has been created. + /// The created for the response. + public static Created Created(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new(uri); + } + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The URI at which the content has been created. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static Created Created(Uri uri, TValue? value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new(uri, value); + } + + /// + /// Produces a response. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static CreatedAtRoute CreatedAtRoute(string? routeName = null, object? routeValues = null) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + => new(routeName, routeValues); + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to be included in the HTTP response body. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static CreatedAtRoute CreatedAtRoute(TValue? value, string? routeName = null, object? routeValues = null) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + => new(routeName, routeValues, value); + + /// + /// Produces a response. + /// + /// The URI with the location at which the status of requested content can be monitored. + /// The created for the response. + public static Accepted Accepted(string? uri) + => new(uri); + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The URI with the location at which the status of requested content can be monitored. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static Accepted Accepted(string? uri, TValue? value) + => new(uri, value); + + /// + /// Produces a response. + /// + /// The URI with the location at which the status of requested content can be monitored. + /// The created for the response. + public static Accepted Accepted(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new(uri); + } + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The URI with the location at which the status of requested content can be monitored. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static Accepted Accepted(Uri uri, TValue? value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new(uri, value); + } + + /// + /// Produces a response. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static AcceptedAtRoute AcceptedAtRoute(string? routeName = null, object? routeValues = null) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + => new(routeName, routeValues); + + /// + /// Produces a response. + /// + /// The type of object that will be JSON serialized to the response body. + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to be included in the HTTP response body. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static AcceptedAtRoute AcceptedAtRoute(TValue? value, string? routeName = null, object? routeValues = null) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + => new(routeName, routeValues, value); + + /// + /// Produces an empty result response, that when executed will do nothing. + /// + public static EmptyHttpResult Empty { get; } = EmptyHttpResult.Instance; +} diff --git a/src/Http/Http.Results/src/UnauthorizedHttpResult.cs b/src/Http/Http.Results/src/UnauthorizedHttpResult.cs index 79bc2c17dd91..e6dfaec3d86f 100644 --- a/src/Http/Http.Results/src/UnauthorizedHttpResult.cs +++ b/src/Http/Http.Results/src/UnauthorizedHttpResult.cs @@ -1,11 +1,11 @@ // 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 Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// Represents an that when executed will /// produce an HTTP response with the No Unauthorized (401) status code. @@ -20,7 +20,7 @@ internal UnauthorizedHttpResult() } /// - /// Gets the HTTP status code. + /// Gets the HTTP status code: /// public int StatusCode => StatusCodes.Status401Unauthorized; @@ -36,5 +36,4 @@ public Task ExecuteAsync(HttpContext httpContext) return Task.CompletedTask; } - } diff --git a/src/Http/Http.Results/src/UnprocessableEntityObjectHttpResult.cs b/src/Http/Http.Results/src/UnprocessableEntity.cs similarity index 56% rename from src/Http/Http.Results/src/UnprocessableEntityObjectHttpResult.cs rename to src/Http/Http.Results/src/UnprocessableEntity.cs index d1c8eda62dae..eaa851ae1c07 100644 --- a/src/Http/Http.Results/src/UnprocessableEntityObjectHttpResult.cs +++ b/src/Http/Http.Results/src/UnprocessableEntity.cs @@ -1,32 +1,29 @@ // 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 Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Http.HttpResults; + /// /// An that on execution will write an object to the response /// with Unprocessable Entity (422) status code. /// -public sealed class UnprocessableEntityObjectHttpResult : IResult +public sealed class UnprocessableEntity : IResult, IEndpointMetadataProvider { /// - /// Initializes a new instance of the class with the values + /// Initializes a new instance of the class with the values /// provided. /// - /// The value to format in the entity body. - internal UnprocessableEntityObjectHttpResult(object? value) + internal UnprocessableEntity() { - Value = value; - HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); } - /// - public object? Value { get; internal init; } - - /// > + /// + /// Gets the HTTP status code: + /// public int StatusCode => StatusCodes.Status422UnprocessableEntity; /// @@ -36,10 +33,15 @@ public Task ExecuteAsync(HttpContext httpContext) var loggerFactory = httpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.UnprocessableEntityObjectResult"); - return HttpResultsHelper.WriteResultAsJsonAsync( - httpContext, - logger: logger, - Value, - StatusCode); + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity)); } } diff --git a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs new file mode 100644 index 000000000000..3232114b569f --- /dev/null +++ b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs @@ -0,0 +1,59 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with Unprocessable Entity (422) status code. +/// +/// The type of object that will be JSON serialized to the response body. +public sealed class UnprocessableEntity : IResult, IEndpointMetadataProvider +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The value to format in the entity body. + internal UnprocessableEntity(TValue? value) + { + Value = value; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// Gets the object result. + /// + public TValue? Value { get; } + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status422UnprocessableEntity; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + // Creating the logger with a string to preserve the category after the refactoring. + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Result.UnprocessableEntityObjectResult"); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger: logger, + Value); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status422UnprocessableEntity, "application/json")); + } +} diff --git a/src/Http/Http.Results/src/ValidationProblem.cs b/src/Http/Http.Results/src/ValidationProblem.cs new file mode 100644 index 000000000000..9812ec9ce0dc --- /dev/null +++ b/src/Http/Http.Results/src/ValidationProblem.cs @@ -0,0 +1,64 @@ +// 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; + +/// +/// An that on execution will write Problem Details +/// HTTP API responses based on https://tools.ietf.org/html/rfc7807 +/// +public sealed class ValidationProblem : IResult, IEndpointMetadataProvider +{ + internal ValidationProblem(HttpValidationProblemDetails problemDetails) + { + ArgumentNullException.ThrowIfNull(problemDetails, nameof(problemDetails)); + if (problemDetails is { Status: not null and not StatusCodes.Status400BadRequest }) + { + throw new ArgumentException($"{nameof(ValidationProblem)} only supports a 400 Bad Request response status code.", nameof(problemDetails)); + } + + ProblemDetails = problemDetails; + HttpResultsHelper.ApplyProblemDetailsDefaults(ProblemDetails, statusCode: StatusCodes.Status400BadRequest); + } + + /// + /// Gets the instance. + /// + public HttpValidationProblemDetails ProblemDetails { get; } + + /// + /// Gets the value for the Content-Type header: application/problem+json. + /// + public string ContentType => "application/problem+json"; + + /// + /// Gets the HTTP status code: + /// + public int StatusCode => StatusCodes.Status400BadRequest; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(typeof(ValidationProblem)); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + httpContext.Response.StatusCode = StatusCode; + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger, + value: ProblemDetails, + ContentType); + } + + /// + static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + { + context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, "application/problem+json")); + } +} diff --git a/src/Http/Http.Results/src/VirtualFileHttpResult.cs b/src/Http/Http.Results/src/VirtualFileHttpResult.cs index cac64665d23d..6739676d9700 100644 --- a/src/Http/Http.Results/src/VirtualFileHttpResult.cs +++ b/src/Http/Http.Results/src/VirtualFileHttpResult.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// A that on execution writes the file specified diff --git a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs new file mode 100644 index 000000000000..45fb26fd95f1 --- /dev/null +++ b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class AcceptedAtRouteOfTResultTests +{ + [Fact] + public void AcceptedAtRouteResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var routeValues = new RouteValueDictionary(new Dictionary() + { + { "test", "case" }, + { "sample", "route" } + }); + var obj = new HttpValidationProblemDetails(); + var result = new AcceptedAtRoute(routeValues, obj); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(StatusCodes.Status202Accepted, obj.Status); + Assert.Equal(obj, result.Value); + } + + [Fact] + public async Task ExecuteResultAsync_FormatsData() + { + // Arrange + var url = "testAction"; + var linkGenerator = new TestLinkGenerator { Url = url }; + var httpContext = GetHttpContext(linkGenerator); + var stream = new MemoryStream(); + httpContext.Response.Body = stream; + + var routeValues = new RouteValueDictionary(new Dictionary() + { + { "test", "case" }, + { "sample", "route" } + }); + + // Act + var result = new AcceptedAtRoute( + routeName: "sample", + routeValues: routeValues, + value: "Hello world"); + await result.ExecuteAsync(httpContext); + + // Assert + var response = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("\"Hello world\"", response); + } + + public static TheoryData AcceptedAtRouteData + { + get + { + return new TheoryData + { + null, + new Dictionary() + { + { "hello", "world" } + }, + new RouteValueDictionary( + new Dictionary() + { + { "test", "case" }, + { "sample", "route" } + }), + }; + } + } + + [Theory] + [MemberData(nameof(AcceptedAtRouteData))] + public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader(object values) + { + // Arrange + var expectedUrl = "testAction"; + var linkGenerator = new TestLinkGenerator { Url = expectedUrl }; + var httpContext = GetHttpContext(linkGenerator); + + // Act + var result = new AcceptedAtRoute(routeValues: values, value: null); + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task ExecuteResultAsync_ThrowsIfRouteUrlIsNull() + { + // Arrange + var linkGenerator = new TestLinkGenerator(); + var httpContext = GetHttpContext(linkGenerator); + + // Act + var result = new AcceptedAtRoute( + routeName: null, + routeValues: new Dictionary(), + value: null); + + // Assert + await ExceptionAssert.ThrowsAsync(() => + result.ExecuteAsync(httpContext), + "No route matches the supplied values."); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + AcceptedAtRoute MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + + private static HttpContext GetHttpContext(LinkGenerator linkGenerator) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(linkGenerator); + return httpContext; + } + + private static IServiceProvider CreateServices(LinkGenerator linkGenerator) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSingleton(linkGenerator); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs index ae0b8f754085..4c43df6dfb70 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs @@ -1,61 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; public class AcceptedAtRouteResultTests { - [Fact] - public void AcceptedAtRouteResult_ProblemDetails_SetsStatusCodeAndValue() - { - // Arrange & Act - var routeValues = new RouteValueDictionary(new Dictionary() - { - { "test", "case" }, - { "sample", "route" } - }); - var obj = new HttpValidationProblemDetails(); - var result = new AcceptedAtRouteHttpResult(routeValues, obj); - - // Assert - Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); - Assert.Equal(StatusCodes.Status202Accepted, obj.Status); - Assert.Equal(obj, result.Value); - } - - [Fact] - public async Task ExecuteResultAsync_FormatsData() - { - // Arrange - var url = "testAction"; - var linkGenerator = new TestLinkGenerator { Url = url }; - var httpContext = GetHttpContext(linkGenerator); - var stream = new MemoryStream(); - httpContext.Response.Body = stream; - - var routeValues = new RouteValueDictionary(new Dictionary() - { - { "test", "case" }, - { "sample", "route" } - }); - - // Act - var result = new AcceptedAtRouteHttpResult( - routeName: "sample", - routeValues: routeValues, - value: "Hello world"); - await result.ExecuteAsync(httpContext); - - // Assert - var response = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal("\"Hello world\"", response); - } - public static TheoryData AcceptedAtRouteData { get @@ -87,7 +43,7 @@ public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader(object valu var httpContext = GetHttpContext(linkGenerator); // Act - var result = new AcceptedAtRouteHttpResult(routeValues: values, value: null); + var result = new AcceptedAtRoute(routeValues: values); await result.ExecuteAsync(httpContext); // Assert @@ -103,10 +59,9 @@ public async Task ExecuteResultAsync_ThrowsIfRouteUrlIsNull() var httpContext = GetHttpContext(linkGenerator); // Act - var result = new AcceptedAtRouteHttpResult( + var result = new AcceptedAtRoute( routeName: null, - routeValues: new Dictionary(), - value: null); + routeValues: new Dictionary()); // Assert await ExceptionAssert.ThrowsAsync(() => @@ -114,6 +69,25 @@ await ExceptionAssert.ThrowsAsync(() => "No route matches the supplied values."); } + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + AcceptedAtRoute MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static HttpContext GetHttpContext(LinkGenerator linkGenerator) { var httpContext = new DefaultHttpContext(); diff --git a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs new file mode 100644 index 000000000000..3bc477cffad1 --- /dev/null +++ b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class AcceptedOfTResultTests +{ + [Fact] + public async Task ExecuteResultAsync_FormatsData() + { + // Arrange + var httpContext = GetHttpContext(); + var stream = new MemoryStream(); + httpContext.Response.Body = stream; + // Act + var result = new Accepted("my-location", value: "Hello world"); + await result.ExecuteAsync(httpContext); + + // Assert + var response = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("\"Hello world\"", response); + } + + [Fact] + public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() + { + // Arrange + var expectedUrl = "testAction"; + var httpContext = GetHttpContext(); + + // Act + var result = new Accepted(expectedUrl, value: "some-value"); + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + [Fact] + public void AcceptedResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var expectedUrl = "testAction"; + var obj = new HttpValidationProblemDetails(); + var result = new Accepted(expectedUrl, obj); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(StatusCodes.Status202Accepted, obj.Status); + Assert.Equal(obj, result.Value); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + Accepted MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(); + return httpContext; + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddLogging(); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/AcceptedResultTests.cs b/src/Http/Http.Results/test/AcceptedResultTests.cs index 9cf5eb6bbfb8..a8fad797f1ea 100644 --- a/src/Http/Http.Results/test/AcceptedResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedResultTests.cs @@ -1,29 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; public class AcceptedResultTests { - [Fact] - public async Task ExecuteResultAsync_FormatsData() - { - // Arrange - var httpContext = GetHttpContext(); - var stream = new MemoryStream(); - httpContext.Response.Body = stream; - // Act - var result = new AcceptedHttpResult("my-location", value: "Hello world"); - await result.ExecuteAsync(httpContext); - - // Assert - var response = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal("\"Hello world\"", response); - } - [Fact] public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() { @@ -32,7 +18,7 @@ public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() var httpContext = GetHttpContext(); // Act - var result = new AcceptedHttpResult(expectedUrl, value: "some-value"); + var result = new Accepted(expectedUrl); await result.ExecuteAsync(httpContext); // Assert @@ -41,19 +27,23 @@ public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() } [Fact] - public void AcceptedResult_ProblemDetails_SetsStatusCodeAndValue() + public void PopulateMetadata_AddsResponseTypeMetadata() { - // Arrange & Act - var expectedUrl = "testAction"; - var obj = new HttpValidationProblemDetails(); - var result = new AcceptedHttpResult(expectedUrl, obj); + // Arrange + Accepted MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); // Assert - Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); - Assert.Equal(StatusCodes.Status202Accepted, obj.Status); - Assert.Equal(obj, result.Value); + Assert.Contains(context.EndpointMetadata, m => m is ProducesResponseTypeMetadata { StatusCode: StatusCodes.Status202Accepted }); } + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static HttpContext GetHttpContext() { var httpContext = new DefaultHttpContext(); diff --git a/src/Http/Http.Results/test/BadRequestObjectResultTests.cs b/src/Http/Http.Results/test/BadRequestOfTResultTests.cs similarity index 50% rename from src/Http/Http.Results/test/BadRequestObjectResultTests.cs rename to src/Http/Http.Results/test/BadRequestOfTResultTests.cs index c72a82752af1..8d223c3b4681 100644 --- a/src/Http/Http.Results/test/BadRequestObjectResultTests.cs +++ b/src/Http/Http.Results/test/BadRequestOfTResultTests.cs @@ -1,21 +1,24 @@ // 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.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; +using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -public class BadRequestObjectResultTests +public class BadRequestOfTResultTests { [Fact] public void BadRequestObjectResult_SetsStatusCodeAndValue() { // Arrange & Act var obj = new object(); - var badRequestObjectResult = new BadRequestObjectHttpResult(obj); + var badRequestObjectResult = new BadRequest(obj); // Assert Assert.Equal(StatusCodes.Status400BadRequest, badRequestObjectResult.StatusCode); @@ -27,7 +30,7 @@ public void BadRequestObjectResult_ProblemDetails_SetsStatusCodeAndValue() { // Arrange & Act var obj = new HttpValidationProblemDetails(); - var result = new BadRequestObjectHttpResult(obj); + var result = new BadRequest(obj); // Assert Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); @@ -39,7 +42,7 @@ public void BadRequestObjectResult_ProblemDetails_SetsStatusCodeAndValue() public async Task BadRequestObjectResult_ExecuteAsync_SetsStatusCode() { // Arrange - var result = new BadRequestObjectHttpResult("Hello"); + var result = new BadRequest("Hello"); var httpContext = new DefaultHttpContext() { RequestServices = CreateServices(), @@ -56,7 +59,7 @@ public async Task BadRequestObjectResult_ExecuteAsync_SetsStatusCode() public async Task BadRequestObjectResult_ExecuteResultAsync_FormatsData() { // Arrange - var result = new BadRequestObjectHttpResult("Hello"); + var result = new BadRequest("Hello"); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -74,6 +77,50 @@ public async Task BadRequestObjectResult_ExecuteResultAsync_FormatsData() Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); } + [Fact] + public async Task BadRequestObjectResult_ExecuteResultAsync_UsesStatusCodeFromResultTypeForProblemDetails() + { + // Arrange + var details = new ProblemDetails { Status = StatusCodes.Status422UnprocessableEntity, }; + var result = new BadRequest(details); + + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, details.Status.Value); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + BadRequest MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status400BadRequest, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + private static IServiceProvider CreateServices() { var services = new ServiceCollection(); diff --git a/src/Http/Http.Results/test/BadRequestResultTests.cs b/src/Http/Http.Results/test/BadRequestResultTests.cs new file mode 100644 index 000000000000..30286b384bc9 --- /dev/null +++ b/src/Http/Http.Results/test/BadRequestResultTests.cs @@ -0,0 +1,67 @@ +// 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.HttpResults; + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +public class BadRequestResultTests +{ + [Fact] + public void BadRequestObjectResult_SetsStatusCode() + { + // Arrange & Act + var badRequestResult = new BadRequest(); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); + } + + [Fact] + public async Task BadRequestObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new BadRequest(); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + BadRequest MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status400BadRequest, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/ChallengeResultTest.cs b/src/Http/Http.Results/test/ChallengeResultTests.cs similarity index 95% rename from src/Http/Http.Results/test/ChallengeResultTest.cs rename to src/Http/Http.Results/test/ChallengeResultTests.cs index 95817e6ba449..a5d02031d54d 100644 --- a/src/Http/Http.Results/test/ChallengeResultTest.cs +++ b/src/Http/Http.Results/test/ChallengeResultTests.cs @@ -7,9 +7,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class ChallengeResultTest +public class ChallengeResultTests { [Fact] public async Task ChallengeResult_ExecuteAsync() diff --git a/src/Http/Http.Results/test/ConflictObjectResultTest.cs b/src/Http/Http.Results/test/ConflictOfTResultTests.cs similarity index 61% rename from src/Http/Http.Results/test/ConflictObjectResultTest.cs rename to src/Http/Http.Results/test/ConflictOfTResultTests.cs index 091d03bc605f..9ecf5b06dcb0 100644 --- a/src/Http/Http.Results/test/ConflictObjectResultTest.cs +++ b/src/Http/Http.Results/test/ConflictOfTResultTests.cs @@ -1,22 +1,24 @@ // 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.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; +using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -public class ConflictObjectResultTest +public class ConflictOfTResultTests { [Fact] public void ConflictObjectResult_SetsStatusCodeAndValue() { // Arrange & Act var obj = new object(); - var conflictObjectResult = new ConflictObjectHttpResult(obj); + var conflictObjectResult = new Conflict(obj); // Assert Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode); @@ -28,7 +30,7 @@ public void ConflictObjectResult_ProblemDetails_SetsStatusCodeAndValue() { // Arrange & Act var obj = new ProblemDetails(); - var conflictObjectResult = new ConflictObjectHttpResult(obj); + var conflictObjectResult = new Conflict(obj); // Assert Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode); @@ -40,7 +42,7 @@ public void ConflictObjectResult_ProblemDetails_SetsStatusCodeAndValue() public async Task ConflictObjectResult_ExecuteAsync_SetsStatusCode() { // Arrange - var result = new ConflictObjectHttpResult("Hello"); + var result = new Conflict("Hello"); var httpContext = new DefaultHttpContext() { RequestServices = CreateServices(), @@ -57,7 +59,7 @@ public async Task ConflictObjectResult_ExecuteAsync_SetsStatusCode() public async Task ConflictObjectResult_ExecuteResultAsync_FormatsData() { // Arrange - var result = new ConflictObjectHttpResult("Hello"); + var result = new Conflict("Hello"); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -75,6 +77,29 @@ public async Task ConflictObjectResult_ExecuteResultAsync_FormatsData() Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); } + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + Conflict MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status409Conflict, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + private static IServiceProvider CreateServices() { var services = new ServiceCollection(); diff --git a/src/Http/Http.Results/test/ConflictResultTests.cs b/src/Http/Http.Results/test/ConflictResultTests.cs new file mode 100644 index 000000000000..45337e675be0 --- /dev/null +++ b/src/Http/Http.Results/test/ConflictResultTests.cs @@ -0,0 +1,68 @@ +// 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.HttpResults; + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +public class ConflictResultTests +{ + [Fact] + public void ConflictObjectResult_SetsStatusCode() + { + // Arrange & Act + var conflictObjectResult = new Conflict(); + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode); + } + + [Fact] + public async Task ConflictObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new Conflict(); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, httpContext.Response.StatusCode); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + Conflict MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status409Conflict, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/ContentResultTest.cs b/src/Http/Http.Results/test/ContentResultTests.cs similarity index 98% rename from src/Http/Http.Results/test/ContentResultTest.cs rename to src/Http/Http.Results/test/ContentResultTests.cs index f45ea77e6226..7b316c410981 100644 --- a/src/Http/Http.Results/test/ContentResultTest.cs +++ b/src/Http/Http.Results/test/ContentResultTests.cs @@ -7,9 +7,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class ContentResultTest +public class ContentResultTests { [Fact] public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding() diff --git a/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs new file mode 100644 index 000000000000..afa6c0cfb4d6 --- /dev/null +++ b/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public partial class CreatedAtRouteOfTResultTests +{ + [Fact] + public void CreatedAtRouteResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var routeValues = new RouteValueDictionary(new Dictionary() + { + { "test", "case" }, + { "sample", "route" } + }); + var obj = new HttpValidationProblemDetails(); + var result = new CreatedAtRoute(routeValues, obj); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(StatusCodes.Status201Created, obj.Status); + Assert.Equal(obj, result.Value); + } + public static IEnumerable CreatedAtRouteData + { + get + { + yield return new object[] { null }; + yield return + new object[] { + new Dictionary() { { "hello", "world" } } + }; + yield return + new object[] { + new RouteValueDictionary(new Dictionary() { + { "test", "case" }, + { "sample", "route" } + }) + }; + } + } + + [Theory] + [MemberData(nameof(CreatedAtRouteData))] + public async Task CreatedAtRouteResult_ReturnsStatusCode_SetsLocationHeader(object values) + { + // Arrange + var expectedUrl = "testAction"; + var httpContext = GetHttpContext(expectedUrl); + + // Act + var result = new CreatedAtRoute(routeName: null, routeValues: values, value: null); + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task CreatedAtRouteResult_ThrowsOnNullUrl() + { + // Arrange + var httpContext = GetHttpContext(expectedUrl: null); + + var result = new CreatedAtRoute( + routeName: null, + routeValues: new Dictionary(), + value: null); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + async () => await result.ExecuteAsync(httpContext), + "No route matches the supplied values."); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + CreatedAtRoute MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + + private static HttpContext GetHttpContext(string expectedUrl) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.PathBase = new PathString(""); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = CreateServices(expectedUrl); + return httpContext; + } + + private static IServiceProvider CreateServices(string expectedUrl) + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(new TestLinkGenerator + { + Url = expectedUrl + }); + + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs index 5fd0575a3c35..bd8f0c2a0fb9 100644 --- a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs @@ -1,33 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; public partial class CreatedAtRouteResultTests { - [Fact] - public void CreatedAtRouteResult_ProblemDetails_SetsStatusCodeAndValue() - { - // Arrange & Act - var routeValues = new RouteValueDictionary(new Dictionary() - { - { "test", "case" }, - { "sample", "route" } - }); - var obj = new HttpValidationProblemDetails(); - var result = new CreatedAtRouteHttpResult(routeValues, obj); - - // Assert - Assert.Equal(StatusCodes.Status201Created, result.StatusCode); - Assert.Equal(StatusCodes.Status201Created, obj.Status); - Assert.Equal(obj, result.Value); - } public static IEnumerable CreatedAtRouteData { get @@ -56,7 +41,7 @@ public async Task CreatedAtRouteResult_ReturnsStatusCode_SetsLocationHeader(obje var httpContext = GetHttpContext(expectedUrl); // Act - var result = new CreatedAtRouteHttpResult(routeName: null, routeValues: values, value: null); + var result = new CreatedAtRoute(routeName: null, routeValues: values); await result.ExecuteAsync(httpContext); // Assert @@ -70,10 +55,9 @@ public async Task CreatedAtRouteResult_ThrowsOnNullUrl() // Arrange var httpContext = GetHttpContext(expectedUrl: null); - var result = new CreatedAtRouteHttpResult( + var result = new CreatedAtRoute( routeName: null, - routeValues: new Dictionary(), - value: null); + routeValues: new Dictionary()); // Act & Assert await ExceptionAssert.ThrowsAsync( @@ -81,6 +65,25 @@ await ExceptionAssert.ThrowsAsync( "No route matches the supplied values."); } + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + CreatedAtRoute MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static HttpContext GetHttpContext(string expectedUrl) { var httpContext = new DefaultHttpContext(); diff --git a/src/Http/Http.Results/test/CreatedOfTResultTests.cs b/src/Http/Http.Results/test/CreatedOfTResultTests.cs new file mode 100644 index 000000000000..f0ac3d838ffc --- /dev/null +++ b/src/Http/Http.Results/test/CreatedOfTResultTests.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class CreatedOfTResultTests +{ + [Fact] + public void CreatedResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var expectedUrl = "testAction"; + var obj = new HttpValidationProblemDetails(); + var result = new Created(expectedUrl, obj); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(StatusCodes.Status201Created, obj.Status); + Assert.Equal(obj, result.Value); + } + + [Fact] + public void CreatedResult_SetsLocation() + { + // Arrange + var location = "http://test/location"; + + // Act + var result = new Created(location, "testInput"); + + // Assert + Assert.Same(location, result.Location); + } + + [Fact] + public async Task CreatedResult_ReturnsStatusCode_SetsLocationHeader() + { + // Arrange + var location = "/test/"; + var httpContext = GetHttpContext(); + var result = new Created(location, "testInput"); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); + Assert.Equal(location, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task CreatedResult_OverwritesLocationHeader() + { + // Arrange + var location = "/test/"; + var httpContext = GetHttpContext(); + httpContext.Response.Headers["Location"] = "/different/location/"; + var result = new Created(location, "testInput"); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); + Assert.Equal(location, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task CreatedResult_ExecuteResultAsync_FormatsData() + { + // Arrange + var location = "/test/"; + var httpContext = GetHttpContext(); + var stream = new MemoryStream(); + httpContext.Response.Body = stream; + httpContext.Response.Headers["Location"] = "/different/location/"; + var result = new Created(location, "testInput"); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + var response = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("\"testInput\"", response); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + Created MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.PathBase = new PathString(""); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = CreateServices(); + return httpContext; + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/CreatedResultTest.cs b/src/Http/Http.Results/test/CreatedResultTests.cs similarity index 67% rename from src/Http/Http.Results/test/CreatedResultTest.cs rename to src/Http/Http.Results/test/CreatedResultTests.cs index 2bc4b0ffa180..0591cd7db003 100644 --- a/src/Http/Http.Results/test/CreatedResultTest.cs +++ b/src/Http/Http.Results/test/CreatedResultTests.cs @@ -1,28 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; public class CreatedResultTests { - [Fact] - public void CreatedResult_ProblemDetails_SetsStatusCodeAndValue() - { - // Arrange & Act - var expectedUrl = "testAction"; - var obj = new HttpValidationProblemDetails(); - var result = new CreatedHttpResult(expectedUrl, obj); - - // Assert - Assert.Equal(StatusCodes.Status201Created, result.StatusCode); - Assert.Equal(StatusCodes.Status201Created, obj.Status); - Assert.Equal(obj, result.Value); - } - [Fact] public void CreatedResult_SetsLocation() { @@ -30,7 +18,7 @@ public void CreatedResult_SetsLocation() var location = "http://test/location"; // Act - var result = new CreatedHttpResult(location, "testInput"); + var result = new Created(location); // Assert Assert.Same(location, result.Location); @@ -42,7 +30,7 @@ public async Task CreatedResult_ReturnsStatusCode_SetsLocationHeader() // Arrange var location = "/test/"; var httpContext = GetHttpContext(); - var result = new CreatedHttpResult(location, "testInput"); + var result = new Created(location); // Act await result.ExecuteAsync(httpContext); @@ -59,7 +47,7 @@ public async Task CreatedResult_OverwritesLocationHeader() var location = "/test/"; var httpContext = GetHttpContext(); httpContext.Response.Headers["Location"] = "/different/location/"; - var result = new CreatedHttpResult(location, "testInput"); + var result = new Created(location); // Act await result.ExecuteAsync(httpContext); @@ -69,6 +57,25 @@ public async Task CreatedResult_OverwritesLocationHeader() Assert.Equal(location, httpContext.Response.Headers["Location"]); } + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + Created MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static HttpContext GetHttpContext() { var httpContext = new DefaultHttpContext(); diff --git a/src/Http/Http.Results/test/EmptyResultTest.cs b/src/Http/Http.Results/test/EmptyResultTests.cs similarity index 93% rename from src/Http/Http.Results/test/EmptyResultTest.cs rename to src/Http/Http.Results/test/EmptyResultTests.cs index b8dd30d4dfff..cde7fde64971 100644 --- a/src/Http/Http.Results/test/EmptyResultTest.cs +++ b/src/Http/Http.Results/test/EmptyResultTests.cs @@ -3,9 +3,9 @@ using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class EmptyResultTest +public class EmptyResultTests { [Fact] public async Task EmptyResult_DoesNothing() diff --git a/src/Http/Http.Results/test/FileContentResultTest.cs b/src/Http/Http.Results/test/FileContentResultTests.cs similarity index 90% rename from src/Http/Http.Results/test/FileContentResultTest.cs rename to src/Http/Http.Results/test/FileContentResultTests.cs index 1628bcc8da6d..eba9c1ead376 100644 --- a/src/Http/Http.Results/test/FileContentResultTest.cs +++ b/src/Http/Http.Results/test/FileContentResultTests.cs @@ -7,9 +7,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class FileContentResultTest : FileContentResultTestBase +public class FileContentResultTests : FileContentResultTestBase { protected override Task ExecuteAsync( HttpContext httpContext, diff --git a/src/Http/Http.Results/test/ForbidResultTest.cs b/src/Http/Http.Results/test/ForbidResultTests.cs similarity index 98% rename from src/Http/Http.Results/test/ForbidResultTest.cs rename to src/Http/Http.Results/test/ForbidResultTests.cs index d3b770178ded..ee58a6539e68 100644 --- a/src/Http/Http.Results/test/ForbidResultTest.cs +++ b/src/Http/Http.Results/test/ForbidResultTests.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class ForbidResultTest +public class ForbidResultTests { [Fact] public async Task ExecuteResultAsync_InvokesForbidAsyncOnAuthenticationService() diff --git a/src/Http/Http.Results/test/FileStreamResultTest.cs b/src/Http/Http.Results/test/HttpFileStreamResultTests.cs similarity index 95% rename from src/Http/Http.Results/test/FileStreamResultTest.cs rename to src/Http/Http.Results/test/HttpFileStreamResultTests.cs index 82f7939da79a..40cbc188ee54 100644 --- a/src/Http/Http.Results/test/FileStreamResultTest.cs +++ b/src/Http/Http.Results/test/HttpFileStreamResultTests.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class FileStreamResultTest : FileStreamResultTestBase +public class HttpFileStreamResultTests : FileStreamResultTestBase { protected override Task ExecuteAsync( HttpContext httpContext, diff --git a/src/Http/Http.Results/test/ObjectResultTests.cs b/src/Http/Http.Results/test/JsonResultTests.cs similarity index 69% rename from src/Http/Http.Results/test/ObjectResultTests.cs rename to src/Http/Http.Results/test/JsonResultTests.cs index 9e81802a9788..d0804b3ab4d3 100644 --- a/src/Http/Http.Results/test/ObjectResultTests.cs +++ b/src/Http/Http.Results/test/JsonResultTests.cs @@ -8,15 +8,15 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class ObjectResultTests +public class JsonResultTests { [Fact] - public async Task ObjectResult_ExecuteAsync_WithNullValue_Works() + public async Task JsonResult_ExecuteAsync_WithNullValue_Works() { // Arrange - var result = new ObjectHttpResult(value: null, 411); + var result = new JsonHttpResult(value: null, statusCode: 411, jsonSerializerOptions: null); var httpContext = new DefaultHttpContext() { @@ -31,10 +31,10 @@ public async Task ObjectResult_ExecuteAsync_WithNullValue_Works() } [Fact] - public async Task ObjectResult_ExecuteAsync_SetsStatusCode() + public async Task JsonResult_ExecuteAsync_SetsStatusCode() { // Arrange - var result = new ObjectHttpResult("Hello", 407); + var result = new JsonHttpResult(value: null, statusCode: 407, jsonSerializerOptions: null); var httpContext = new DefaultHttpContext() { @@ -49,10 +49,10 @@ public async Task ObjectResult_ExecuteAsync_SetsStatusCode() } [Fact] - public async Task ObjectResult_ExecuteAsync_JsonSerializesBody() + public async Task JsonResult_ExecuteAsync_JsonSerializesBody() { // Arrange - var result = new ObjectHttpResult("Hello", 407); + var result = new JsonHttpResult(value: "Hello", statusCode: 407, jsonSerializerOptions: null); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -70,13 +70,50 @@ public async Task ObjectResult_ExecuteAsync_JsonSerializesBody() Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); } + [Fact] + public async Task JsonResult_ExecuteAsync_JsonSerializesBody_WithOptions() + { + // Arrange + var jsonOptions = new JsonSerializerOptions() + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + var value = new Todo(10, "MyName") { Description = null }; + var result = new JsonHttpResult(value, jsonSerializerOptions: jsonOptions); + var stream = new MemoryStream(); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + Response = + { + Body = stream, + }, + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); + + stream.Position = 0; + var responseDetails = JsonSerializer.Deserialize(stream); + Assert.Equal(value.Id, responseDetails.Id); + Assert.Equal(value.Title, responseDetails.Title); + Assert.Equal(value.Description, responseDetails.Description); + + stream.Position = 0; + Assert.Equal(JsonSerializer.Serialize(value, options: jsonOptions), Encoding.UTF8.GetString(stream.ToArray())); + } + [Fact] public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() { // Arrange var details = new ProblemDetails(); - var result = new ObjectHttpResult(details); + var result = new JsonHttpResult(details, jsonSerializerOptions: null); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -105,7 +142,7 @@ public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails() // Arrange var details = new HttpValidationProblemDetails(); - var result = new ObjectHttpResult(details); + var result = new JsonHttpResult(details, jsonSerializerOptions: null); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -134,7 +171,7 @@ public async Task ExecuteAsync_SetsProblemDetailsStatus_ForValidationProblemDeta // Arrange var details = new HttpValidationProblemDetails(); - var result = new ObjectHttpResult(details, StatusCodes.Status422UnprocessableEntity); + var result = new JsonHttpResult(details, StatusCodes.Status422UnprocessableEntity, jsonSerializerOptions: null); var httpContext = new DefaultHttpContext() { RequestServices = CreateServices(), @@ -153,7 +190,7 @@ public async Task ExecuteAsync_GetsStatusCodeFromProblemDetails() // Arrange var details = new ProblemDetails { Status = StatusCodes.Status413RequestEntityTooLarge, }; - var result = new ObjectHttpResult(details); + var result = new JsonHttpResult(details, jsonSerializerOptions: null); var httpContext = new DefaultHttpContext() { @@ -169,28 +206,6 @@ public async Task ExecuteAsync_GetsStatusCodeFromProblemDetails() Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, httpContext.Response.StatusCode); } - [Fact] - public async Task ExecuteAsync_UsesStatusCodeFromResultTypeForProblemDetails() - { - // Arrange - var details = new ProblemDetails { Status = StatusCodes.Status422UnprocessableEntity, }; - - var result = new BadRequestObjectHttpResult(details); - - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - }; - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(StatusCodes.Status422UnprocessableEntity, details.Status.Value); - Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); - Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); - } - private static IServiceProvider CreateServices() { var services = new ServiceCollection(); @@ -198,4 +213,9 @@ private static IServiceProvider CreateServices() return services.BuildServiceProvider(); } + + private record Todo(int Id, string Title) + { + public string Description { get; init; } + } } diff --git a/src/Http/Http.Results/test/LocalRedirectResultTest.cs b/src/Http/Http.Results/test/LocalRedirectResultTests.cs similarity index 98% rename from src/Http/Http.Results/test/LocalRedirectResultTest.cs rename to src/Http/Http.Results/test/LocalRedirectResultTests.cs index 33e49752fefd..982d0dbc0ceb 100644 --- a/src/Http/Http.Results/test/LocalRedirectResultTest.cs +++ b/src/Http/Http.Results/test/LocalRedirectResultTests.cs @@ -5,9 +5,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class LocalRedirectResultTest +public class LocalRedirectResultTests { [Fact] public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod() diff --git a/src/Http/Http.Results/test/NoContentResultTests.cs b/src/Http/Http.Results/test/NoContentResultTests.cs index 18b56774b6f5..de0c82a63277 100644 --- a/src/Http/Http.Results/test/NoContentResultTests.cs +++ b/src/Http/Http.Results/test/NoContentResultTests.cs @@ -1,8 +1,10 @@ // 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.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; +using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -13,7 +15,7 @@ public class NoContentResultTests public void NoContentResultTests_InitializesStatusCode() { // Arrange & act - var result = new NoContentHttpResult(); + var result = new NoContent(); // Assert Assert.Equal(StatusCodes.Status204NoContent, result.StatusCode); @@ -23,7 +25,7 @@ public void NoContentResultTests_InitializesStatusCode() public void NoContentResultTests_ExecuteResultSetsResponseStatusCode() { // Arrange - var result = new NoContentHttpResult(); + var result = new NoContent(); var httpContext = GetHttpContext(); @@ -34,6 +36,25 @@ public void NoContentResultTests_ExecuteResultSetsResponseStatusCode() Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode); } + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + NoContent MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status204NoContent, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static IServiceCollection CreateServices() { var services = new ServiceCollection(); diff --git a/src/Http/Http.Results/test/NotFoundOfTResultTests.cs b/src/Http/Http.Results/test/NotFoundOfTResultTests.cs new file mode 100644 index 000000000000..9a15ae0d76c7 --- /dev/null +++ b/src/Http/Http.Results/test/NotFoundOfTResultTests.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class NotFoundOfTResultTests +{ + [Fact] + public void NotFoundObjectResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var obj = new HttpValidationProblemDetails(); + var result = new NotFound(obj); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal(StatusCodes.Status404NotFound, obj.Status); + Assert.Equal(obj, result.Value); + } + + [Fact] + public void NotFoundObjectResult_InitializesStatusCode() + { + // Arrange & act + var notFound = new NotFound(null); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); + } + + [Fact] + public void NotFoundObjectResult_InitializesStatusCodeAndResponseContent() + { + // Arrange & act + var notFound = new NotFound("Test Content"); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); + Assert.Equal("Test Content", notFound.Value); + } + + [Fact] + public async Task NotFoundObjectResult_ExecuteSuccessful() + { + // Arrange + var httpContext = GetHttpContext(); + var result = new NotFound("Test Content"); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + NotFound MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status404NotFound, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.PathBase = new PathString(""); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = CreateServices(); + return httpContext; + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/NotFoundObjectResultTest.cs b/src/Http/Http.Results/test/NotFoundResultTests.cs similarity index 59% rename from src/Http/Http.Results/test/NotFoundObjectResultTest.cs rename to src/Http/Http.Results/test/NotFoundResultTests.cs index b9c4426e1b40..20d69a54e488 100644 --- a/src/Http/Http.Results/test/NotFoundObjectResultTest.cs +++ b/src/Http/Http.Results/test/NotFoundResultTests.cs @@ -1,62 +1,59 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class NotFoundObjectResultTest +public class NotFoundResultTests { - [Fact] - public void NotFoundObjectResult_ProblemDetails_SetsStatusCodeAndValue() - { - // Arrange & Act - var obj = new HttpValidationProblemDetails(); - var result = new NotFoundObjectHttpResult(obj); - - // Assert - Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); - Assert.Equal(StatusCodes.Status404NotFound, obj.Status); - Assert.Equal(obj, result.Value); - } - [Fact] public void NotFoundObjectResult_InitializesStatusCode() { // Arrange & act - var notFound = new NotFoundObjectHttpResult(null); + var notFound = new NotFound(); // Assert Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); } [Fact] - public void NotFoundObjectResult_InitializesStatusCodeAndResponseContent() + public async Task NotFoundObjectResult_ExecuteSuccessful() { - // Arrange & act - var notFound = new NotFoundObjectHttpResult("Test Content"); + // Arrange + var httpContext = GetHttpContext(); + var result = new NotFound(); + + // Act + await result.ExecuteAsync(httpContext); // Assert - Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); - Assert.Equal("Test Content", notFound.Value); + Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); } [Fact] - public async Task NotFoundObjectResult_ExecuteSuccessful() + public void PopulateMetadata_AddsResponseTypeMetadata() { // Arrange - var httpContext = GetHttpContext(); - var result = new NotFoundObjectHttpResult("Test Content"); + NotFound MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); // Act - await result.ExecuteAsync(httpContext); + PopulateMetadata(context); // Assert - Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status404NotFound, producesResponseTypeMetadata.StatusCode); } + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static HttpContext GetHttpContext() { var httpContext = new DefaultHttpContext(); diff --git a/src/Http/Http.Results/test/OkObjectResultTest.cs b/src/Http/Http.Results/test/OkObjectResultTest.cs deleted file mode 100644 index 64d4d85a8a82..000000000000 --- a/src/Http/Http.Results/test/OkObjectResultTest.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.Http.Result; - -public class OkObjectResultTest -{ - [Fact] - public async Task OkObjectResult_SetsStatusCodeAndValue() - { - // Arrange - var result = new OkObjectHttpResult("Hello world"); - var httpContext = GetHttpContext(); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); - } - - [Fact] - public void OkObjectResult_ProblemDetails_SetsStatusCodeAndValue() - { - // Arrange & Act - var obj = new HttpValidationProblemDetails(); - var result = new OkObjectHttpResult(obj); - - // Assert - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - Assert.Equal(StatusCodes.Status200OK, obj.Status); - Assert.Equal(obj, result.Value); - } - - [Fact] - public async Task OkObjectResult_ExecuteAsync_FormatsData() - { - // Arrange - var result = new OkObjectHttpResult("Hello"); - var stream = new MemoryStream(); - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - Response = - { - Body = stream, - }, - }; - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); - } - - private static HttpContext GetHttpContext() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.PathBase = new PathString(""); - httpContext.Response.Body = new MemoryStream(); - httpContext.RequestServices = CreateServices(); - return httpContext; - } - - private static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(); - return services.BuildServiceProvider(); - } -} diff --git a/src/Http/Http.Results/test/OkOfTResultTests.cs b/src/Http/Http.Results/test/OkOfTResultTests.cs new file mode 100644 index 000000000000..11e1af79a23e --- /dev/null +++ b/src/Http/Http.Results/test/OkOfTResultTests.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class OkOfTResultTests +{ + [Fact] + public void OkObjectResult_SetsStatusCodeAndValue() + { + // Arrange & Act + var value = "Hello world"; + var result = new Ok(value); + + // Assert + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void OkObjectResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var obj = new HttpValidationProblemDetails(); + var result = new Ok(obj); + + // Assert + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + Assert.Equal(StatusCodes.Status200OK, obj.Status); + Assert.Equal(obj, result.Value); + } + + [Fact] + public async Task OkObjectResult_ExecuteAsync_FormatsData() + { + // Arrange + var result = new Ok("Hello"); + var stream = new MemoryStream(); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + Response = + { + Body = stream, + }, + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public async Task OkObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new Ok("Hello"); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices() + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + Ok MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status200OK, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/OkResultTests.cs b/src/Http/Http.Results/test/OkResultTests.cs new file mode 100644 index 000000000000..c199a2b08634 --- /dev/null +++ b/src/Http/Http.Results/test/OkResultTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class OkResultTests +{ + [Fact] + public void OkObjectResult_SetsStatusCodeAndValue() + { + // Arrange & Act + var result = new Ok(); + + // Assert + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + } + + [Fact] + public async Task OkObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new Ok(); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices() + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + Ok MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status200OK, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/PhysicalFileResultTest.cs b/src/Http/Http.Results/test/PhysicalFileResultTest.cs index 6ba0c2f0b64a..e1a4c18b5d4d 100644 --- a/src/Http/Http.Results/test/PhysicalFileResultTest.cs +++ b/src/Http/Http.Results/test/PhysicalFileResultTest.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; public class PhysicalFileResultTest : PhysicalFileResultTestBase { diff --git a/src/Http/Http.Results/test/ProblemResultTests.cs b/src/Http/Http.Results/test/ProblemResultTests.cs index 28dfd0653cd8..35bf6ea7ba27 100644 --- a/src/Http/Http.Results/test/ProblemResultTests.cs +++ b/src/Http/Http.Results/test/ProblemResultTests.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; public class ProblemResultTests { diff --git a/src/Http/Http.Results/test/PushStreamResultTest.cs b/src/Http/Http.Results/test/PushStreamResultTests.cs similarity index 96% rename from src/Http/Http.Results/test/PushStreamResultTest.cs rename to src/Http/Http.Results/test/PushStreamResultTests.cs index a4791775dc75..669720f87f44 100644 --- a/src/Http/Http.Results/test/PushStreamResultTest.cs +++ b/src/Http/Http.Results/test/PushStreamResultTests.cs @@ -5,9 +5,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class PushStreamResultTest +public class PushStreamResultTests { [Fact] public async Task PushStreamResultsExposeTheResponseBody() diff --git a/src/Http/Http.Results/test/RedirectResultTest.cs b/src/Http/Http.Results/test/RedirectResultTests.cs similarity index 88% rename from src/Http/Http.Results/test/RedirectResultTest.cs rename to src/Http/Http.Results/test/RedirectResultTests.cs index 207e6c6850b9..8690e68a5be4 100644 --- a/src/Http/Http.Results/test/RedirectResultTest.cs +++ b/src/Http/Http.Results/test/RedirectResultTests.cs @@ -3,9 +3,9 @@ using Microsoft.AspNetCore.Internal; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class RedirectResultTest : RedirectResultTestBase +public class RedirectResultTests : RedirectResultTestBase { [Fact] public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMethod_SetsResultUrlPermanentAndPreservesMethod() diff --git a/src/Http/Http.Results/test/RedirectToRouteResultTest.cs b/src/Http/Http.Results/test/RedirectToRouteResultTests.cs similarity index 97% rename from src/Http/Http.Results/test/RedirectToRouteResultTest.cs rename to src/Http/Http.Results/test/RedirectToRouteResultTests.cs index 99dcac7836dc..d7f27a5561da 100644 --- a/src/Http/Http.Results/test/RedirectToRouteResultTest.cs +++ b/src/Http/Http.Results/test/RedirectToRouteResultTests.cs @@ -7,9 +7,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class RedirectToRouteResultTest +public class RedirectToRouteResultTests { [Fact] public async Task RedirectToRoute_Execute_ThrowsOnNullUrl() diff --git a/src/Http/Http.Results/test/ResultsCacheTests.cs b/src/Http/Http.Results/test/ResultsCacheTests.cs index 15593f88fdea..6058166f8bc7 100644 --- a/src/Http/Http.Results/test/ResultsCacheTests.cs +++ b/src/Http/Http.Results/test/ResultsCacheTests.cs @@ -1,7 +1,7 @@ // 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.Results.Tests; +namespace Microsoft.AspNetCore.Http.HttpResults; using Mono.TextTemplating; diff --git a/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs b/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs index f950aca6aada..a17541fa7c2b 100644 --- a/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs +++ b/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -63,7 +64,7 @@ Results MyApi(int checksum) public void ResultsOfTResult1TResult2_Throws_ArgumentNullException_WhenHttpContextIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -82,7 +83,7 @@ Results MyApi() public void ResultsOfTResult1TResult2_Throws_InvalidOperationException_WhenResultIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -271,7 +272,7 @@ Results MyApi(int checksum) public void ResultsOfTResult1TResult2TResult3_Throws_ArgumentNullException_WhenHttpContextIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -290,7 +291,7 @@ Results MyApi() public void ResultsOfTResult1TResult2TResult3_Throws_InvalidOperationException_WhenResultIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -548,7 +549,7 @@ Results MyAp public void ResultsOfTResult1TResult2TResult3TResult4_Throws_ArgumentNullException_WhenHttpContextIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -567,7 +568,7 @@ Results MyApi() public void ResultsOfTResult1TResult2TResult3TResult4_Throws_InvalidOperationException_WhenResultIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -902,7 +903,7 @@ Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -921,7 +922,7 @@ Results MyApi() public void ResultsOfTResult1TResult2TResult3TResult4TResult5_Throws_InvalidOperationException_WhenResultIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -1341,7 +1342,7 @@ Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } @@ -1360,7 +1361,7 @@ Results MyApi() public void ResultsOfTResult1TResult2TResult3TResult4TResult5TResult6_Throws_InvalidOperationException_WhenResultIsNull() { // Arrange - Results MyApi() + Results MyApi() { return new ChecksumResult1(1); } diff --git a/src/Http/Http.Results/test/ResultsTests.cs b/src/Http/Http.Results/test/ResultsTests.cs new file mode 100644 index 000000000000..213163608c06 --- /dev/null +++ b/src/Http/Http.Results/test/ResultsTests.cs @@ -0,0 +1,947 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; +using System.IO.Pipelines; +using System.Linq.Expressions; +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class ResultsTests +{ + [Fact] + public void Accepted_WithUrlAndValue_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.org"; + var value = new { }; + + // Act + var result = Results.Accepted(uri, value) as Accepted; + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri, result.Location); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Accepted_WithUrl_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.org"; + + // Act + var result = Results.Accepted(uri) as Accepted; + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri, result.Location); + } + + [Fact] + public void Accepted_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.Accepted() as Accepted; + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Null(result.Location); + } + + [Fact] + public void AcceptedAtRoute_WithRouteNameAndRouteValuesAndValue_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + var value = new { }; + + // Act + var result = Results.AcceptedAtRoute(routeName, routeValues, value) as AcceptedAtRoute; + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + Assert.Equal(value, result.Value); + } + + [Fact] + public void AcceptedAtRoute_WithRouteNameAndRouteValues_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + + // Act + var result = Results.AcceptedAtRoute(routeName, routeValues) as AcceptedAtRoute; + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + } + + [Fact] + public void AcceptedAtRoute_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.AcceptedAtRoute() as AcceptedAtRoute; + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Null(result.RouteName); + Assert.NotNull(result.RouteValues); + } + + [Fact] + public void BadRequest_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = Results.BadRequest(value) as BadRequest; + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void BadRequest_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.BadRequest() as BadRequest; + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + } + + [Theory] + [MemberData(nameof(BytesOrFile_ResultHasCorrectValues_Data))] + public void BytesOrFile_ResultHasCorrectValues(int bytesOrFile, string contentType, string fileDownloadName, bool enableRangeProcessing, DateTimeOffset lastModified, EntityTagHeaderValue entityTag) + { + // Arrange + var contents = new byte[0]; + + // Act + var result = bytesOrFile switch + { + 0 => Results.Bytes(contents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag), + _ => Results.File(contents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag) + } as FileContentHttpResult; + + // Assert + Assert.Equal(contents, result.FileContents); + Assert.Equal(contentType ?? "application/octet-stream", result.ContentType); + Assert.Equal(fileDownloadName, result.FileDownloadName); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); + Assert.Equal(lastModified, result.LastModified); + Assert.Equal(entityTag, result.EntityTag); + } + + public static IEnumerable BytesOrFile_ResultHasCorrectValues_Data => new List + { + new object[] { 0, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 0, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) }, + new object[] { 1, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 1, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) } + }; + + [Theory] + [MemberData(nameof(Stream_ResultHasCorrectValues_Data))] + public void Stream_ResultHasCorrectValues(int overload, string contentType, string fileDownloadName, bool enableRangeProcessing, DateTimeOffset lastModified, EntityTagHeaderValue entityTag) + { + // Arrange + var stream = new MemoryStream(); + + // Act + var result = overload switch + { + 0 => Results.Stream(stream, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing), + 1 => Results.Stream(PipeReader.Create(stream), contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing), + _ => Results.Stream((s) => Task.CompletedTask, contentType, fileDownloadName, lastModified, entityTag) + }; + + // Assert + switch (overload) + { + case <= 1: + var fileStreamResult = result as FileStreamHttpResult; + Assert.NotNull(fileStreamResult.FileStream); + Assert.Equal(contentType ?? "application/octet-stream", fileStreamResult.ContentType); + Assert.Equal(fileDownloadName, fileStreamResult.FileDownloadName); + Assert.Equal(enableRangeProcessing, fileStreamResult.EnableRangeProcessing); + Assert.Equal(lastModified, fileStreamResult.LastModified); + Assert.Equal(entityTag, fileStreamResult.EntityTag); + break; + + default: + var pushStreamResult = result as PushStreamHttpResult; + Assert.Equal(contentType ?? "application/octet-stream", pushStreamResult.ContentType); + Assert.Equal(fileDownloadName, pushStreamResult.FileDownloadName); + Assert.False(pushStreamResult.EnableRangeProcessing); + Assert.Equal(lastModified, pushStreamResult.LastModified); + Assert.Equal(entityTag, pushStreamResult.EntityTag); + break; + } + + } + + public static IEnumerable Stream_ResultHasCorrectValues_Data => new List + { + new object[] { 0, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 0, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) }, + new object[] { 1, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 1, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) }, + new object[] { 2, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 2, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) } + }; + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void Challenge_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Act + var result = Results.Challenge(properties, authenticationSchemes) as ChallengeHttpResult; + + // Assert + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes ?? new ReadOnlyCollection(new List()), result.AuthenticationSchemes); + } + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void Forbid_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Act + var result = Results.Forbid(properties, authenticationSchemes) as ForbidHttpResult; + + // Assert + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes ?? new ReadOnlyCollection(new List()), result.AuthenticationSchemes); + } + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void SignOut_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Act + var result = Results.SignOut(properties, authenticationSchemes) as SignOutHttpResult; + + // Assert + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes ?? new ReadOnlyCollection(new List()), result.AuthenticationSchemes); + } + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void SignIn_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Arrange + var principal = new ClaimsPrincipal(); + + // Act + var result = Results.SignIn(principal, properties, authenticationSchemes?.First()) as SignInHttpResult; + + // Assert + Assert.Equal(principal, result.Principal); + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes?.First(), result.AuthenticationScheme); + } + + public static IEnumerable ChallengeForbidSignInOut_ResultHasCorrectValues_Data => new List + { + new object[] { new AuthenticationProperties(), new List { "TestScheme" } }, + new object[] { new AuthenticationProperties(), default(IList) }, + new object[] { default(AuthenticationProperties), new List { "TestScheme" } }, + new object[] { default(AuthenticationProperties), default(IList) }, + }; + + [Fact] + public void SignIn_WithNullPrincipal_ThrowsArgNullException() + { + Assert.Throws("principal", () => Results.SignIn(null)); + } + + [Fact] + public void Conflict_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = Results.Conflict(value) as Conflict; + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Conflict_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.Conflict() as Conflict; + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode); + } + + [Fact] + public void Content_WithContentAndMediaType_ResultHasCorrectValues() + { + // Arrange + var content = "test content"; + var mediaType = MediaTypeHeaderValue.Parse("text/plain"); + + // Act + var result = Results.Content(content, mediaType) as ContentHttpResult; + + // Assert + Assert.Null(result.StatusCode); + Assert.Equal(content, result.ResponseContent); + Assert.Equal(mediaType.ToString(), result.ContentType); + } + + [Fact] + public void Content_WithContentAndContentTypeAndEncoding_ResultHasCorrectValues() + { + // Arrange + var content = "test content"; + var contentType = "text/plain"; + var encoding = Encoding.UTF8; + + // Act + var result = Results.Content(content, contentType, null) as ContentHttpResult; + + // Assert + Assert.Null(result.StatusCode); + Assert.Equal(content, result.ResponseContent); + Assert.Equal(contentType, result.ContentType); + } + + [Fact] + public void Created_WithStringUriAndValue_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.com/entity"; + var value = new { }; + + // Act + var result = Results.Created(uri, value) as Created; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri, result.Location); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Created_WithStringUri_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.com/entity"; + + // Act + var result = Results.Created(uri, null) as Created; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri, result.Location); + } + + [Fact] + public void Created_WithUriAndValue_ResultHasCorrectValues() + { + // Arrange + var uri = new Uri("https://example.com/entity"); + var value = new { }; + + // Act + var result = Results.Created(uri, value) as Created; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri.ToString(), result.Location); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Created_WithUri_ResultHasCorrectValues() + { + // Arrange + var uri = new Uri("https://example.com/entity"); + + // Act + var result = Results.Created(uri, null) as Created; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri.ToString(), result.Location); + } + + [Fact] + public void Created_WithNullStringUri_ThrowsArgNullException() + { + Assert.Throws("uri", () => Results.Created(default(string), null)); + } + + [Fact] + public void Created_WithNullUri_ThrowsArgNullException() + { + Assert.Throws("uri", () => Results.Created(default(Uri), null)); + } + + [Fact] + public void CreatedAtRoute_WithRouteNameAndRouteValuesAndValue_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + var value = new { }; + + // Act + var result = Results.CreatedAtRoute(routeName, routeValues, value) as CreatedAtRoute; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + Assert.Equal(value, result.Value); + } + + [Fact] + public void CreatedAtRoute_WithRouteNameAndValue_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var value = new { }; + + // Act + var result = Results.CreatedAtRoute(routeName, null, value) as CreatedAtRoute; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(), result.RouteValues); + Assert.Equal(value, result.Value); + } + + [Fact] + public void CreatedAtRoute_WithRouteName_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + + // Act + var result = Results.CreatedAtRoute(routeName, null, null) as CreatedAtRoute; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(), result.RouteValues); + } + + [Fact] + public void CreatedAtRoute_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.CreatedAtRoute() as CreatedAtRoute; + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Null(result.RouteName); + Assert.Equal(new RouteValueDictionary(), result.RouteValues); + } + + [Fact] + public void Empty_IsEmptyInstance() + { + // Act + var result = Results.Empty as EmptyHttpResult; + + // Assert + Assert.Equal(EmptyHttpResult.Instance, result); + } + + [Fact] + public void Json_WithAllArgs_ResultHasCorrectValues() + { + // Arrange + var data = new { }; + var options = new JsonSerializerOptions(); + var contentType = "application/custom+json"; + var statusCode = StatusCodes.Status208AlreadyReported; + + // Act + var result = Results.Json(data, options, contentType, statusCode) as JsonHttpResult; + + // Assert + Assert.Equal(data, result.Value); + Assert.Equal(options, result.JsonSerializerOptions); + Assert.Equal(contentType, result.ContentType); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void Json_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.Json(null) as JsonHttpResult; + + // Assert + Assert.Null(result.Value); + Assert.Null(result.JsonSerializerOptions); + Assert.Null(result.ContentType); + Assert.Null(result.StatusCode); + } + + [Fact] + public void LocalRedirect_WithUrl_ResultHasCorrectValues() + { + // Arrange + var localUrl = "test/path"; + + // Act + var result = Results.LocalRedirect(localUrl) as RedirectHttpResult; + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.False(result.Permanent); + Assert.False(result.PreserveMethod); + } + + [Fact] + public void LocalRedirect_WithUrlAndPermanentTrue_ResultHasCorrectValues() + { + // Arrange + var localUrl = "test/path"; + var permanent = true; + + // Act + var result = Results.LocalRedirect(localUrl, permanent) as RedirectHttpResult; + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.True(result.Permanent); + Assert.False(result.PreserveMethod); + } + + [Fact] + public void LocalRedirect_WithUrlAndPermanentTrueAndPreserveTrue_ResultHasCorrectValues() + { + // Arrange + var localUrl = "test/path"; + var permanent = true; + var preserveMethod = true; + + // Act + var result = Results.LocalRedirect(localUrl, permanent, preserveMethod) as RedirectHttpResult; + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.True(result.Permanent); + Assert.True(result.PreserveMethod); + } + + [Fact] + public void LocalRedirect_WithNonLocalUrlAndPermanentTrueAndPreserveTrue_ResultHasCorrectValues() + { + // Arrange + var localUrl = "https://example.com/non-local-url/example"; + var permanent = true; + var preserveMethod = true; + + // Act + var result = Results.LocalRedirect(localUrl, permanent, preserveMethod) as RedirectHttpResult; + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.True(result.Permanent); + Assert.True(result.PreserveMethod); + } + + [Fact] + public void NoContent_ResultHasCorrectValues() + { + // Act + var result = Results.NoContent() as NoContent; + + // Assert + Assert.Equal(StatusCodes.Status204NoContent, result.StatusCode); + } + + [Fact] + public void NotFound_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = Results.NotFound(value) as NotFound; + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void NotFound_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.NotFound() as NotFound; + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + } + + [Fact] + public void Ok_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = Results.Ok(value) as Ok; + + // Assert + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Ok_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.Ok() as Ok; + + // Assert + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + } + + [Fact] + public void Problem_WithArgs_ResultHasCorrectValues() + { + // Arrange + var detail = "test detail"; + var instance = "test instance"; + var statusCode = StatusCodes.Status409Conflict; + var title = "test title"; + var type = "test type"; + var extensions = new Dictionary { { "test", "value" } }; + + // Act + var result = Results.Problem(detail, instance, statusCode, title, type, extensions) as ProblemHttpResult; + + // Assert + Assert.Equal(detail, result.ProblemDetails.Detail); + Assert.Equal(instance, result.ProblemDetails.Instance); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(statusCode, result.StatusCode); + Assert.Equal(title, result.ProblemDetails.Title); + Assert.Equal(type, result.ProblemDetails.Type); + Assert.Equal(extensions, result.ProblemDetails.Extensions); + } + + [Fact] + public void Problem_WithNoArgs_ResultHasCorrectValues() + { + /// Act + var result = Results.Problem() as ProblemHttpResult; + + // Assert + Assert.Null(result.ProblemDetails.Detail); + Assert.Null(result.ProblemDetails.Instance); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("An error occurred while processing your request.", result.ProblemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", result.ProblemDetails.Type); + Assert.Empty(result.ProblemDetails.Extensions); + } + + [Fact] + public void Problem_WithProblemArg_ResultHasCorrectValues() + { + // Arrange + var problem = new ProblemDetails { Title = "Test title" }; + + // Act + var result = Results.Problem(problem) as ProblemHttpResult; + + // Assert + Assert.Equal(problem, result.ProblemDetails); + Assert.Equal("Test title", result.ProblemDetails.Title); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + } + + [Fact] + public void Problem_WithValidationProblemArg_ResultHasCorrectValues() + { + // Arrange + var problem = new HttpValidationProblemDetails { Title = "Test title" }; + + // Act + var result = Results.Problem(problem) as ProblemHttpResult; + + // Assert + Assert.Equal(problem, result.ProblemDetails); + Assert.Equal("Test title", result.ProblemDetails.Title); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + } + + [Fact] + public void ValidationProblem_WithValidationProblemArg_ResultHasCorrectValues() + { + // Arrange + var errors = new Dictionary() { { "testField", new[] { "test error" } } }; + var detail = "test detail"; + var instance = "test instance"; + var statusCode = StatusCodes.Status412PreconditionFailed; // obscure for the test on purpose + var title = "test title"; + var type = "test type"; + var extensions = new Dictionary() { { "testExtension", "test value" } }; + + // Act + // Note: Results.ValidationProblem returns ProblemHttpResult instead of ValidationProblem by design as + // as ValidationProblem doesn't allow setting a custom status code so that it can accurately report + // a single status code in endpoint metadata via its implementation of IEndpointMetadataProvider + var result = Results.ValidationProblem(errors, detail, instance, statusCode, title, type, extensions) as ProblemHttpResult; + + // Assert + Assert.IsType(result.ProblemDetails); + Assert.Equal(errors, ((HttpValidationProblemDetails)result.ProblemDetails).Errors); + Assert.Equal(detail, result.ProblemDetails.Detail); + Assert.Equal(instance, result.ProblemDetails.Instance); + Assert.Equal(statusCode, result.ProblemDetails.Status); + Assert.Equal(statusCode, result.StatusCode); + Assert.Equal(title, result.ProblemDetails.Title); + Assert.Equal(type, result.ProblemDetails.Type); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(extensions, result.ProblemDetails.Extensions); + } + + [Fact] + public void Redirect_WithDefaults_ResultHasCorrectValues() + { + // Arrange + var url = "https://example.com"; + + // Act + var result = Results.Redirect(url) as RedirectHttpResult; + + // Assert + Assert.Equal(url, result.Url); + Assert.False(result.PreserveMethod); + Assert.False(result.Permanent); + Assert.False(result.AcceptLocalUrlOnly); + } + + [Fact] + public void Redirect_WithPermanentTrue_ResultHasCorrectValues() + { + // Arrange + var url = "https://example.com"; + + // Act + var result = Results.Redirect(url, true) as RedirectHttpResult; + + // Assert + Assert.Equal(url, result.Url); + Assert.False(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.False(result.AcceptLocalUrlOnly); + } + + [Fact] + public void Redirect_WithPreserveMethodTrue_ResultHasCorrectValues() + { + // Arrange + var url = "https://example.com"; + + // Act + var result = Results.Redirect(url, false, true) as RedirectHttpResult; + + // Assert + Assert.Equal(url, result.Url); + Assert.True(result.PreserveMethod); + Assert.False(result.Permanent); + Assert.False(result.AcceptLocalUrlOnly); + } + + [Fact] + public void RedirectToRoute_WithRouteNameAndRouteValuesAndFragment_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + var fragment = "test"; + + // Act + var result = Results.RedirectToRoute(routeName, routeValues, true, true, fragment) as RedirectToRouteHttpResult; + + // Assert + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + Assert.True(result.Permanent); + Assert.True(result.PreserveMethod); + Assert.Equal(fragment, result.Fragment); + } + + [Fact] + public void RedirectToRoute_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = Results.RedirectToRoute() as RedirectToRouteHttpResult; + + // Assert + Assert.Null(result.RouteName); + Assert.Null(result.RouteValues); + } + + [Fact] + public void StatusCode_ResultHasCorrectValues() + { + // Arrange + var statusCode = StatusCodes.Status412PreconditionFailed; + + // Act + var result = Results.StatusCode(statusCode) as StatusCodeHttpResult; + + // Assert + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void Text_WithContentAndContentTypeAndEncoding_ResultHasCorrectValues() + { + // Arrange + var content = "test content"; + var contentType = "text/plain"; + var encoding = Encoding.ASCII; + + // Act + var result = Results.Text(content, contentType, encoding) as ContentHttpResult; + + // Assert + Assert.Null(result.StatusCode); + Assert.Equal(content, result.ResponseContent); + var expectedMediaType = MediaTypeHeaderValue.Parse(contentType); + expectedMediaType.Encoding = encoding; + Assert.Equal(expectedMediaType.ToString(), result.ContentType); + } + + [Fact] + public void Unauthorized_ResultHasCorrectValues() + { + // Act + var result = Results.Unauthorized() as UnauthorizedHttpResult; + + // Assert + Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); + } + + [Fact] + public void UnprocessableEntity_ResultHasCorrectValues() + { + // Act + var result = Results.UnprocessableEntity() as UnprocessableEntity; + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); + } + + [Theory] + [MemberData(nameof(FactoryMethodsFromTuples))] + public void FactoryMethod_ReturnsCorrectResultType(Expression> expression, Type expectedReturnType) + { + var method = expression.Compile(); + Assert.IsType(expectedReturnType, method()); + } + + [Fact] + public void TestTheTests() + { + var testedMethods = new HashSet(FactoryMethodsTuples.Select(t => GetMemberName(t.Item1.Body))); + var actualMethods = new HashSet(typeof(Results).GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => !m.IsSpecialName) + .Select(m => m.Name)); + + // Ensure every static method on Results type is covered by at least the default case for its parameters + Assert.All(actualMethods, name => Assert.Single(testedMethods, name)); + } + + private static string GetMemberName(Expression expression) + { + return expression switch + { + MethodCallExpression mce => mce.Method.Name, + MemberExpression me => me.Member.Name, + _ => throw new InvalidOperationException() + }; + } + + private static IEnumerable<(Expression>, Type)> FactoryMethodsTuples { get; } = new List<(Expression>, Type)> + { + (() => Results.Accepted(null, null), typeof(Accepted)), + (() => Results.Accepted(null, new()), typeof(Accepted)), + (() => Results.AcceptedAtRoute("routeName", null, null), typeof(AcceptedAtRoute)), + (() => Results.AcceptedAtRoute("routeName", null, new()), typeof(AcceptedAtRoute)), + (() => Results.BadRequest(null), typeof(BadRequest)), + (() => Results.BadRequest(new()), typeof(BadRequest)), + (() => Results.Bytes(new byte[0], null, null, false, null, null), typeof(FileContentHttpResult)), + (() => Results.Challenge(null, null), typeof(ChallengeHttpResult)), + (() => Results.Conflict(null), typeof(Conflict)), + (() => Results.Conflict(new()), typeof(Conflict)), + (() => Results.Content("content", null, null), typeof(ContentHttpResult)), + (() => Results.Created("/path", null), typeof(Created)), + (() => Results.Created("/path", new()), typeof(Created)), + (() => Results.CreatedAtRoute("routeName", null, null), typeof(CreatedAtRoute)), + (() => Results.CreatedAtRoute("routeName", null, new()), typeof(CreatedAtRoute)), + (() => Results.Empty, typeof(EmptyHttpResult)), + (() => Results.File(new byte[0], null, null, false, null, null), typeof(FileContentHttpResult)), + (() => Results.File(new MemoryStream(), null, null, null, null, false), typeof(FileStreamHttpResult)), + (() => Results.File(Path.Join(Path.DirectorySeparatorChar.ToString(), "rooted", "path"), null, null, null, null, false), typeof(PhysicalFileHttpResult)), + (() => Results.File("path", null, null, null, null, false), typeof(VirtualFileHttpResult)), + (() => Results.Forbid(null, null), typeof(ForbidHttpResult)), + (() => Results.Json(new(), null, null, null), typeof(JsonHttpResult)), + (() => Results.NoContent(), typeof(NoContent)), + (() => Results.NotFound(null), typeof(NotFound)), + (() => Results.NotFound(new()), typeof(NotFound)), + (() => Results.Ok(null), typeof(Ok)), + (() => Results.Ok(new()), typeof(Ok)), + (() => Results.Problem(new()), typeof(ProblemHttpResult)), + (() => Results.Stream(new MemoryStream(), null, null, null, null, false), typeof(FileStreamHttpResult)), + (() => Results.Stream(s => Task.CompletedTask, null, null, null, null), typeof(PushStreamHttpResult)), + (() => Results.Text("content", null, null), typeof(ContentHttpResult)), + (() => Results.Redirect("/path", false, false), typeof(RedirectHttpResult)), + (() => Results.LocalRedirect("/path", false, false), typeof(RedirectHttpResult)), + (() => Results.RedirectToRoute("routeName", null, false, false, null), typeof(RedirectToRouteHttpResult)), + (() => Results.SignIn(new(), null, null), typeof(SignInHttpResult)), + (() => Results.SignOut(new(), null), typeof(SignOutHttpResult)), + (() => Results.StatusCode(200), typeof(StatusCodeHttpResult)), + (() => Results.Unauthorized(), typeof(UnauthorizedHttpResult)), + (() => Results.UnprocessableEntity(null), typeof(UnprocessableEntity)), + (() => Results.UnprocessableEntity(new()), typeof(UnprocessableEntity)), + (() => Results.ValidationProblem(new Dictionary(), null, null, null, null, null, null), typeof(ProblemHttpResult)) + }; + + public static IEnumerable FactoryMethodsFromTuples() => FactoryMethodsTuples.Select(t => new object[] { t.Item1, t.Item2 }); +} diff --git a/src/Http/Http.Results/test/SignInResultTest.cs b/src/Http/Http.Results/test/SignInResultTests.cs similarity index 97% rename from src/Http/Http.Results/test/SignInResultTest.cs rename to src/Http/Http.Results/test/SignInResultTests.cs index 6aef97223c54..3d4c4136740a 100644 --- a/src/Http/Http.Results/test/SignInResultTest.cs +++ b/src/Http/Http.Results/test/SignInResultTests.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class SignInResultTest +public class SignInResultTests { [Fact] public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManager() diff --git a/src/Http/Http.Results/test/SignOutResultTest.cs b/src/Http/Http.Results/test/SignOutResultTests.cs similarity index 97% rename from src/Http/Http.Results/test/SignOutResultTest.cs rename to src/Http/Http.Results/test/SignOutResultTests.cs index d3cfa5073659..386b29e928b6 100644 --- a/src/Http/Http.Results/test/SignOutResultTest.cs +++ b/src/Http/Http.Results/test/SignOutResultTests.cs @@ -7,9 +7,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class SignOutResultTest +public class SignOutResultTests { [Fact] public async Task ExecuteAsync_NoArgsInvokesDefaultSignOut() diff --git a/src/Http/Http.Results/test/StatusCodeResultTests.cs b/src/Http/Http.Results/test/StatusCodeResultTests.cs index ae3493329307..4e28357b5b19 100644 --- a/src/Http/Http.Results/test/StatusCodeResultTests.cs +++ b/src/Http/Http.Results/test/StatusCodeResultTests.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; public class StatusCodeResultTests { diff --git a/src/Http/Http.Results/test/TestLinkGenerator.cs b/src/Http/Http.Results/test/TestLinkGenerator.cs index 38d6ea9fe92f..41b1aa1c2cb2 100644 --- a/src/Http/Http.Results/test/TestLinkGenerator.cs +++ b/src/Http/Http.Results/test/TestLinkGenerator.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; internal sealed class TestLinkGenerator : LinkGenerator { diff --git a/src/Http/Http.Results/test/TypedResultsTests.cs b/src/Http/Http.Results/test/TypedResultsTests.cs new file mode 100644 index 000000000000..c927bd942345 --- /dev/null +++ b/src/Http/Http.Results/test/TypedResultsTests.cs @@ -0,0 +1,907 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; +using System.IO.Pipelines; +using System.Linq.Expressions; +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class TypedResultsTests +{ + [Fact] + public void Accepted_WithStringUrlAndValue_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.org"; + var value = new { }; + + // Act + var result = TypedResults.Accepted(uri, value); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri, result.Location); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Accepted_WithStringUrl_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.org"; + + // Act + var result = TypedResults.Accepted(uri); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri, result.Location); + } + + [Fact] + public void Accepted_WithNullStringUrl_ResultHasCorrectValues() + { + // Arrange + var uri = default(string); + + // Act + var result = TypedResults.Accepted(uri); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri, result.Location); + } + + [Fact] + public void Accepted_WithUriAndValue_ResultHasCorrectValues() + { + // Arrange + var uri = new Uri("https://example.org"); + var value = new { }; + + // Act + var result = TypedResults.Accepted(uri, value); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri.ToString(), result.Location); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Accepted_WithUri_ResultHasCorrectValues() + { + // Arrange + var uri = new Uri("https://example.org"); + + // Act + var result = TypedResults.Accepted(uri); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri.ToString(), result.Location); + } + + [Fact] + public void Accepted_WithNullUri_ThrowsArgumentNullException() + { + Assert.Throws("uri", () => TypedResults.Accepted(default(Uri))); + } + + [Fact] + public void AcceptedAtRoute_WithRouteNameAndRouteValuesAndValue_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + var value = new { }; + + // Act + var result = TypedResults.AcceptedAtRoute(value, routeName, routeValues); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + Assert.Equal(value, result.Value); + } + + [Fact] + public void AcceptedAtRoute_WithRouteNameAndRouteValues_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + + // Act + var result = TypedResults.AcceptedAtRoute(routeName, routeValues); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + } + + [Fact] + public void AcceptedAtRoute_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = TypedResults.AcceptedAtRoute(); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Null(result.RouteName); + Assert.NotNull(result.RouteValues); + } + + [Fact] + public void BadRequest_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = TypedResults.BadRequest(value); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void BadRequest_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = TypedResults.BadRequest(); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + } + + [Theory] + [MemberData(nameof(BytesOrFile_ResultHasCorrectValues_Data))] + public void BytesOrFile_ResultHasCorrectValues(int bytesOrFile, string contentType, string fileDownloadName, bool enableRangeProcessing, DateTimeOffset lastModified, EntityTagHeaderValue entityTag) + { + // Arrange + var contents = new byte[0]; + + // Act + var result = bytesOrFile switch + { + 0 => TypedResults.Bytes(contents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag), + _ => TypedResults.File(contents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag) + }; + + // Assert + Assert.Equal(contents, result.FileContents); + Assert.Equal(contentType ?? "application/octet-stream", result.ContentType); + Assert.Equal(fileDownloadName, result.FileDownloadName); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); + Assert.Equal(lastModified, result.LastModified); + Assert.Equal(entityTag, result.EntityTag); + } + + public static IEnumerable BytesOrFile_ResultHasCorrectValues_Data => new List + { + new object[] { 0, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 0, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) }, + new object[] { 1, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 1, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) } + }; + + [Theory] + [MemberData(nameof(Stream_ResultHasCorrectValues_Data))] + public void Stream_ResultHasCorrectValues(int overload, string contentType, string fileDownloadName, bool enableRangeProcessing, DateTimeOffset lastModified, EntityTagHeaderValue entityTag) + { + // Arrange + var stream = new MemoryStream(); + + // Act + var result = overload switch + { + 0 => TypedResults.Stream(stream, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing), + 1 => TypedResults.Stream(PipeReader.Create(stream), contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing), + _ => (IResult)TypedResults.Stream((s) => Task.CompletedTask, contentType, fileDownloadName, lastModified, entityTag) + }; + + // Assert + switch (overload) + { + case <= 1: + var fileStreamResult = result as FileStreamHttpResult; + Assert.NotNull(fileStreamResult.FileStream); + Assert.Equal(contentType ?? "application/octet-stream", fileStreamResult.ContentType); + Assert.Equal(fileDownloadName, fileStreamResult.FileDownloadName); + Assert.Equal(enableRangeProcessing, fileStreamResult.EnableRangeProcessing); + Assert.Equal(lastModified, fileStreamResult.LastModified); + Assert.Equal(entityTag, fileStreamResult.EntityTag); + break; + + default: + var pushStreamResult = result as PushStreamHttpResult; + Assert.Equal(contentType ?? "application/octet-stream", pushStreamResult.ContentType); + Assert.Equal(fileDownloadName, pushStreamResult.FileDownloadName); + Assert.False(pushStreamResult.EnableRangeProcessing); + Assert.Equal(lastModified, pushStreamResult.LastModified); + Assert.Equal(entityTag, pushStreamResult.EntityTag); + break; + } + + } + + public static IEnumerable Stream_ResultHasCorrectValues_Data => new List + { + new object[] { 0, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 0, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) }, + new object[] { 1, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 1, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) }, + new object[] { 2, "text/plain", "testfile", true, new DateTimeOffset(2022, 1, 1, 0, 0, 1, TimeSpan.FromHours(-8)), EntityTagHeaderValue.Any }, + new object[] { 2, default(string), default(string), default(bool), default(DateTimeOffset?), default(EntityTagHeaderValue) } + }; + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void Challenge_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Act + var result = TypedResults.Challenge(properties, authenticationSchemes); + + // Assert + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes ?? new ReadOnlyCollection(new List()), result.AuthenticationSchemes); + } + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void Forbid_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Act + var result = TypedResults.Forbid(properties, authenticationSchemes); + + // Assert + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes ?? new ReadOnlyCollection(new List()), result.AuthenticationSchemes); + } + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void SignOut_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Act + var result = TypedResults.SignOut(properties, authenticationSchemes); + + // Assert + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes ?? new ReadOnlyCollection(new List()), result.AuthenticationSchemes); + } + + [Theory] + [MemberData(nameof(ChallengeForbidSignInOut_ResultHasCorrectValues_Data))] + public void SignIn_ResultHasCorrectValues(AuthenticationProperties properties, IList authenticationSchemes) + { + // Arrange + var principal = new ClaimsPrincipal(); + + // Act + var result = TypedResults.SignIn(principal, properties, authenticationSchemes?.First()); + + // Assert + Assert.Equal(principal, result.Principal); + Assert.Equal(properties, result.Properties); + Assert.Equal(authenticationSchemes?.First(), result.AuthenticationScheme); + } + + public static IEnumerable ChallengeForbidSignInOut_ResultHasCorrectValues_Data => new List + { + new object[] { new AuthenticationProperties(), new List { "TestScheme" } }, + new object[] { new AuthenticationProperties(), default(IList) }, + new object[] { default(AuthenticationProperties), new List { "TestScheme" } }, + new object[] { default(AuthenticationProperties), default(IList) }, + }; + + [Fact] + public void SignIn_WithNullPrincipal_ThrowsArgNullException() + { + Assert.Throws("principal", () => TypedResults.SignIn(null)); + } + + [Fact] + public void Conflict_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = TypedResults.Conflict(value); + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Conflict_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = TypedResults.Conflict(); + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode); + } + + [Fact] + public void Content_WithContentAndMediaType_ResultHasCorrectValues() + { + // Arrange + var content = "test content"; + var mediaType = MediaTypeHeaderValue.Parse("text/plain"); + + // Act + var result = TypedResults.Content(content, mediaType); + + // Assert + Assert.Null(result.StatusCode); + Assert.Equal(content, result.ResponseContent); + Assert.Equal(mediaType.ToString(), result.ContentType); + } + + [Fact] + public void Content_WithContentAndContentTypeAndEncoding_ResultHasCorrectValues() + { + // Arrange + var content = "test content"; + var contentType = "text/plain"; + var encoding = Encoding.UTF8; + + // Act + var result = TypedResults.Content(content, contentType, null); + + // Assert + Assert.Null(result.StatusCode); + Assert.Equal(content, result.ResponseContent); + Assert.Equal(contentType, result.ContentType); + } + + [Fact] + public void Created_WithStringUriAndValue_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.com/entity"; + var value = new { }; + + // Act + var result = TypedResults.Created(uri, value); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri, result.Location); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Created_WithStringUri_ResultHasCorrectValues() + { + // Arrange + var uri = "https://example.com/entity"; + + // Act + var result = TypedResults.Created(uri); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri, result.Location); + } + + [Fact] + public void Created_WithUriAndValue_ResultHasCorrectValues() + { + // Arrange + var uri = new Uri("https://example.com/entity"); + var value = new { }; + + // Act + var result = TypedResults.Created(uri, value); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri.ToString(), result.Location); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Created_WithUri_ResultHasCorrectValues() + { + // Arrange + var uri = new Uri("https://example.com/entity"); + + // Act + var result = TypedResults.Created(uri); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(uri.ToString(), result.Location); + } + + [Fact] + public void Created_WithNullStringUri_ThrowsArgNullException() + { + Assert.Throws("uri", () => TypedResults.Created(default(string))); + } + + [Fact] + public void Created_WithNullUri_ThrowsArgNullException() + { + Assert.Throws("uri", () => TypedResults.Created(default(Uri))); + } + + [Fact] + public void CreatedAtRoute_WithRouteNameAndRouteValuesAndValue_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + var value = new { }; + + // Act + var result = TypedResults.CreatedAtRoute(value, routeName, routeValues); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + Assert.Equal(value, result.Value); + } + + [Fact] + public void CreatedAtRoute_WithRouteNameAndValue_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var value = new { }; + + // Act + var result = TypedResults.CreatedAtRoute(value, routeName, null); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(), result.RouteValues); + Assert.Equal(value, result.Value); + } + + [Fact] + public void CreatedAtRoute_WithRouteName_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + + // Act + var result = TypedResults.CreatedAtRoute(routeName, default(object)); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(), result.RouteValues); + } + + [Fact] + public void CreatedAtRoute_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = TypedResults.CreatedAtRoute(); + + // Assert + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + Assert.Null(result.RouteName); + Assert.Equal(new RouteValueDictionary(), result.RouteValues); + } + + [Fact] + public void Empty_IsEmptyInstance() + { + // Act + var result = TypedResults.Empty; + + // Assert + Assert.Equal(EmptyHttpResult.Instance, result); + } + + [Fact] + public void Json_WithAllArgs_ResultHasCorrectValues() + { + // Arrange + var data = new { }; + var options = new JsonSerializerOptions(); + var contentType = "application/custom+json"; + var statusCode = StatusCodes.Status208AlreadyReported; + + // Act + var result = TypedResults.Json(data, options, contentType, statusCode); + + // Assert + Assert.Equal(data, result.Value); + Assert.Equal(options, result.JsonSerializerOptions); + Assert.Equal(contentType, result.ContentType); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void Json_WithNoArgs_ResultHasCorrectValues() + { + // Arrange + var data = default(object); + + // Act + var result = TypedResults.Json(data); + + // Assert + Assert.Null(result.Value); + Assert.Null(result.JsonSerializerOptions); + Assert.Null(result.ContentType); + Assert.Null(result.StatusCode); + } + + [Fact] + public void LocalRedirect_WithUrl_ResultHasCorrectValues() + { + // Arrange + var localUrl = "test/path"; + + // Act + var result = TypedResults.LocalRedirect(localUrl); + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.False(result.Permanent); + Assert.False(result.PreserveMethod); + } + + [Fact] + public void LocalRedirect_WithUrlAndPermanentTrue_ResultHasCorrectValues() + { + // Arrange + var localUrl = "test/path"; + var permanent = true; + + // Act + var result = TypedResults.LocalRedirect(localUrl, permanent); + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.True(result.Permanent); + Assert.False(result.PreserveMethod); + } + + [Fact] + public void LocalRedirect_WithUrlAndPermanentTrueAndPreserveTrue_ResultHasCorrectValues() + { + // Arrange + var localUrl = "test/path"; + var permanent = true; + var preserveMethod = true; + + // Act + var result = TypedResults.LocalRedirect(localUrl, permanent, preserveMethod); + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.True(result.Permanent); + Assert.True(result.PreserveMethod); + } + + [Fact] + public void LocalRedirect_WithNonLocalUrlAndPermanentTrueAndPreserveTrue_ResultHasCorrectValues() + { + // Arrange + var localUrl = "https://example.com/non-local-url/example"; + var permanent = true; + var preserveMethod = true; + + // Act + var result = TypedResults.LocalRedirect(localUrl, permanent, preserveMethod); + + // Assert + Assert.Equal(localUrl, result.Url); + Assert.True(result.AcceptLocalUrlOnly); + Assert.True(result.Permanent); + Assert.True(result.PreserveMethod); + } + + [Fact] + public void NoContent_ResultHasCorrectValues() + { + // Act + var result = TypedResults.NoContent(); + + // Assert + Assert.Equal(StatusCodes.Status204NoContent, result.StatusCode); + } + + [Fact] + public void NotFound_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = TypedResults.NotFound(value); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void NotFound_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = TypedResults.NotFound(); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + } + + [Fact] + public void Ok_WithValue_ResultHasCorrectValues() + { + // Arrange + var value = new { }; + + // Act + var result = TypedResults.Ok(value); + + // Assert + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + Assert.Equal(value, result.Value); + } + + [Fact] + public void Ok_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = TypedResults.Ok(); + + // Assert + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + } + + [Fact] + public void Problem_WithArgs_ResultHasCorrectValues() + { + // Arrange + var detail = "test detail"; + var instance = "test instance"; + var statusCode = StatusCodes.Status409Conflict; + var title = "test title"; + var type = "test type"; + var extensions = new Dictionary { { "test", "value" } }; + + // Act + var result = TypedResults.Problem(detail, instance, statusCode, title, type, extensions); + + // Assert + Assert.Equal(detail, result.ProblemDetails.Detail); + Assert.Equal(instance, result.ProblemDetails.Instance); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(statusCode, result.StatusCode); + Assert.Equal(title, result.ProblemDetails.Title); + Assert.Equal(type, result.ProblemDetails.Type); + Assert.Equal(extensions, result.ProblemDetails.Extensions); + } + + [Fact] + public void Problem_WithNoArgs_ResultHasCorrectValues() + { + /// Act + var result = TypedResults.Problem(); + + // Assert + Assert.Null(result.ProblemDetails.Detail); + Assert.Null(result.ProblemDetails.Instance); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("An error occurred while processing your request.", result.ProblemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", result.ProblemDetails.Type); + Assert.Empty(result.ProblemDetails.Extensions); + } + + [Fact] + public void Problem_WithProblemArg_ResultHasCorrectValues() + { + // Arrange + var problem = new ProblemDetails { Title = "Test title" }; + + // Act + var result = TypedResults.Problem(problem); + + // Assert + Assert.Equal(problem, result.ProblemDetails); + Assert.Equal("Test title", result.ProblemDetails.Title); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + } + + [Fact] + public void Problem_WithValidationProblemArg_ResultHasCorrectValues() + { + // Arrange + var problem = new HttpValidationProblemDetails { Title = "Test title" }; + + // Act + var result = TypedResults.Problem(problem); + + // Assert + Assert.Equal(problem, result.ProblemDetails); + Assert.Equal("Test title", result.ProblemDetails.Title); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + } + + [Fact] + public void ValidationProblem_WithValidationProblemArg_ResultHasCorrectValues() + { + // Arrange + var errors = new Dictionary() { { "testField", new[] { "test error" } } }; + var detail = "test detail"; + var instance = "test instance"; + var title = "test title"; + var type = "test type"; + var extensions = new Dictionary() { { "testExtension", "test value" } }; + + // Act + var result = TypedResults.ValidationProblem(errors, detail, instance, title, type, extensions); + + // Assert + Assert.Equal(errors, result.ProblemDetails.Errors); + Assert.Equal(detail, result.ProblemDetails.Detail); + Assert.Equal(instance, result.ProblemDetails.Instance); + Assert.Equal(StatusCodes.Status400BadRequest, result.ProblemDetails.Status); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal(title, result.ProblemDetails.Title); + Assert.Equal(type, result.ProblemDetails.Type); + Assert.Equal("application/problem+json", result.ContentType); + Assert.Equal(extensions, result.ProblemDetails.Extensions); + } + + [Fact] + public void Redirect_WithDefaults_ResultHasCorrectValues() + { + // Arrange + var url = "https://example.com"; + + // Act + var result = TypedResults.Redirect(url); + + // Assert + Assert.Equal(url, result.Url); + Assert.False(result.PreserveMethod); + Assert.False(result.Permanent); + Assert.False(result.AcceptLocalUrlOnly); + } + + [Fact] + public void Redirect_WithPermanentTrue_ResultHasCorrectValues() + { + // Arrange + var url = "https://example.com"; + + // Act + var result = TypedResults.Redirect(url, true); + + // Assert + Assert.Equal(url, result.Url); + Assert.False(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.False(result.AcceptLocalUrlOnly); + } + + [Fact] + public void Redirect_WithPreserveMethodTrue_ResultHasCorrectValues() + { + // Arrange + var url = "https://example.com"; + + // Act + var result = TypedResults.Redirect(url, false, true); + + // Assert + Assert.Equal(url, result.Url); + Assert.True(result.PreserveMethod); + Assert.False(result.Permanent); + Assert.False(result.AcceptLocalUrlOnly); + } + + [Fact] + public void RedirectToRoute_WithRouteNameAndRouteValuesAndFragment_ResultHasCorrectValues() + { + // Arrange + var routeName = "routeName"; + var routeValues = new { foo = 123 }; + var fragment = "test"; + + // Act + var result = TypedResults.RedirectToRoute(routeName, routeValues, true, true, fragment); + + // Assert + Assert.Equal(routeName, result.RouteName); + Assert.Equal(new RouteValueDictionary(routeValues), result.RouteValues); + Assert.True(result.Permanent); + Assert.True(result.PreserveMethod); + Assert.Equal(fragment, result.Fragment); + } + + [Fact] + public void RedirectToRoute_WithNoArgs_ResultHasCorrectValues() + { + // Act + var result = TypedResults.RedirectToRoute(); + + // Assert + Assert.Null(result.RouteName); + Assert.Null(result.RouteValues); + } + + [Fact] + public void StatusCode_ResultHasCorrectValues() + { + // Arrange + var statusCode = StatusCodes.Status412PreconditionFailed; + + // Act + var result = TypedResults.StatusCode(statusCode); + + // Assert + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void Text_WithContentAndContentTypeAndEncoding_ResultHasCorrectValues() + { + // Arrange + var content = "test content"; + var contentType = "text/plain"; + var encoding = Encoding.ASCII; + + // Act + var result = TypedResults.Text(content, contentType, encoding); + + // Assert + Assert.Null(result.StatusCode); + Assert.Equal(content, result.ResponseContent); + var expectedMediaType = MediaTypeHeaderValue.Parse(contentType); + expectedMediaType.Encoding = encoding; + Assert.Equal(expectedMediaType.ToString(), result.ContentType); + } + + [Fact] + public void Unauthorized_ResultHasCorrectValues() + { + // Act + var result = TypedResults.Unauthorized(); + + // Assert + Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); + } + + [Fact] + public void UnprocessableEntity_ResultHasCorrectValues() + { + // Act + var result = TypedResults.UnprocessableEntity(); + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); + } +} diff --git a/src/Http/Http.Results/test/UnauthorizedResultTests.cs b/src/Http/Http.Results/test/UnauthorizedResultTests.cs index 50aab7fd7bf9..9f25870fea87 100644 --- a/src/Http/Http.Results/test/UnauthorizedResultTests.cs +++ b/src/Http/Http.Results/test/UnauthorizedResultTests.cs @@ -1,7 +1,7 @@ // 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.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs similarity index 60% rename from src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs rename to src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs index f04410925e29..d5d3938d85a2 100644 --- a/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs +++ b/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs @@ -1,21 +1,23 @@ // 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.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; +using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -public class UnprocessableEntityObjectResultTests +public class UnprocessableEntityOfTResultTests { [Fact] public void NotFoundObjectResult_ProblemDetails_SetsStatusCodeAndValue() { // Arrange & Act var obj = new HttpValidationProblemDetails(); - var result = new UnprocessableEntityObjectHttpResult(obj); + var result = new UnprocessableEntity(obj); // Assert Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); @@ -28,7 +30,7 @@ public void UnprocessableEntityObjectResult_SetsStatusCodeAndValue() { // Arrange & Act var obj = new object(); - var result = new UnprocessableEntityObjectHttpResult(obj); + var result = new UnprocessableEntity(obj); // Assert Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); @@ -39,7 +41,7 @@ public void UnprocessableEntityObjectResult_SetsStatusCodeAndValue() public async Task UnprocessableEntityObjectResult_ExecuteAsync_SetsStatusCode() { // Arrange - var result = new UnprocessableEntityObjectHttpResult("Hello"); + var result = new UnprocessableEntity("Hello"); var httpContext = new DefaultHttpContext() { RequestServices = CreateServices(), @@ -56,7 +58,7 @@ public async Task UnprocessableEntityObjectResult_ExecuteAsync_SetsStatusCode() public async Task UnprocessableEntityObjectResult_ExecuteResultAsync_FormatsData() { // Arrange - var result = new UnprocessableEntityObjectHttpResult("Hello"); + var result = new UnprocessableEntity("Hello"); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -74,6 +76,29 @@ public async Task UnprocessableEntityObjectResult_ExecuteResultAsync_FormatsData Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); } + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + UnprocessableEntity MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata>(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status422UnprocessableEntity, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private record Todo(int Id, string Title); + private static IServiceProvider CreateServices() { var services = new ServiceCollection(); diff --git a/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs new file mode 100644 index 000000000000..049d3bfab56f --- /dev/null +++ b/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs @@ -0,0 +1,67 @@ +// 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.HttpResults; + +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +public class UnprocessableEntityResultTests +{ + [Fact] + public void UnprocessableEntityObjectResult_SetsStatusCode() + { + // Arrange & Act + var result = new UnprocessableEntity(); + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); + } + + [Fact] + public async Task UnprocessableEntityObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new UnprocessableEntity(); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, httpContext.Response.StatusCode); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + UnprocessableEntity MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status422UnprocessableEntity, producesResponseTypeMetadata.StatusCode); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/ValidationProblemResultTests.cs b/src/Http/Http.Results/test/ValidationProblemResultTests.cs new file mode 100644 index 000000000000..e6b9455d43ef --- /dev/null +++ b/src/Http/Http.Results/test/ValidationProblemResultTests.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text.Json; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Http.HttpResults; + +public class ValidationProblemResultTests +{ + [Fact] + public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() + { + // Arrange + var details = new HttpValidationProblemDetails(); + var result = new ValidationProblem(details); + var stream = new MemoryStream(); + var httpContext = new DefaultHttpContext + { + RequestServices = CreateServices(), + Response = + { + Body = stream, + }, + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); + Assert.Equal(details, result.ProblemDetails); + stream.Position = 0; + var responseDetails = JsonSerializer.Deserialize(stream); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", responseDetails.Type); + Assert.Equal("One or more validation errors occurred.", responseDetails.Title); + Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status); + } + + [Fact] + public void ExecuteAsync_ThrowsArgumentNullException_ForNullProblemDetails() + { + Assert.Throws("problemDetails", () => new ValidationProblem(null)); + } + + [Fact] + public void ExecuteAsync_ThrowsArgumentException_ForNon400StatusCodeFromProblemDetails() + { + Assert.Throws("problemDetails", () => new ValidationProblem( + new HttpValidationProblemDetails { Status = StatusCodes.Status413RequestEntityTooLarge, })); + } + + [Fact] + public void PopulateMetadata_AddsResponseTypeMetadata() + { + // Arrange + ValidationProblem MyApi() { throw new NotImplementedException(); } + var metadata = new List(); + var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, null); + + // Act + PopulateMetadata(context); + + // Assert + var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + Assert.Equal(StatusCodes.Status400BadRequest, producesResponseTypeMetadata.StatusCode); + Assert.Equal(typeof(HttpValidationProblemDetails), producesResponseTypeMetadata.Type); + Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/problem+json"); + } + + private static void PopulateMetadata(EndpointMetadataContext context) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddTransient(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddSingleton(NullLoggerFactory.Instance); + + return services.BuildServiceProvider(); + } +} diff --git a/src/Http/Http.Results/test/VirtualFileResultTest.cs b/src/Http/Http.Results/test/VirtualFileResultTests.cs similarity index 86% rename from src/Http/Http.Results/test/VirtualFileResultTest.cs rename to src/Http/Http.Results/test/VirtualFileResultTests.cs index d910676c5365..04ad231a47db 100644 --- a/src/Http/Http.Results/test/VirtualFileResultTest.cs +++ b/src/Http/Http.Results/test/VirtualFileResultTests.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http.HttpResults; -public class VirtualFileResultTest : VirtualFileResultTestBase +public class VirtualFileResultTests : VirtualFileResultTestBase { protected override Task ExecuteAsync(HttpContext httpContext, string path, string contentType, DateTimeOffset? lastModified = null, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false) { diff --git a/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs b/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs index 44fb25c7009a..033e1f00e4de 100644 --- a/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs +++ b/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs @@ -64,7 +64,7 @@ static void GenerateClassFile(string classFilePath, int typeArgCount, bool inter writer.WriteLine(); // Namespace - writer.WriteLine("namespace Microsoft.AspNetCore.Http;"); + writer.WriteLine("namespace Microsoft.AspNetCore.Http.HttpResults;"); writer.WriteLine(); // Skip 1 as we don't have a Results class @@ -231,6 +231,7 @@ static void GenerateTestFiles(string testFilePath, int typeArgCount, bool intera writer.WriteLine("using System.Reflection;"); writer.WriteLine("using System.Threading.Tasks;"); writer.WriteLine("using Microsoft.AspNetCore.Http.Metadata;"); + writer.WriteLine("using Microsoft.AspNetCore.Http.HttpResults;"); writer.WriteLine("using Microsoft.Extensions.DependencyInjection;"); writer.WriteLine("using Microsoft.Extensions.Logging;"); writer.WriteLine("using Microsoft.Extensions.Logging.Abstractions;"); @@ -388,13 +389,13 @@ static void GenerateTest_ExecuteResult_ExecutesAssignedResult(StreamWriter write //public async Task ResultsOfTResult1TResult2_ExecuteResult_ExecutesAssignedResult(int input, object expected) //{ // // Arrange - // Results MyApi(int checksum) + // Results MyApi(int checksum) // { // return checksum switch // { // 1 => new ChecksumResult1(checksum), // 2 => new ChecksumResult2(checksum), - // _ => (NoContentHttpResult)Results.NoContent() + // _ => (NoContent)Results.NoContent() // }; // } // var httpContext = GetHttpContext(); @@ -478,7 +479,7 @@ static void GenerateTest_Throws_ArgumentNullException_WhenHttpContextIsNull(Stre //public void ResultsOfTResult1TResult2_Throws_ArgumentNullException_WhenHttpContextIsNull() //{ // // Arrange - // Results MyApi() + // Results MyApi() // { // return new ChecksumResult1(1); // } @@ -507,7 +508,7 @@ static void GenerateTest_Throws_ArgumentNullException_WhenHttpContextIsNull(Stre // Arrange writer.WriteIndentedLine(2, "// Arrange"); - writer.WriteIndentedLine(2, "Results MyApi()"); + writer.WriteIndentedLine(2, "Results MyApi()"); writer.WriteIndentedLine(2, "{"); writer.WriteIndentedLine(3, "return new ChecksumResult1(1);"); writer.WriteIndentedLine(2, "}"); @@ -535,7 +536,7 @@ static void GenerateTest_Throws_InvalidOperationException_WhenResultIsNull(Strea //public void ResultsOfTResult1TResult2_Throws_InvalidOperationException_WhenResultIsNull() //{ // // Arrange - // Results MyApi() + // Results MyApi() // { // return (ChecksumResult1)null; // } @@ -564,7 +565,7 @@ static void GenerateTest_Throws_InvalidOperationException_WhenResultIsNull(Strea // Arrange writer.WriteIndentedLine(2, "// Arrange"); - writer.WriteIndentedLine(2, "Results MyApi()"); + writer.WriteIndentedLine(2, "Results MyApi()"); writer.WriteIndentedLine(2, "{"); writer.WriteIndentedLine(3, "return new ChecksumResult1(1);"); writer.WriteIndentedLine(2, "}"); diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs index 1b9b249a1d57..f2f50d8c274c 100644 --- a/src/Http/samples/MinimalSample/Program.cs +++ b/src/Http/samples/MinimalSample/Program.cs @@ -1,9 +1,12 @@ // 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.HttpResults; using Microsoft.AspNetCore.Mvc; -var app = WebApplication.Create(args); +var builder = WebApplication.CreateBuilder(args); + +var app = builder.Build(); if (app.Environment.IsDevelopment()) { @@ -21,14 +24,12 @@ app.MapGet("/null-result", IResult () => null); -app.MapGet("/todo/{id}", Results (int id) => -{ - return id switch +app.MapGet("/todo/{id}", Results, NotFound, BadRequest> (int id) => id switch { - >= 1 and <= 10 => (OkObjectHttpResult)Results.Ok(new { Id = id, Title = "Walk the dog" }), - _ => (NotFoundObjectHttpResult)Results.NotFound() - }; -}); + <= 0 => TypedResults.BadRequest(), + >= 1 and <= 10 => TypedResults.Ok(new Todo(id, "Walk the dog")), + _ => TypedResults.NotFound() + }); var extensions = new Dictionary() { { "traceId", "traceId123" } }; @@ -38,7 +39,7 @@ app.MapGet("/problem-object", () => Results.Problem(new ProblemDetails() { Status = 500, Extensions = { { "traceId", "traceId123" } } })); -var errors = new Dictionary(); +var errors = new Dictionary() { { "Title", new[] { "The Title field is required." } } }; app.MapGet("/validation-problem", () => Results.ValidationProblem(errors, statusCode: 400, extensions: extensions)); @@ -46,4 +47,9 @@ app.MapGet("/validation-problem-object", () => Results.Problem(new HttpValidationProblemDetails(errors) { Status = 400, Extensions = { { "traceId", "traceId123" } } })); +app.MapGet("/validation-problem-typed", () => + TypedResults.ValidationProblem(errors, extensions: extensions)); + app.Run(); + +internal record Todo(int Id, string Title);