diff --git a/src/Http/Http.Results/src/AcceptedAtRouteResult.cs b/src/Http/Http.Results/src/AcceptedAtRouteHttpResult.cs similarity index 53% rename from src/Http/Http.Results/src/AcceptedAtRouteResult.cs rename to src/Http/Http.Results/src/AcceptedAtRouteHttpResult.cs index 1958f7d402fd..0e45e84164ce 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteResult.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteHttpResult.cs @@ -1,41 +1,54 @@ // 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.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result; - -internal sealed class AcceptedAtRouteResult : ObjectResult +/// +/// 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 { /// - /// 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. - public AcceptedAtRouteResult(object? routeValues, object? value) + internal AcceptedAtRouteHttpResult(object? routeValues, object? 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. - public AcceptedAtRouteResult( + internal AcceptedAtRouteHttpResult( string? routeName, object? routeValues, object? value) - : base(value, StatusCodes.Status202Accepted) { + Value = value; RouteName = routeName; RouteValues = new RouteValueDictionary(routeValues); + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); } + /// + /// Gets the object result. + /// + public object? Value { get; } + /// /// Gets the name of the route to use for generating the URL. /// @@ -46,12 +59,17 @@ public AcceptedAtRouteResult( /// public RouteValueDictionary RouteValues { get; } - /// - protected override void ConfigureResponseHeaders(HttpContext context) + /// + /// Gets the HTTP status code. + /// + public int StatusCode => StatusCodes.Status202Accepted; + + /// + public Task ExecuteAsync(HttpContext httpContext) { - var linkGenerator = context.RequestServices.GetRequiredService(); + var linkGenerator = httpContext.RequestServices.GetRequiredService(); var url = linkGenerator.GetUriByAddress( - context, + httpContext, RouteName, RouteValues, fragment: FragmentString.Empty); @@ -61,6 +79,11 @@ protected override void ConfigureResponseHeaders(HttpContext context) throw new InvalidOperationException("No route matches the supplied values."); } - context.Response.Headers.Location = url; + // 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; + return HttpResultsHelper.WriteResultAsJsonAsync(httpContext, logger, Value, StatusCode); } } diff --git a/src/Http/Http.Results/src/AcceptedHttpResult.cs b/src/Http/Http.Results/src/AcceptedHttpResult.cs new file mode 100644 index 000000000000..d0b6026e0c56 --- /dev/null +++ b/src/Http/Http.Results/src/AcceptedHttpResult.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. + +namespace Microsoft.AspNetCore.Http; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +/// +/// 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 +{ + /// + /// 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) + { + Value = value; + Location = location; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// 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) + { + Value = value; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + + if (locationUri == null) + { + throw new ArgumentNullException(nameof(locationUri)); + } + + if (locationUri.IsAbsoluteUri) + { + Location = locationUri.AbsoluteUri; + } + else + { + Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + } + } + + /// + /// Gets the object result. + /// + public object? Value { get; } + + /// + /// 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) + { + if (!string.IsNullOrEmpty(Location)) + { + httpContext.Response.Headers.Location = Location; + } + + // 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"); + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/AcceptedResult.cs b/src/Http/Http.Results/src/AcceptedResult.cs deleted file mode 100644 index 0f836f4db67b..000000000000 --- a/src/Http/Http.Results/src/AcceptedResult.cs +++ /dev/null @@ -1,66 +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.Result; - -internal sealed class AcceptedResult : ObjectResult -{ - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - public AcceptedResult() - : base(value: null, StatusCodes.Status202Accepted) - { - } - - /// - /// 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. - public AcceptedResult(string? location, object? value) - : base(value, StatusCodes.Status202Accepted) - { - 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. - /// The value to format in the entity body. - public AcceptedResult(Uri locationUri, object? value) - : base(value, StatusCodes.Status202Accepted) - { - if (locationUri == null) - { - throw new ArgumentNullException(nameof(locationUri)); - } - - if (locationUri.IsAbsoluteUri) - { - Location = locationUri.AbsoluteUri; - } - else - { - Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); - } - } - - /// - /// Gets or sets the location at which the status of the requested content can be monitored. - /// - public string? Location { get; set; } - - /// - protected override void ConfigureResponseHeaders(HttpContext context) - { - if (!string.IsNullOrEmpty(Location)) - { - context.Response.Headers.Location = Location; - } - } -} diff --git a/src/Http/Http.Results/src/BadRequestObjectHttpResult.cs b/src/Http/Http.Results/src/BadRequestObjectHttpResult.cs new file mode 100644 index 000000000000..dc9ad6e4fb2f --- /dev/null +++ b/src/Http/Http.Results/src/BadRequestObjectHttpResult.cs @@ -0,0 +1,49 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with Bad Request (400) status code. +/// +public sealed class BadRequestObjectHttpResult : IResult +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The error content to format in the entity body. + internal BadRequestObjectHttpResult(object? error) + { + Value = error; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// Gets the object result. + /// + public object? Value { get; internal init; } + + /// + /// 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"); + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger: logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/BadRequestObjectResult.cs b/src/Http/Http.Results/src/BadRequestObjectResult.cs deleted file mode 100644 index 7f58b7c9d6bc..000000000000 --- a/src/Http/Http.Results/src/BadRequestObjectResult.cs +++ /dev/null @@ -1,12 +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.Result; - -internal sealed class BadRequestObjectResult : ObjectResult -{ - public BadRequestObjectResult(object? error) - : base(error, StatusCodes.Status400BadRequest) - { - } -} diff --git a/src/Http/Http.Results/src/ChallengeResult.cs b/src/Http/Http.Results/src/ChallengeHttpResult.cs similarity index 62% rename from src/Http/Http.Results/src/ChallengeResult.cs rename to src/Http/Http.Results/src/ChallengeHttpResult.cs index a0aa35ae28fe..cff6c06937dd 100644 --- a/src/Http/Http.Results/src/ChallengeResult.cs +++ b/src/Http/Http.Results/src/ChallengeHttpResult.cs @@ -6,84 +6,93 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http; /// /// An that on execution invokes . /// -internal sealed partial class ChallengeResult : IResult +public sealed partial class ChallengeHttpResult : IResult { /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// - public ChallengeResult() + internal ChallengeHttpResult() : this(Array.Empty()) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication scheme. /// /// The authentication scheme to challenge. - public ChallengeResult(string authenticationScheme) + internal ChallengeHttpResult(string authenticationScheme) : this(new[] { authenticationScheme }) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication schemes. /// /// The authentication schemes to challenge. - public ChallengeResult(IList authenticationSchemes) + internal ChallengeHttpResult(IList authenticationSchemes) : this(authenticationSchemes, properties: null) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified . /// /// used to perform the authentication /// challenge. - public ChallengeResult(AuthenticationProperties? properties) + internal ChallengeHttpResult(AuthenticationProperties? properties) : this(Array.Empty(), properties) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication scheme and . /// /// The authentication schemes to challenge. /// used to perform the authentication /// challenge. - public ChallengeResult(string authenticationScheme, AuthenticationProperties? properties) + internal ChallengeHttpResult(string authenticationScheme, AuthenticationProperties? properties) : this(new[] { authenticationScheme }, properties) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication schemes and . /// /// The authentication scheme to challenge. /// used to perform the authentication /// challenge. - public ChallengeResult(IList authenticationSchemes, AuthenticationProperties? properties) + internal ChallengeHttpResult(IList authenticationSchemes, AuthenticationProperties? properties) { - AuthenticationSchemes = authenticationSchemes; + AuthenticationSchemes = authenticationSchemes.AsReadOnly(); Properties = properties; } - public IList AuthenticationSchemes { get; init; } = Array.Empty(); + /// + /// Gets the authentication schemes that are challenged. + /// + public IReadOnlyList AuthenticationSchemes { get; internal init; } = Array.Empty(); - public AuthenticationProperties? Properties { get; init; } + /// + /// Gets the used to perform the sign-out operation. + /// + public AuthenticationProperties? Properties { get; internal init; } + /// public async Task ExecuteAsync(HttpContext httpContext) { - var logger = httpContext.RequestServices.GetRequiredService>(); + // 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.ChallengeResult"); Log.ChallengeResultExecuting(logger, AuthenticationSchemes); @@ -102,7 +111,7 @@ public async Task ExecuteAsync(HttpContext httpContext) private static partial class Log { - public static void ChallengeResultExecuting(ILogger logger, IList authenticationSchemes) + public static void ChallengeResultExecuting(ILogger logger, IReadOnlyList authenticationSchemes) { if (logger.IsEnabled(LogLevel.Information)) { diff --git a/src/Http/Http.Results/src/ConflictObjectHttpResult.cs b/src/Http/Http.Results/src/ConflictObjectHttpResult.cs new file mode 100644 index 000000000000..782775395bfc --- /dev/null +++ b/src/Http/Http.Results/src/ConflictObjectHttpResult.cs @@ -0,0 +1,49 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with Conflict (409) status code. +/// +public sealed class ConflictObjectHttpResult : IResult +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The error content to format in the entity body. + internal ConflictObjectHttpResult(object? error) + { + Value = error; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// Gets the object result. + /// + public object? Value { get; internal init; } + + /// + /// 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"); + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger: logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/ConflictObjectResult.cs b/src/Http/Http.Results/src/ConflictObjectResult.cs deleted file mode 100644 index 68308b14d2eb..000000000000 --- a/src/Http/Http.Results/src/ConflictObjectResult.cs +++ /dev/null @@ -1,12 +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.Result; - -internal sealed class ConflictObjectResult : ObjectResult -{ - public ConflictObjectResult(object? error) : - base(error, StatusCodes.Status409Conflict) - { - } -} diff --git a/src/Http/Http.Results/src/ContentHttpResult.cs b/src/Http/Http.Results/src/ContentHttpResult.cs new file mode 100644 index 000000000000..a50675408e2d --- /dev/null +++ b/src/Http/Http.Results/src/ContentHttpResult.cs @@ -0,0 +1,66 @@ +// 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; + +/// +/// An that when executed +/// will produce a response with content. +/// +public sealed partial class ContentHttpResult : IResult +{ + /// + /// Initializes a new instance of the class with the values. + /// + /// The value to format in the entity body. + /// The Content-Type header for the response + internal ContentHttpResult(string? content, string? contentType) + : this(content, contentType, statusCode: null) + { + } + + /// + /// 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 Content-Type header for the response + internal ContentHttpResult(string? content, string? contentType, int? statusCode) + { + Content = content; + StatusCode = statusCode; + ContentType = contentType; + } + + /// + /// Gets or set the content representing the body of the response. + /// + public string? Content { get; internal init; } + + /// + /// Gets or sets the Content-Type header for the response. + /// + public string? ContentType { get; internal init; } + + /// + /// Gets the HTTP status code. + /// + public int? StatusCode { get; internal init; } + + /// + /// Writes the content to the HTTP response. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + 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.ContentResult"); + + return HttpResultsHelper.WriteResultAsContentAsync(httpContext, logger, Content, StatusCode, ContentType); + } +} diff --git a/src/Http/Http.Results/src/ContentResult.cs b/src/Http/Http.Results/src/ContentResult.cs deleted file mode 100644 index 4630b5945633..000000000000 --- a/src/Http/Http.Results/src/ContentResult.cs +++ /dev/null @@ -1,73 +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.AspNetCore.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Http.Result; - -internal sealed partial class ContentResult : IResult -{ - private const string DefaultContentType = "text/plain; charset=utf-8"; - private static readonly Encoding DefaultEncoding = Encoding.UTF8; - - /// - /// Gets or set the content representing the body of the response. - /// - public string? Content { get; init; } - - /// - /// Gets or sets the Content-Type header for the response. - /// - public string? ContentType { get; init; } - - /// - /// Gets or sets the HTTP status code. - /// - public int? StatusCode { get; init; } - - /// - /// Writes the content to the HTTP response. - /// - /// The for the current request. - /// A task that represents the asynchronous execute operation. - public async Task ExecuteAsync(HttpContext httpContext) - { - var response = httpContext.Response; - - ResponseContentTypeHelper.ResolveContentTypeAndEncoding( - ContentType, - response.ContentType, - (DefaultContentType, DefaultEncoding), - ResponseContentTypeHelper.GetEncoding, - out var resolvedContentType, - out var resolvedContentTypeEncoding); - - response.ContentType = resolvedContentType; - - if (StatusCode != null) - { - response.StatusCode = StatusCode.Value; - } - - var logger = httpContext.RequestServices.GetRequiredService>(); - - Log.ContentResultExecuting(logger, resolvedContentType); - - if (Content != null) - { - response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content); - await response.WriteAsync(Content, resolvedContentTypeEncoding); - } - } - - private static partial class Log - { - [LoggerMessage(1, LogLevel.Information, - "Executing ContentResult with HTTP Response ContentType of {ContentType}", - EventName = "ContentResultExecuting")] - internal static partial void ContentResultExecuting(ILogger logger, string contentType); - } -} diff --git a/src/Http/Http.Results/src/CreatedAtRouteHttpResult.cs b/src/Http/Http.Results/src/CreatedAtRouteHttpResult.cs new file mode 100644 index 000000000000..f59593be8505 --- /dev/null +++ b/src/Http/Http.Results/src/CreatedAtRouteHttpResult.cs @@ -0,0 +1,92 @@ +// 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.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +/// +/// 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 +{ + /// + /// 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) + : this(routeName: null, routeValues: routeValues, value: value) + { + } + + /// + /// 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( + string? routeName, + object? routeValues, + object? value) + { + Value = value; + RouteName = routeName; + RouteValues = new RouteValueDictionary(routeValues); + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// Gets the object result. + /// + public object? Value { get; } + + /// + /// 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; + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/CreatedAtRouteResult.cs b/src/Http/Http.Results/src/CreatedAtRouteResult.cs deleted file mode 100644 index 10b2fe9ab776..000000000000 --- a/src/Http/Http.Results/src/CreatedAtRouteResult.cs +++ /dev/null @@ -1,66 +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 Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Http.Result; - -internal sealed class CreatedAtRouteResult : ObjectResult -{ - /// - /// 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. - public CreatedAtRouteResult(object? routeValues, object? value) - : this(routeName: null, routeValues: routeValues, value: value) - { - } - - /// - /// 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. - public CreatedAtRouteResult( - string? routeName, - object? routeValues, - object? value) - : base(value, StatusCodes.Status201Created) - { - RouteName = routeName; - RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); - } - - /// - /// Gets or sets the name of the route to use for generating the URL. - /// - public string? RouteName { get; set; } - - /// - /// Gets or sets the route data to use for generating the URL. - /// - public RouteValueDictionary? RouteValues { get; set; } - - /// - protected override void ConfigureResponseHeaders(HttpContext context) - { - var linkGenerator = context.RequestServices.GetRequiredService(); - var url = linkGenerator.GetUriByRouteValues( - context, - RouteName, - RouteValues, - fragment: FragmentString.Empty); - - if (string.IsNullOrEmpty(url)) - { - throw new InvalidOperationException("No route matches the supplied values."); - } - - context.Response.Headers.Location = url; - } -} diff --git a/src/Http/Http.Results/src/CreatedHttpResult.cs b/src/Http/Http.Results/src/CreatedHttpResult.cs new file mode 100644 index 000000000000..4106cacd7fec --- /dev/null +++ b/src/Http/Http.Results/src/CreatedHttpResult.cs @@ -0,0 +1,84 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with status code Created (201) and Location header. +/// +public sealed class CreatedHttpResult : IResult +{ + /// + /// 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) + { + Value = value; + Location = location; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// 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) + { + Value = value; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + + if (locationUri == null) + { + throw new ArgumentNullException(nameof(locationUri)); + } + + if (locationUri.IsAbsoluteUri) + { + Location = locationUri.AbsoluteUri; + } + else + { + Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + } + } + + /// + /// Gets the object result. + /// + public object? Value { get; } + + /// + /// Gets the HTTP status code. + /// + public int StatusCode => StatusCodes.Status201Created; + + /// + public string? Location { get; } + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + if (!string.IsNullOrEmpty(Location)) + { + httpContext.Response.Headers.Location = Location; + } + + // 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"); + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/CreatedResult.cs b/src/Http/Http.Results/src/CreatedResult.cs deleted file mode 100644 index 78cc9a361bc0..000000000000 --- a/src/Http/Http.Results/src/CreatedResult.cs +++ /dev/null @@ -1,54 +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.Result; - -internal sealed class CreatedResult : ObjectResult -{ - /// - /// 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. - public CreatedResult(string location, object? value) - : base(value, StatusCodes.Status201Created) - { - Location = location; - } - - /// - /// 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. - public CreatedResult(Uri location, object? value) - : base(value, StatusCodes.Status201Created) - { - if (location == null) - { - throw new ArgumentNullException(nameof(location)); - } - - if (location.IsAbsoluteUri) - { - Location = location.AbsoluteUri; - } - else - { - Location = location.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); - } - } - - /// - /// Gets or sets the location at which the content has been created. - /// - public string Location { get; init; } - - /// - protected override void ConfigureResponseHeaders(HttpContext context) - { - context.Response.Headers.Location = Location; - } -} diff --git a/src/Http/Http.Results/src/EmptyResult.cs b/src/Http/Http.Results/src/EmptyHttpResult.cs similarity index 61% rename from src/Http/Http.Results/src/EmptyResult.cs rename to src/Http/Http.Results/src/EmptyHttpResult.cs index 739fa5783a34..9e9b25c166dd 100644 --- a/src/Http/Http.Results/src/EmptyResult.cs +++ b/src/Http/Http.Results/src/EmptyHttpResult.cs @@ -7,14 +7,18 @@ namespace Microsoft.AspNetCore.Http; /// Represents an that when executed will /// do nothing. /// -internal sealed class EmptyResult : IResult +public sealed class EmptyHttpResult : IResult { - internal static readonly EmptyResult Instance = new(); - - private EmptyResult() + private EmptyHttpResult() { } + /// + /// Gets an instance of . + /// + public static EmptyHttpResult Instance { get; } = new(); + + /// public Task ExecuteAsync(HttpContext httpContext) { return Task.CompletedTask; diff --git a/src/Http/Http.Results/src/FileContentHttpResult.cs b/src/Http/Http.Results/src/FileContentHttpResult.cs new file mode 100644 index 000000000000..56cd17d13944 --- /dev/null +++ b/src/Http/Http.Results/src/FileContentHttpResult.cs @@ -0,0 +1,127 @@ +// 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.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents an that when executed will +/// write a file from the content to the response. +/// +public sealed partial class FileContentHttpResult : IResult +{ + /// + /// Creates a new instance with + /// the provided and the + /// provided . + /// + /// The bytes that represent the file contents. + /// The Content-Type of the file. + internal FileContentHttpResult(ReadOnlyMemory fileContents, string? contentType) + : this(fileContents, contentType, fileDownloadName: null) + { + } + + /// + /// Creates a new instance with + /// the provided , the provided + /// and the provided . + /// + /// The bytes that represent the file contents. + /// The Content-Type header of the response. + /// The suggested file name. + internal FileContentHttpResult( + ReadOnlyMemory fileContents, + string? contentType, + string? fileDownloadName) + : this(fileContents, contentType, fileDownloadName, enableRangeProcessing: false) + { + } + + /// + /// Creates a new instance with the provided values. + /// + /// The bytes that represent 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. + internal FileContentHttpResult( + ReadOnlyMemory fileContents, + string? contentType, + string? fileDownloadName, + bool enableRangeProcessing, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + { + FileContents = fileContents; + FileLength = fileContents.Length; + ContentType = contentType ?? "application/octet-stream"; + FileDownloadName = fileDownloadName; + EnableRangeProcessing = enableRangeProcessing; + LastModified = lastModified; + EntityTag = entityTag; + } + + /// + /// Gets the Content-Type header for the response. + /// + public string ContentType { get; internal set; } + + /// + /// Gets the file name that will be used in the Content-Disposition header of the response. + /// + public string? FileDownloadName { get; internal set; } + + /// + /// Gets the last modified information associated with the file result. + /// + public DateTimeOffset? LastModified { get; internal set; } + + /// + /// Gets the etag associated with the file result. + /// + public EntityTagHeaderValue? EntityTag { get; internal init; } + + /// + /// Gets the value that enables range processing for the file result. + /// + public bool EnableRangeProcessing { get; internal init; } + + /// + /// Gets or sets the file length information . + /// + public long? FileLength { get; internal set; } + + /// + /// Gets or sets the file contents. + /// + public ReadOnlyMemory FileContents { 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.FileContentResult"); + + var (range, rangeLength, completed) = HttpResultsHelper.WriteResultAsFileCore( + httpContext, + logger, + FileDownloadName, + FileLength, + ContentType, + EnableRangeProcessing, + LastModified, + EntityTag); + + return completed ? + Task.CompletedTask : + FileResultHelper.WriteFileAsync(httpContext, FileContents, range, rangeLength); + } +} diff --git a/src/Http/Http.Results/src/FileContentResult.cs b/src/Http/Http.Results/src/FileContentResult.cs deleted file mode 100644 index f4a81e92a3c4..000000000000 --- a/src/Http/Http.Results/src/FileContentResult.cs +++ /dev/null @@ -1,41 +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 Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http.Result; - -internal sealed partial class FileContentResult : FileResult -{ - /// - /// Creates a new instance with - /// the provided and the - /// provided . - /// - /// The bytes that represent the file contents. - /// The Content-Type header of the response. - public FileContentResult(ReadOnlyMemory fileContents, string? contentType) - : base(contentType) - { - FileContents = fileContents; - FileLength = fileContents.Length; - } - - /// - /// Gets or sets the file contents. - /// - public ReadOnlyMemory FileContents { get; init; } - - protected override ILogger GetLogger(HttpContext httpContext) - { - return httpContext.RequestServices.GetRequiredService>(); - } - - protected override Task ExecuteCoreAsync(HttpContext httpContext, RangeItemHeaderValue? range, long rangeLength) - { - return FileResultHelper.WriteFileAsync(httpContext, FileContents, range, rangeLength); - } -} diff --git a/src/Http/Http.Results/src/FileResult.cs b/src/Http/Http.Results/src/FileResult.cs deleted file mode 100644 index a65bd86c82d3..000000000000 --- a/src/Http/Http.Results/src/FileResult.cs +++ /dev/null @@ -1,62 +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 Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http.Result; - -internal abstract class FileResult : FileResultBase, IResult -{ - public FileResult(string? contentType) - : base(contentType) - { - } - - protected abstract ILogger GetLogger(HttpContext httpContext); - - protected abstract Task ExecuteCoreAsync(HttpContext httpContext, RangeItemHeaderValue? range, long rangeLength); - - public virtual Task ExecuteAsync(HttpContext httpContext) - { - var logger = GetLogger(httpContext); - - Log.ExecutingFileResult(logger, this); - - var fileResultInfo = new FileResultInfo - { - ContentType = ContentType, - EnableRangeProcessing = EnableRangeProcessing, - EntityTag = EntityTag, - FileDownloadName = FileDownloadName, - LastModified = LastModified, - }; - - var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( - httpContext, - fileResultInfo, - FileLength, - EnableRangeProcessing, - LastModified, - EntityTag, - logger); - - if (!serveBody) - { - return Task.CompletedTask; - } - - if (range != null && rangeLength == 0) - { - return Task.CompletedTask; - } - - if (range != null) - { - FileResultHelper.Log.WritingRangeToBody(logger); - } - - return ExecuteCoreAsync(httpContext, range, rangeLength); - } -} diff --git a/src/Http/Http.Results/src/FileResultBase.cs b/src/Http/Http.Results/src/FileResultBase.cs deleted file mode 100644 index a5cdeb88f9de..000000000000 --- a/src/Http/Http.Results/src/FileResultBase.cs +++ /dev/null @@ -1,88 +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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http.Result; - -internal abstract partial class FileResultBase -{ - private string? _fileDownloadName; - - /// - /// Creates a new instance with - /// the provided . - /// - /// The Content-Type header of the response. - protected FileResultBase(string? contentType) - { - ContentType = contentType ?? "application/octet-stream"; - } - - /// - /// Gets the Content-Type header for the response. - /// - public string ContentType { get; } - - /// - /// Gets the file name that will be used in the Content-Disposition header of the response. - /// - [AllowNull] - public string FileDownloadName - { - get { return _fileDownloadName ?? string.Empty; } - init { _fileDownloadName = value; } - } - - /// - /// Gets or sets the last modified information associated with the . - /// - public DateTimeOffset? LastModified { get; set; } - - /// - /// Gets or sets the etag associated with the . - /// - public EntityTagHeaderValue? EntityTag { get; init; } - - /// - /// Gets or sets the value that enables range processing for the . - /// - public bool EnableRangeProcessing { get; init; } - - public long? FileLength { get; set; } - - protected static partial class Log - { - public static void ExecutingFileResult(ILogger logger, FileResultBase fileResult) - { - if (logger.IsEnabled(LogLevel.Information)) - { - var fileResultType = fileResult.GetType().Name; - ExecutingFileResultWithNoFileName(logger, fileResultType, fileResult.FileDownloadName); - } - } - - public static void ExecutingFileResult(ILogger logger, FileResultBase fileResult, string fileName) - { - if (logger.IsEnabled(LogLevel.Information)) - { - var fileResultType = fileResult.GetType().Name; - ExecutingFileResult(logger, fileResultType, fileName, fileResult.FileDownloadName); - } - } - - [LoggerMessage(1, LogLevel.Information, - "Executing {FileResultType}, sending file with download name '{FileDownloadName}'.", - EventName = "ExecutingFileResultWithNoFileName", - SkipEnabledCheck = true)] - private static partial void ExecutingFileResultWithNoFileName(ILogger logger, string fileResultType, string fileDownloadName); - - [LoggerMessage(2, LogLevel.Information, - "Executing {FileResultType}, sending file '{FileDownloadPath}' with download name '{FileDownloadName}'.", - EventName = "ExecutingFileResult", - SkipEnabledCheck = true)] - private static partial void ExecutingFileResult(ILogger logger, string fileResultType, string fileDownloadPath, string fileDownloadName); - } -} diff --git a/src/Http/Http.Results/src/FileStreamHttpResult.cs b/src/Http/Http.Results/src/FileStreamHttpResult.cs new file mode 100644 index 000000000000..4a2273c535f0 --- /dev/null +++ b/src/Http/Http.Results/src/FileStreamHttpResult.cs @@ -0,0 +1,140 @@ +// 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.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents an that when executed will +/// write a file from a stream to the response. +/// +public sealed class FileStreamHttpResult : IResult +{ + /// + /// Creates a new instance with + /// the provided and the + /// provided . + /// + /// The stream with the file. + /// The Content-Type of the file. + internal FileStreamHttpResult(Stream fileStream, string? contentType) + : this(fileStream, contentType, fileDownloadName: null) + { + } + + /// + /// Creates a new instance with + /// the provided , the provided + /// and the provided . + /// + /// The stream with the file. + /// The Content-Type header of the response. + /// The suggested file name. + internal FileStreamHttpResult( + Stream fileStream, + string? contentType, + string? fileDownloadName) + : this(fileStream, contentType, fileDownloadName, enableRangeProcessing: false) + { + } + + /// + /// Creates a new instance with the provided values. + /// + /// The stream 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. + internal FileStreamHttpResult( + Stream fileStream, + string? contentType, + string? fileDownloadName, + bool enableRangeProcessing, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + { + if (fileStream == null) + { + throw new ArgumentNullException(nameof(fileStream)); + } + + FileStream = fileStream; + if (fileStream.CanSeek) + { + FileLength = fileStream.Length; + } + + ContentType = contentType ?? "application/octet-stream"; + FileDownloadName = fileDownloadName; + EnableRangeProcessing = enableRangeProcessing; + LastModified = lastModified; + EntityTag = entityTag; + } + + /// + /// Gets the Content-Type header for the response. + /// + public string ContentType { get; internal set; } + + /// + /// Gets the file name that will be used in the Content-Disposition header of the response. + /// + public string? FileDownloadName { get; internal set; } + + /// + /// Gets the last modified information associated with the file result. + /// + public DateTimeOffset? LastModified { get; internal set; } + + /// + /// Gets the etag associated with the file result. + /// + public EntityTagHeaderValue? EntityTag { get; internal init; } + + /// + /// Gets the value that enables range processing for the file result. + /// + public bool EnableRangeProcessing { get; internal init; } + + /// + /// Gets or sets the file length information . + /// + public long? FileLength { get; internal set; } + + /// + /// Gets or sets the stream with the file that will be sent back as the response. + /// + public Stream FileStream { get; } + + /// + public async 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.FileStreamResult"); + + await using (FileStream) + { + var (range, rangeLength, completed) = HttpResultsHelper.WriteResultAsFileCore( + httpContext, + logger, + FileDownloadName, + FileLength, + ContentType, + EnableRangeProcessing, + LastModified, + EntityTag); + + if (!completed) + { + await FileResultHelper.WriteFileAsync(httpContext, FileStream, range, rangeLength); + } + } + } +} diff --git a/src/Http/Http.Results/src/FileStreamResult.cs b/src/Http/Http.Results/src/FileStreamResult.cs deleted file mode 100644 index ce196d18a009..000000000000 --- a/src/Http/Http.Results/src/FileStreamResult.cs +++ /dev/null @@ -1,61 +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 Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http.Result; - -/// -/// Represents an that when executed will -/// write a file from a stream to the response. -/// -internal sealed class FileStreamResult : FileResult, IResult -{ - /// - /// Creates a new instance with - /// the provided and the - /// provided . - /// - /// The stream with the file. - /// The Content-Type header of the response. - public FileStreamResult(Stream fileStream, string? contentType) - : base(contentType) - { - if (fileStream == null) - { - throw new ArgumentNullException(nameof(fileStream)); - } - - FileStream = fileStream; - if (fileStream.CanSeek) - { - FileLength = fileStream.Length; - } - } - - /// - /// Gets or sets the stream with the file that will be sent back as the response. - /// - public Stream FileStream { get; } - - protected override ILogger GetLogger(HttpContext httpContext) - { - return httpContext.RequestServices.GetRequiredService>(); - } - - public override async Task ExecuteAsync(HttpContext httpContext) - { - await using (FileStream) - { - await base.ExecuteAsync(httpContext); - } - } - - protected override Task ExecuteCoreAsync(HttpContext context, RangeItemHeaderValue? range, long rangeLength) - { - return FileResultHelper.WriteFileAsync(context, FileStream, range, rangeLength); - } -} diff --git a/src/Http/Http.Results/src/ForbidResult.cs b/src/Http/Http.Results/src/ForbidHttpResult.cs similarity index 63% rename from src/Http/Http.Results/src/ForbidResult.cs rename to src/Http/Http.Results/src/ForbidHttpResult.cs index fe66fce56ac0..f9d8d9c27897 100644 --- a/src/Http/Http.Results/src/ForbidResult.cs +++ b/src/Http/Http.Results/src/ForbidHttpResult.cs @@ -6,88 +6,93 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http; -internal sealed partial class ForbidResult : IResult +/// +/// An that on execution invokes . +/// +public sealed partial class ForbidHttpResult : IResult { /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// - public ForbidResult() + internal ForbidHttpResult() : this(Array.Empty()) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication scheme. /// /// The authentication scheme to challenge. - public ForbidResult(string authenticationScheme) + internal ForbidHttpResult(string authenticationScheme) : this(new[] { authenticationScheme }) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication schemes. /// /// The authentication schemes to challenge. - public ForbidResult(IList authenticationSchemes) + internal ForbidHttpResult(IList authenticationSchemes) : this(authenticationSchemes, properties: null) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified . /// /// used to perform the authentication /// challenge. - public ForbidResult(AuthenticationProperties? properties) + internal ForbidHttpResult(AuthenticationProperties? properties) : this(Array.Empty(), properties) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication scheme and . /// /// The authentication schemes to challenge. /// used to perform the authentication /// challenge. - public ForbidResult(string authenticationScheme, AuthenticationProperties? properties) + internal ForbidHttpResult(string authenticationScheme, AuthenticationProperties? properties) : this(new[] { authenticationScheme }, properties) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication schemes and . /// /// The authentication scheme to challenge. /// used to perform the authentication /// challenge. - public ForbidResult(IList authenticationSchemes, AuthenticationProperties? properties) + internal ForbidHttpResult(IList authenticationSchemes, AuthenticationProperties? properties) { - AuthenticationSchemes = authenticationSchemes; + AuthenticationSchemes = authenticationSchemes.AsReadOnly(); Properties = properties; } /// - /// Gets or sets the authentication schemes that are challenged. + /// Gets the authentication schemes that are challenged. /// - public IList AuthenticationSchemes { get; init; } + public IReadOnlyList AuthenticationSchemes { get; internal init; } /// - /// Gets or sets the used to perform the authentication challenge. + /// Gets the used to perform the authentication challenge. /// - public AuthenticationProperties? Properties { get; init; } + public AuthenticationProperties? Properties { get; internal init; } /// public async Task ExecuteAsync(HttpContext httpContext) { - var logger = httpContext.RequestServices.GetRequiredService>(); + // 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.ForbidResult"); Log.ForbidResultExecuting(logger, AuthenticationSchemes); @@ -106,7 +111,7 @@ public async Task ExecuteAsync(HttpContext httpContext) private static partial class Log { - public static void ForbidResultExecuting(ILogger logger, IList authenticationSchemes) + public static void ForbidResultExecuting(ILogger logger, IReadOnlyList authenticationSchemes) { if (logger.IsEnabled(LogLevel.Information)) { diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs new file mode 100644 index 000000000000..f447e1d9539e --- /dev/null +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -0,0 +1,215 @@ +// 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 System.Text.Json; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +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( + HttpContext httpContext, + ILogger logger, + object? value, + int? statusCode, + 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; + } + + return httpContext.Response.WriteAsJsonAsync( + value, + value.GetType(), + options: jsonSerializerOptions, + contentType: contentType); + } + + public static Task WriteResultAsContentAsync( + HttpContext httpContext, + ILogger logger, + string? content, + int? statusCode, + string? contentType = null) + { + var response = httpContext.Response; + ResponseContentTypeHelper.ResolveContentTypeAndEncoding( + contentType, + response.ContentType, + (DefaultContentType, DefaultEncoding), + ResponseContentTypeHelper.GetEncoding, + out var resolvedContentType, + out var resolvedContentTypeEncoding); + + response.ContentType = resolvedContentType; + + if (statusCode is { } code) + { + response.StatusCode = code; + } + + Log.WritingResultAsContent(logger, resolvedContentType); + + if (content != null) + { + response.ContentLength = resolvedContentTypeEncoding.GetByteCount(content); + return response.WriteAsync(content, resolvedContentTypeEncoding); + } + + return Task.CompletedTask; + } + + public static (RangeItemHeaderValue? range, long rangeLength, bool completed) WriteResultAsFileCore( + HttpContext httpContext, + ILogger logger, + string? fileDownloadName, + long? fileLength, + string contentType, + bool enableRangeProcessing, + DateTimeOffset? lastModified, + EntityTagHeaderValue? entityTag) + { + var completed = false; + fileDownloadName ??= string.Empty; + + Log.WritingResultAsFile(logger, fileDownloadName); + + var fileResultInfo = new FileResultInfo + { + ContentType = contentType, + EnableRangeProcessing = enableRangeProcessing, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + LastModified = lastModified, + }; + + var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( + httpContext, + fileResultInfo, + fileLength, + enableRangeProcessing, + lastModified, + entityTag, + logger); + + if (range != null) + { + FileResultHelper.Log.WritingRangeToBody(logger); + } + + if (!serveBody) + { + completed = true; + } + + if (range != null && rangeLength == 0) + { + completed = true; + } + + return (range, rangeLength, completed); + } + + public static void ApplyProblemDetailsDefaultsIfNeeded(object? value, int? statusCode) + { + if (value is ProblemDetails problemDetails) + { + ApplyProblemDetailsDefaults(problemDetails, statusCode); + } + } + + public static void ApplyProblemDetailsDefaults(ProblemDetails problemDetails, int? statusCode) + { + // We allow StatusCode to be specified either on ProblemDetails or on the ObjectResult and use it to configure the other. + // This lets users write return Conflict(new Problem("some description")) + // or return Problem("some-problem", 422) and have the response have consistent fields. + if (problemDetails.Status is null) + { + if (statusCode is not null) + { + problemDetails.Status = statusCode; + } + else + { + problemDetails.Status = problemDetails is HttpValidationProblemDetails ? + StatusCodes.Status400BadRequest : + StatusCodes.Status500InternalServerError; + } + } + + if (ProblemDetailsDefaults.Defaults.TryGetValue(problemDetails.Status.Value, out var defaults)) + { + problemDetails.Title ??= defaults.Title; + problemDetails.Type ??= defaults.Type; + } + } + + 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")] + public static partial void WritingResultAsStatusCode(ILogger logger, int statusCode); + + [LoggerMessage(2, LogLevel.Information, + "Write content with HTTP Response ContentType of {ContentType}", + 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(5, LogLevel.Information, + "Sending file with download name '{FileDownloadName}'.", + EventName = "WritingResultAsFileWithNoFileName", + SkipEnabledCheck = true)] + private static partial void WritingResultAsFileWithNoFileName(ILogger logger, string fileDownloadName); + } +} diff --git a/src/Http/Http.Results/src/JsonHttpResult.cs b/src/Http/Http.Results/src/JsonHttpResult.cs new file mode 100644 index 000000000000..1e7800a32bdb --- /dev/null +++ b/src/Http/Http.Results/src/JsonHttpResult.cs @@ -0,0 +1,99 @@ +// 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.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Http; + +/// +/// An action result which formats the given object as JSON. +/// +public sealed class JsonHttpResult : IResult +{ + /// + /// 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) + : this(value, statusCode: null, contentType: null, jsonSerializerOptions: jsonSerializerOptions) + { + } + + /// + /// 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) + : this(value, statusCode: statusCode, contentType: null, jsonSerializerOptions: jsonSerializerOptions) + { + } + + /// + /// 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) + : this(value, statusCode: null, contentType: contentType, jsonSerializerOptions: jsonSerializerOptions) + { + + } + + /// + /// 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) + { + Value = value; + StatusCode = statusCode; + JsonSerializerOptions = jsonSerializerOptions; + ContentType = contentType; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// Gets or sets the serializer settings. + /// + public JsonSerializerOptions? JsonSerializerOptions { get; internal init; } + + /// + /// Gets the object result. + /// + public object? Value { get; } + + /// + /// Gets the value for the Content-Type header. + /// + public string? ContentType { get; internal set; } + + /// + /// Gets the HTTP status code. + /// + public int? StatusCode { 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.JsonResult"); + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger, + Value, + StatusCode, + ContentType, + JsonSerializerOptions); + } +} diff --git a/src/Http/Http.Results/src/JsonResult.cs b/src/Http/Http.Results/src/JsonResult.cs deleted file mode 100644 index d2dd5920d5f3..000000000000 --- a/src/Http/Http.Results/src/JsonResult.cs +++ /dev/null @@ -1,76 +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.Json; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Http.Result; - -/// -/// An action result which formats the given object as JSON. -/// -internal sealed partial class JsonResult : IResult -{ - /// - /// Gets or sets the representing the Content-Type header of the response. - /// - public string? ContentType { get; init; } - - /// - /// Gets or sets the serializer settings. - /// - /// When using System.Text.Json, this should be an instance of - /// - /// - /// When using Newtonsoft.Json, this should be an instance of JsonSerializerSettings. - /// - /// - public JsonSerializerOptions? JsonSerializerOptions { get; init; } - - /// - /// Gets or sets the HTTP status code. - /// - public int? StatusCode { get; init; } - - /// - /// Gets or sets the value to be formatted. - /// - public object? Value { get; init; } - - /// - /// Write the result as JSON to the HTTP response. - /// - /// The for the current request. - /// A task that represents the asynchronous execute operation. - Task IResult.ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); - Log.JsonResultExecuting(logger, Value); - - if (StatusCode is int statusCode) - { - httpContext.Response.StatusCode = statusCode; - } - - return httpContext.Response.WriteAsJsonAsync(Value, JsonSerializerOptions, ContentType); - } - - private static partial class Log - { - public static void JsonResultExecuting(ILogger logger, object? value) - { - if (logger.IsEnabled(LogLevel.Information)) - { - var type = value == null ? "null" : value.GetType().FullName!; - JsonResultExecuting(logger, type); - } - } - - [LoggerMessage(1, LogLevel.Information, - "Executing JsonResult, writing value of type '{Type}'.", - EventName = "JsonResultExecuting", - SkipEnabledCheck = true)] - private static partial void JsonResultExecuting(ILogger logger, string type); - } -} diff --git a/src/Http/Http.Results/src/LocalRedirectResult.cs b/src/Http/Http.Results/src/LocalRedirectResult.cs deleted file mode 100644 index 0cec3ab1529e..000000000000 --- a/src/Http/Http.Results/src/LocalRedirectResult.cs +++ /dev/null @@ -1,108 +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 Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Http.Result; - -/// -/// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), -/// or Permanent Redirect (308) response with a Location header to the supplied local URL. -/// -internal sealed partial class LocalRedirectResult : IResult -{ - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The local URL to redirect to. - public LocalRedirectResult(string localUrl) - : this(localUrl, permanent: false) - { - } - - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The local URL to redirect to. - /// Specifies whether the redirect should be permanent (301) or temporary (302). - public LocalRedirectResult(string localUrl, bool permanent) - : this(localUrl, permanent, preserveMethod: false) - { - } - - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// 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's method. - public LocalRedirectResult(string localUrl, bool permanent, bool preserveMethod) - { - if (string.IsNullOrEmpty(localUrl)) - { - throw new ArgumentException("Argument cannot be null or empty", nameof(localUrl)); - } - - Permanent = permanent; - PreserveMethod = preserveMethod; - Url = localUrl; - } - - /// - /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false. - /// - public bool Permanent { get; } - - /// - /// Gets or sets an indication that the redirect preserves the initial request method. - /// - public bool PreserveMethod { get; } - - /// - /// Gets or sets the local URL to redirect to. - /// - public string Url { get; } - - /// - public Task ExecuteAsync(HttpContext httpContext) - { - if (!SharedUrlHelper.IsLocalUrl(Url)) - { - throw new InvalidOperationException("The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local."); - } - - var destinationUrl = SharedUrlHelper.Content(httpContext, Url); - - // IsLocalUrl is called to handle URLs starting with '~/'. - var logger = httpContext.RequestServices.GetRequiredService>(); - - Log.LocalRedirectResultExecuting(logger, destinationUrl); - - if (PreserveMethod) - { - httpContext.Response.StatusCode = Permanent - ? StatusCodes.Status308PermanentRedirect - : StatusCodes.Status307TemporaryRedirect; - httpContext.Response.Headers.Location = destinationUrl; - } - else - { - httpContext.Response.Redirect(destinationUrl, Permanent); - } - - return Task.CompletedTask; - } - - private static partial class Log - { - [LoggerMessage(1, LogLevel.Information, - "Executing LocalRedirectResult, redirecting to {Destination}.", - EventName = "LocalRedirectResultExecuting")] - public static partial void LocalRedirectResultExecuting(ILogger logger, string destination); - } -} diff --git a/src/Http/Http.Results/src/NoContentHttpResult.cs b/src/Http/Http.Results/src/NoContentHttpResult.cs new file mode 100644 index 000000000000..0339cc3f1f38 --- /dev/null +++ b/src/Http/Http.Results/src/NoContentHttpResult.cs @@ -0,0 +1,40 @@ +// 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; + +/// +/// Represents an that when executed will +/// produce an HTTP response with the No Content (204) status code. +/// +public class NoContentHttpResult : IResult +{ + /// + /// Initializes a new instance of the class. + /// + internal NoContentHttpResult() + { + } + + /// + /// Gets the HTTP status code. + /// + public int StatusCode => StatusCodes.Status204NoContent; + + /// + 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.NoContentResult"); + + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } +} diff --git a/src/Http/Http.Results/src/NoContentResult.cs b/src/Http/Http.Results/src/NoContentResult.cs deleted file mode 100644 index 582484c406c0..000000000000 --- a/src/Http/Http.Results/src/NoContentResult.cs +++ /dev/null @@ -1,11 +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.Result; - -internal class NoContentResult : StatusCodeResult -{ - public NoContentResult() : base(StatusCodes.Status204NoContent) - { - } -} diff --git a/src/Http/Http.Results/src/NotFoundObjectHttpResult.cs b/src/Http/Http.Results/src/NotFoundObjectHttpResult.cs new file mode 100644 index 000000000000..45220b7d0483 --- /dev/null +++ b/src/Http/Http.Results/src/NotFoundObjectHttpResult.cs @@ -0,0 +1,48 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with Not Found (404) status code. +/// +public sealed class NotFoundObjectHttpResult : IResult +{ + /// + /// Initializes a new instance of the class with the values. + /// + /// The value to format in the entity body. + internal NotFoundObjectHttpResult(object? value) + { + Value = value; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// Gets the object result. + /// + public object? Value { get; internal init; } + + /// + /// 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"); + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger: logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/NotFoundObjectResult.cs b/src/Http/Http.Results/src/NotFoundObjectResult.cs deleted file mode 100644 index 5ce0e6083bc9..000000000000 --- a/src/Http/Http.Results/src/NotFoundObjectResult.cs +++ /dev/null @@ -1,12 +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.Result; - -internal sealed class NotFoundObjectResult : ObjectResult -{ - public NotFoundObjectResult(object? value) - : base(value, StatusCodes.Status404NotFound) - { - } -} diff --git a/src/Http/Http.Results/src/ObjectHttpResult.cs b/src/Http/Http.Results/src/ObjectHttpResult.cs new file mode 100644 index 000000000000..252ab122a1da --- /dev/null +++ b/src/Http/Http.Results/src/ObjectHttpResult.cs @@ -0,0 +1,80 @@ +// 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/ObjectResult.cs b/src/Http/Http.Results/src/ObjectResult.cs deleted file mode 100644 index 3204d866b558..000000000000 --- a/src/Http/Http.Results/src/ObjectResult.cs +++ /dev/null @@ -1,135 +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 Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Http.Result; - -internal partial class ObjectResult : IResult -{ - /// - /// Creates a new instance with the provided . - /// - public ObjectResult(object? value) - { - Value = value; - } - - /// - /// Creates a new instance with the provided . - /// - public ObjectResult(object? value, int? statusCode) - { - Value = value; - StatusCode = statusCode; - } - - /// - /// The object result. - /// - public object? Value { get; } - - /// - /// Gets the HTTP status code. - /// - public int? StatusCode { get; set; } - - /// - /// Gets the value for the Content-Type header. - /// - public string? ContentType { get; set; } - - public Task ExecuteAsync(HttpContext httpContext) - { - var loggerFactory = httpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(GetType()); - Log.ObjectResultExecuting(logger, Value, StatusCode); - - if (Value is ProblemDetails problemDetails) - { - ApplyProblemDetailsDefaults(problemDetails); - } - - if (StatusCode is { } statusCode) - { - httpContext.Response.StatusCode = statusCode; - } - - ConfigureResponseHeaders(httpContext); - - if (Value is null) - { - return Task.CompletedTask; - } - - OnFormatting(httpContext); - return httpContext.Response.WriteAsJsonAsync(Value, Value.GetType(), options: null, contentType: ContentType); - } - - protected virtual void OnFormatting(HttpContext httpContext) - { - } - - protected virtual void ConfigureResponseHeaders(HttpContext httpContext) - { - } - - private void ApplyProblemDetailsDefaults(ProblemDetails problemDetails) - { - // We allow StatusCode to be specified either on ProblemDetails or on the ObjectResult and use it to configure the other. - // This lets users write return Conflict(new Problem("some description")) - // or return Problem("some-problem", 422) and have the response have consistent fields. - if (problemDetails.Status is null) - { - if (StatusCode is not null) - { - problemDetails.Status = StatusCode; - } - else - { - problemDetails.Status = problemDetails is HttpValidationProblemDetails ? - StatusCodes.Status400BadRequest : - StatusCodes.Status500InternalServerError; - } - } - - if (StatusCode is null) - { - StatusCode = problemDetails.Status; - } - - if (ProblemDetailsDefaults.Defaults.TryGetValue(problemDetails.Status.Value, out var defaults)) - { - problemDetails.Title ??= defaults.Title; - problemDetails.Type ??= defaults.Type; - } - } - - private static partial class Log - { - public static void ObjectResultExecuting(ILogger logger, object? value, int? statusCode) - { - if (logger.IsEnabled(LogLevel.Information)) - { - if (value is null) - { - ObjectResultExecutingWithoutValue(logger, statusCode ?? StatusCodes.Status200OK); - } - else - { - var valueType = value.GetType().FullName!; - ObjectResultExecuting(logger, valueType, statusCode ?? StatusCodes.Status200OK); - } - } - } - - [LoggerMessage(1, LogLevel.Information, "Writing value of type '{Type}' with status code '{StatusCode}'.", EventName = "ObjectResultExecuting", SkipEnabledCheck = true)] - private static partial void ObjectResultExecuting(ILogger logger, string type, int statusCode); - - [LoggerMessage(2, LogLevel.Information, "Executing result with status code '{StatusCode}'.", EventName = "ObjectResultExecutingWithoutValue", SkipEnabledCheck = true)] - private static partial void ObjectResultExecutingWithoutValue(ILogger logger, int statusCode); - } -} diff --git a/src/Http/Http.Results/src/OkObjectHttpResult.cs b/src/Http/Http.Results/src/OkObjectHttpResult.cs new file mode 100644 index 000000000000..2dedc139d7ab --- /dev/null +++ b/src/Http/Http.Results/src/OkObjectHttpResult.cs @@ -0,0 +1,49 @@ +// 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.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +/// +/// An that on execution will write an object to the response +/// with Ok (200) status code. +/// +public sealed class OkObjectHttpResult : IResult +{ + /// + /// Initializes a new instance of the class with the values. + /// + /// The value to format in the entity body. + internal OkObjectHttpResult(object? value) + { + Value = value; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + /// Gets the object result. + /// + public object? Value { get; internal init; } + + /// + /// 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"); + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger: logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/OkObjectResult.cs b/src/Http/Http.Results/src/OkObjectResult.cs deleted file mode 100644 index 70013671deea..000000000000 --- a/src/Http/Http.Results/src/OkObjectResult.cs +++ /dev/null @@ -1,12 +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.Result; - -internal sealed class OkObjectResult : ObjectResult -{ - public OkObjectResult(object? value) - : base(value, StatusCodes.Status200OK) - { - } -} diff --git a/src/Http/Http.Results/src/PhysicalFileHttpResult.cs b/src/Http/Http.Results/src/PhysicalFileHttpResult.cs new file mode 100644 index 000000000000..97f8e01dca38 --- /dev/null +++ b/src/Http/Http.Results/src/PhysicalFileHttpResult.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +/// +/// A on execution will write a file from disk to the response +/// using mechanisms provided by the host. +/// +public sealed partial class PhysicalFileHttpResult : IResult +{ + /// + /// Creates a new instance with + /// the provided and the provided . + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + internal PhysicalFileHttpResult(string fileName, string? contentType) + : this(fileName, contentType, fileDownloadName: null) + { + } + + /// + /// Creates a new instance with + /// the provided , the provided + /// and the provided . + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + /// The suggested file name. + internal PhysicalFileHttpResult( + string fileName, + string? contentType, + string? fileDownloadName) + : this(fileName, contentType, fileDownloadName, enableRangeProcessing: false) + { + } + + /// + /// Creates a new instance with the provided values. + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + /// 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. + internal PhysicalFileHttpResult( + string fileName, + string? contentType, + string? fileDownloadName, + bool enableRangeProcessing, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + { + FileName = fileName; + ContentType = contentType ?? "application/octet-stream"; + FileDownloadName = fileDownloadName; + EnableRangeProcessing = enableRangeProcessing; + LastModified = lastModified; + EntityTag = entityTag; + } + + /// + /// Gets the Content-Type header for the response. + /// + public string ContentType { get; internal set; } + + /// + /// Gets the file name that will be used in the Content-Disposition header of the response. + /// + public string? FileDownloadName { get; internal set; } + + /// + /// Gets the last modified information associated with the file result. + /// + public DateTimeOffset? LastModified { get; internal set; } + + /// + /// Gets the etag associated with the file result. + /// + public EntityTagHeaderValue? EntityTag { get; internal init; } + + /// + /// Gets the value that enables range processing for the file result. + /// + public bool EnableRangeProcessing { get; internal init; } + + /// + /// Gets or sets the file length information . + /// + public long? FileLength { get; internal set; } + + /// + /// Gets or sets the path to the file that will be sent back as the response. + /// + public string FileName { get; } + + // For testing + internal Func GetFileInfoWrapper { get; init; } = + static path => new FileInfoWrapper(path); + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var fileInfo = GetFileInfoWrapper(FileName); + if (!fileInfo.Exists) + { + throw new FileNotFoundException($"Could not find file: {FileName}", FileName); + } + + LastModified ??= fileInfo.LastWriteTimeUtc; + FileLength = fileInfo.Length; + + // 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.PhysicalFileResult"); + + var (range, rangeLength, completed) = HttpResultsHelper.WriteResultAsFileCore( + httpContext, + logger, + FileDownloadName, + FileLength, + ContentType, + EnableRangeProcessing, + LastModified, + EntityTag); + + return completed ? + Task.CompletedTask : + ExecuteCoreAsync(httpContext, range, rangeLength, FileName); + } + + private static Task ExecuteCoreAsync(HttpContext httpContext, RangeItemHeaderValue? range, long rangeLength, string fileName) + { + var response = httpContext.Response; + if (!Path.IsPathRooted(fileName)) + { + throw new NotSupportedException($"Path '{fileName}' was not rooted."); + } + + var offset = 0L; + var count = (long?)null; + if (range != null) + { + offset = range.From ?? 0L; + count = rangeLength; + } + + return response.SendFileAsync( + fileName, + offset: offset, + count: count); + } + + internal readonly struct FileInfoWrapper + { + public FileInfoWrapper(string path) + { + var fileInfo = new FileInfo(path); + + // It means we are dealing with a symlink and need to get the information + // from the target file instead. + if (fileInfo.Exists && !string.IsNullOrEmpty(fileInfo.LinkTarget)) + { + fileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true) ?? fileInfo; + } + + Exists = fileInfo.Exists; + Length = fileInfo.Length; + LastWriteTimeUtc = fileInfo.LastWriteTimeUtc; + } + + public bool Exists { get; init; } + + public long Length { get; init; } + + public DateTimeOffset LastWriteTimeUtc { get; init; } + } +} diff --git a/src/Http/Http.Results/src/PhysicalFileResult.cs b/src/Http/Http.Results/src/PhysicalFileResult.cs deleted file mode 100644 index 63d883ea6cdc..000000000000 --- a/src/Http/Http.Results/src/PhysicalFileResult.cs +++ /dev/null @@ -1,102 +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 Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http.Result; - -/// -/// A on execution will write a file from disk to the response -/// using mechanisms provided by the host. -/// -internal sealed partial class PhysicalFileResult : FileResult, IResult -{ - /// - /// Creates a new instance with - /// the provided and the provided . - /// - /// The path to the file. The path must be an absolute path. - /// The Content-Type header of the response. - public PhysicalFileResult(string fileName, string? contentType) - : base(contentType) - { - FileName = fileName; - } - - /// - /// Gets or sets the path to the file that will be sent back as the response. - /// - public string FileName { get; } - - // For testing - public Func GetFileInfoWrapper { get; init; } = - static path => new FileInfoWrapper(path); - - protected override ILogger GetLogger(HttpContext httpContext) - { - return httpContext.RequestServices.GetRequiredService>(); - } - - public override Task ExecuteAsync(HttpContext httpContext) - { - var fileInfo = GetFileInfoWrapper(FileName); - if (!fileInfo.Exists) - { - throw new FileNotFoundException($"Could not find file: {FileName}", FileName); - } - - LastModified = LastModified ?? fileInfo.LastWriteTimeUtc; - FileLength = fileInfo.Length; - - return base.ExecuteAsync(httpContext); - } - - protected override Task ExecuteCoreAsync(HttpContext httpContext, RangeItemHeaderValue? range, long rangeLength) - { - var response = httpContext.Response; - if (!Path.IsPathRooted(FileName)) - { - throw new NotSupportedException($"Path '{FileName}' was not rooted."); - } - - var offset = 0L; - var count = (long?)null; - if (range != null) - { - offset = range.From ?? 0L; - count = rangeLength; - } - - return response.SendFileAsync( - FileName, - offset: offset, - count: count); - } - - internal readonly struct FileInfoWrapper - { - public FileInfoWrapper(string path) - { - var fileInfo = new FileInfo(path); - - // It means we are dealing with a symlink and need to get the information - // from the target file instead. - if (fileInfo.Exists && !string.IsNullOrEmpty(fileInfo.LinkTarget)) - { - fileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true) ?? fileInfo; - } - - Exists = fileInfo.Exists; - Length = fileInfo.Length; - LastWriteTimeUtc = fileInfo.LastWriteTimeUtc; - } - - public bool Exists { get; init; } - - public long Length { get; init; } - - public DateTimeOffset LastWriteTimeUtc { get; init; } - } -} diff --git a/src/Http/Http.Results/src/ProblemHttpResult.cs b/src/Http/Http.Results/src/ProblemHttpResult.cs new file mode 100644 index 000000000000..0a5c52a0fcca --- /dev/null +++ b/src/Http/Http.Results/src/ProblemHttpResult.cs @@ -0,0 +1,55 @@ +// 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 Problem Details +/// HTTP API responses based on https://tools.ietf.org/html/rfc7807 +/// +public sealed class ProblemHttpResult : IResult +{ + /// + /// Creates a new instance with + /// the provided . + /// + /// The instance to format in the entity body. + internal ProblemHttpResult(ProblemDetails problemDetails) + { + ProblemDetails = problemDetails; + HttpResultsHelper.ApplyProblemDetailsDefaults(ProblemDetails, statusCode: null); + } + + /// + /// Gets the instance. + /// + public ProblemDetails ProblemDetails { get; } + + /// + /// Gets or sets the value for the Content-Type header. + /// + public string ContentType => "application/problem+json"; + + /// + /// Gets the HTTP status code. + /// + public int? StatusCode => ProblemDetails.Status; + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(typeof(ProblemHttpResult)); + + 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 aafb2178b5a7..b53f85370ac8 100644 --- a/src/Http/Http.Results/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Results/src/PublicAPI.Unshipped.txt @@ -1,4 +1,148 @@ #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.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! 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! diff --git a/src/Http/Http.Results/src/PushStreamHttpResult.cs b/src/Http/Http.Results/src/PushStreamHttpResult.cs new file mode 100644 index 000000000000..71f454af2bf2 --- /dev/null +++ b/src/Http/Http.Results/src/PushStreamHttpResult.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents an that when executed will +/// write a file from the writer callback to the response. +/// +public sealed class PushStreamHttpResult : IResult +{ + private readonly Func _streamWriterCallback; + + /// + /// Creates a new instance with + /// the provided and the provided . + /// + /// The stream writer callback. + /// The Content-Type header of the response. + internal PushStreamHttpResult(Func streamWriterCallback, string? contentType) + : this(streamWriterCallback, contentType, fileDownloadName: null) + { + } + + /// + /// Creates a new instance with + /// the provided , the provided + /// and the provided . + /// + /// The stream writer callback. + /// The Content-Type header of the response. + /// The suggested file name. + internal PushStreamHttpResult( + Func streamWriterCallback, + string? contentType, + string? fileDownloadName) + : this(streamWriterCallback, contentType, fileDownloadName, enableRangeProcessing: false) + { + } + + /// + /// Creates a new instance with the provided values. + /// + /// The stream writer callback. + /// The Content-Type header of the response. + /// 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. + internal PushStreamHttpResult( + Func streamWriterCallback, + string? contentType, + string? fileDownloadName, + bool enableRangeProcessing, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + { + _streamWriterCallback = streamWriterCallback; + ContentType = contentType ?? "application/octet-stream"; + FileDownloadName = fileDownloadName; + EnableRangeProcessing = enableRangeProcessing; + LastModified = lastModified; + EntityTag = entityTag; + } + + /// + /// Gets the Content-Type header for the response. + /// + public string ContentType { get; internal set; } + + /// + /// Gets the file name that will be used in the Content-Disposition header of the response. + /// + public string? FileDownloadName { get; internal set; } + + /// + /// Gets the last modified information associated with the file result. + /// + public DateTimeOffset? LastModified { get; internal set; } + + /// + /// Gets the etag associated with the file result. + /// + public EntityTagHeaderValue? EntityTag { get; internal init; } + + /// + /// Gets the value that enables range processing for the file result. + /// + public bool EnableRangeProcessing { get; internal init; } + + /// + /// Gets or sets the file length information . + /// + public long? FileLength { get; internal set; } + + /// + 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.PushStreamResult"); + + var (range, rangeLength, completed) = HttpResultsHelper.WriteResultAsFileCore( + httpContext, + logger, + FileDownloadName, + FileLength, + ContentType, + EnableRangeProcessing, + LastModified, + EntityTag); + + return completed ? + Task.CompletedTask : + _streamWriterCallback(httpContext.Response.Body); + } +} diff --git a/src/Http/Http.Results/src/PushStreamResult.cs b/src/Http/Http.Results/src/PushStreamResult.cs deleted file mode 100644 index c498bf69d95b..000000000000 --- a/src/Http/Http.Results/src/PushStreamResult.cs +++ /dev/null @@ -1,29 +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 Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http.Result; - -internal sealed class PushStreamResult : FileResult -{ - private readonly Func _streamWriterCallback; - - public PushStreamResult(Func streamWriterCallback, string? contentType) - : base(contentType) - { - _streamWriterCallback = streamWriterCallback; - } - - protected override ILogger GetLogger(HttpContext httpContext) - { - return httpContext.RequestServices.GetRequiredService>(); - } - - protected override Task ExecuteCoreAsync(HttpContext httpContext, RangeItemHeaderValue? range, long rangeLength) - { - return _streamWriterCallback(httpContext.Response.Body); - } -} diff --git a/src/Http/Http.Results/src/RedirectHttpResult.cs b/src/Http/Http.Results/src/RedirectHttpResult.cs new file mode 100644 index 000000000000..3c1bdc6ce911 --- /dev/null +++ b/src/Http/Http.Results/src/RedirectHttpResult.cs @@ -0,0 +1,138 @@ +// 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.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Http; + +/// +/// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), +/// or Permanent Redirect (308) response with a Location header to the supplied URL. +/// +public sealed partial class RedirectHttpResult : IResult +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The URL to redirect to. + internal RedirectHttpResult(string url) + : this(url, permanent: false) + { + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + internal RedirectHttpResult(string url, bool permanent) + : this(url, permanent, preserveMethod: false) + { + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// 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. + internal RedirectHttpResult(string url, bool permanent, bool preserveMethod) + : this(url, acceptLocalUrlOnly: false, permanent, preserveMethod) + { } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// 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. + /// If set to true, only local URLs are accepted + /// and will throw an exception when the supplied URL is not considered local. + internal RedirectHttpResult(string url, bool acceptLocalUrlOnly, bool permanent, bool preserveMethod) + { + if (url == null) + { + throw new ArgumentNullException(nameof(url)); + } + + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentException("Argument cannot be null or empty", nameof(url)); + } + + Url = url; + Permanent = permanent; + PreserveMethod = preserveMethod; + AcceptLocalUrlOnly = acceptLocalUrlOnly; + } + + /// + /// Gets the value that specifies that the redirect should be permanent if true or temporary if false. + /// + public bool Permanent { get; } + + /// + /// Gets an indication that the redirect preserves the initial request method. + /// + public bool PreserveMethod { get; } + + /// + /// Gets the URL to redirect to. + /// + public string Url { get; } + + /// + /// Gets an indication that only local URLs are accepted. + /// + public bool AcceptLocalUrlOnly { 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.RedirectResult"); + + var isLocalUrl = SharedUrlHelper.IsLocalUrl(Url); + + if (AcceptLocalUrlOnly && !isLocalUrl) + { + throw new InvalidOperationException("The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local."); + } + + // IsLocalUrl is called to handle URLs starting with '~/'. + var destinationUrl = isLocalUrl ? SharedUrlHelper.Content(httpContext, contentPath: Url) : Url; + + Log.RedirectResultExecuting(logger, destinationUrl); + + if (PreserveMethod) + { + httpContext.Response.StatusCode = Permanent + ? StatusCodes.Status308PermanentRedirect + : StatusCodes.Status307TemporaryRedirect; + httpContext.Response.Headers.Location = destinationUrl; + } + else + { + httpContext.Response.Redirect(destinationUrl, Permanent); + } + + return Task.CompletedTask; + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Information, + "Executing RedirectResult, redirecting to {Destination}.", + EventName = "RedirectResultExecuting")] + public static partial void RedirectResultExecuting(ILogger logger, string destination); + } +} diff --git a/src/Http/Http.Results/src/RedirectResult.cs b/src/Http/Http.Results/src/RedirectResult.cs deleted file mode 100644 index b5bdf7b1775d..000000000000 --- a/src/Http/Http.Results/src/RedirectResult.cs +++ /dev/null @@ -1,83 +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 Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Http.Result; - -internal sealed partial class RedirectResult : IResult -{ - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// 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. - public RedirectResult(string url, bool permanent, bool preserveMethod) - { - if (url == null) - { - throw new ArgumentNullException(nameof(url)); - } - - if (string.IsNullOrEmpty(url)) - { - throw new ArgumentException("Argument cannot be null or empty", nameof(url)); - } - - Permanent = permanent; - PreserveMethod = preserveMethod; - Url = url; - } - - /// - /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false. - /// - public bool Permanent { get; } - - /// - /// Gets or sets an indication that the redirect preserves the initial request method. - /// - public bool PreserveMethod { get; } - - /// - /// Gets or sets the URL to redirect to. - /// - public string Url { get; } - - /// - public Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); - - // IsLocalUrl is called to handle URLs starting with '~/'. - var destinationUrl = SharedUrlHelper.IsLocalUrl(Url) ? SharedUrlHelper.Content(httpContext, Url) : Url; - - Log.RedirectResultExecuting(logger, destinationUrl); - - if (PreserveMethod) - { - httpContext.Response.StatusCode = Permanent - ? StatusCodes.Status308PermanentRedirect - : StatusCodes.Status307TemporaryRedirect; - httpContext.Response.Headers.Location = destinationUrl; - } - else - { - httpContext.Response.Redirect(destinationUrl, Permanent); - } - - return Task.CompletedTask; - } - - private static partial class Log - { - [LoggerMessage(1, LogLevel.Information, - "Executing RedirectResult, redirecting to {Destination}.", - EventName = "RedirectResultExecuting")] - public static partial void RedirectResultExecuting(ILogger logger, string destination); - } -} diff --git a/src/Http/Http.Results/src/RedirectToRouteResult.cs b/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs similarity index 76% rename from src/Http/Http.Results/src/RedirectToRouteResult.cs rename to src/Http/Http.Results/src/RedirectToRouteHttpResult.cs index 1d7458fe6738..586694792e59 100644 --- a/src/Http/Http.Results/src/RedirectToRouteResult.cs +++ b/src/Http/Http.Results/src/RedirectToRouteHttpResult.cs @@ -5,32 +5,32 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http; /// /// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), /// or Permanent Redirect (308) response with a Location header. /// Targets a registered route. /// -internal sealed partial class RedirectToRouteResult : IResult +public sealed partial class RedirectToRouteHttpResult : IResult { /// - /// Initializes a new instance of the with the values + /// Initializes a new instance of the with the values /// provided. /// /// The parameters for the route. - public RedirectToRouteResult(object? routeValues) + internal RedirectToRouteHttpResult(object? routeValues) : this(routeName: null, routeValues: routeValues) { } /// - /// Initializes a new instance of the with the values + /// Initializes a new instance of the with the values /// provided. /// /// The name of the route. /// The parameters for the route. - public RedirectToRouteResult( + internal RedirectToRouteHttpResult( string? routeName, object? routeValues) : this(routeName, routeValues, permanent: false) @@ -38,13 +38,14 @@ public RedirectToRouteResult( } /// - /// Initializes a new instance of the with the values + /// Initializes a new instance of the with the values /// provided. /// /// The name of the route. /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). - public RedirectToRouteResult( + /// If set to true, makes the redirect permanent (301). + /// Otherwise a temporary redirect is used (302). + internal RedirectToRouteHttpResult( string? routeName, object? routeValues, bool permanent) @@ -53,14 +54,16 @@ public RedirectToRouteResult( } /// - /// Initializes a new instance of the with the values + /// Initializes a new instance of the with the values /// provided. /// /// The name of the route. /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. - public RedirectToRouteResult( + /// If set to true, makes the redirect permanent (301). + /// Otherwise a temporary redirect is used (302). + /// If set to true, make the temporary redirect (307) + /// or permanent redirect (308) preserve the initial request method. + internal RedirectToRouteHttpResult( string? routeName, object? routeValues, bool permanent, @@ -70,13 +73,13 @@ public RedirectToRouteResult( } /// - /// Initializes a new instance of the with the values + /// Initializes a new instance of the with the values /// provided. /// /// The name of the route. /// The parameters for the route. /// The fragment to add to the URL. - public RedirectToRouteResult( + internal RedirectToRouteHttpResult( string? routeName, object? routeValues, string? fragment) @@ -85,14 +88,15 @@ public RedirectToRouteResult( } /// - /// Initializes a new instance of the with the values + /// Initializes a new instance of the with the values /// provided. /// /// The name of the route. /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + /// If set to true, makes the redirect permanent (301). + /// Otherwise a temporary redirect is used (302). /// The fragment to add to the URL. - public RedirectToRouteResult( + internal RedirectToRouteHttpResult( string? routeName, object? routeValues, bool permanent, @@ -102,15 +106,17 @@ public RedirectToRouteResult( } /// - /// Initializes a new instance of the with the values + /// Initializes a new instance of the with the values /// provided. /// /// The name of the route. /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// If set to true, makes the redirect permanent (301). + /// Otherwise a temporary redirect is used (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. - public RedirectToRouteResult( + internal RedirectToRouteHttpResult( string? routeName, object? routeValues, bool permanent, @@ -125,27 +131,27 @@ public RedirectToRouteResult( } /// - /// Gets or sets the name of the route to use for generating the URL. + /// Gets the name of the route to use for generating the URL. /// public string? RouteName { get; } /// - /// Gets or sets the route data to use for generating the URL. + /// Gets the route data to use for generating the URL. /// public RouteValueDictionary? RouteValues { get; } /// - /// Gets or sets an indication that the redirect is permanent. + /// Gets the value that specifies that the redirect should be permanent if true or temporary if false. /// public bool Permanent { get; } /// - /// Gets or sets an indication that the redirect preserves the initial request method. + /// Gets an indication that the redirect preserves the initial request method. /// public bool PreserveMethod { get; } /// - /// Gets or sets the fragment to add to the URL. + /// Gets the fragment to add to the URL. /// public string? Fragment { get; } @@ -159,12 +165,15 @@ public Task ExecuteAsync(HttpContext httpContext) RouteName, RouteValues, fragment: Fragment == null ? FragmentString.Empty : new FragmentString("#" + Fragment)); + if (string.IsNullOrEmpty(destinationUrl)) { throw new InvalidOperationException("No route matches the supplied values."); } - var logger = httpContext.RequestServices.GetRequiredService>(); + // 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.RedirectToRouteResult"); Log.RedirectToRouteResultExecuting(logger, destinationUrl, RouteName); if (PreserveMethod) diff --git a/src/Http/Http.Results/src/Results.cs b/src/Http/Http.Results/src/Results.cs index 0f0328e76110..89b9c12bde7d 100644 --- a/src/Http/Http.Results/src/Results.cs +++ b/src/Http/Http.Results/src/Results.cs @@ -7,7 +7,6 @@ using System.Text.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http.Result; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; @@ -33,7 +32,7 @@ public static class Results public static IResult Challenge( AuthenticationProperties? properties = null, IList? authenticationSchemes = null) - => new ChallengeResult { AuthenticationSchemes = authenticationSchemes ?? Array.Empty(), Properties = properties }; + => new ChallengeHttpResult(authenticationSchemes: authenticationSchemes ?? Array.Empty(), properties); /// /// Creates a that on execution invokes . @@ -51,7 +50,7 @@ public static IResult Challenge( /// a redirect to show a login page. /// public static IResult Forbid(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) - => new ForbidResult { Properties = properties, AuthenticationSchemes = authenticationSchemes ?? Array.Empty(), }; + => new ForbidHttpResult(authenticationSchemes: authenticationSchemes ?? Array.Empty(), properties); /// /// Creates an that on execution invokes . @@ -64,7 +63,7 @@ public static IResult SignIn( ClaimsPrincipal principal, AuthenticationProperties? properties = null, string? authenticationScheme = null) - => new SignInResult(authenticationScheme, principal, properties); + => new SignInHttpResult(principal, authenticationScheme, properties); /// /// Creates an that on execution invokes . @@ -73,7 +72,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 SignOutResult(authenticationSchemes ?? Array.Empty(), properties); + => new SignOutHttpResult(authenticationSchemes ?? Array.Empty(), properties); /// /// Writes the string to the HTTP response. @@ -115,11 +114,7 @@ public static IResult Text(string content, string? contentType = null, Encoding? mediaTypeHeaderValue.Encoding = contentEncoding ?? mediaTypeHeaderValue.Encoding; } - return new ContentResult - { - Content = content, - ContentType = mediaTypeHeaderValue?.ToString() - }; + return new ContentHttpResult(content, mediaTypeHeaderValue?.ToString()); } /// @@ -129,11 +124,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 ContentResult - { - Content = content, - ContentType = contentType.ToString() - }; + => new ContentHttpResult(content, contentType.ToString()); /// /// Creates a that serializes the specified object to JSON. @@ -142,17 +133,14 @@ 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 JsonResult + => new JsonHttpResult(data, statusCode, options) { - Value = data, - JsonSerializerOptions = options, ContentType = contentType, - StatusCode = statusCode, }; /// @@ -165,11 +153,11 @@ 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( @@ -180,7 +168,7 @@ public static IResult File( bool enableRangeProcessing = false, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) - => new FileContentResult(fileContents, contentType) + => new FileContentHttpResult(fileContents, contentType) { FileDownloadName = fileDownloadName, EnableRangeProcessing = enableRangeProcessing, @@ -211,7 +199,7 @@ public static IResult Bytes( bool enableRangeProcessing = false, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) - => new FileContentResult(contents, contentType) + => new FileContentHttpResult(contents, contentType) { FileDownloadName = fileDownloadName, EnableRangeProcessing = enableRangeProcessing, @@ -242,7 +230,7 @@ public static IResult Bytes( bool enableRangeProcessing = false, DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) - => new FileContentResult(contents, contentType) + => new FileContentHttpResult(contents, contentType) { FileDownloadName = fileDownloadName, EnableRangeProcessing = enableRangeProcessing, @@ -282,7 +270,7 @@ public static IResult File( EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) { - return new FileStreamResult(fileStream, contentType) + return new FileStreamHttpResult(fileStream, contentType) { LastModified = lastModified, EntityTag = entityTag, @@ -321,7 +309,7 @@ public static IResult Stream( EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) { - return new FileStreamResult(stream, contentType) + return new FileStreamHttpResult(stream, contentType) { LastModified = lastModified, EntityTag = entityTag, @@ -359,7 +347,7 @@ public static IResult Stream( EntityTagHeaderValue? entityTag = null, bool enableRangeProcessing = false) { - return new FileStreamResult(pipeReader.AsStream(), contentType) + return new FileStreamHttpResult(pipeReader.AsStream(), contentType) { LastModified = lastModified, EntityTag = entityTag, @@ -392,7 +380,7 @@ public static IResult Stream( DateTimeOffset? lastModified = null, EntityTagHeaderValue? entityTag = null) { - return new PushStreamResult(streamWriterCallback, contentType) + return new PushStreamHttpResult(streamWriterCallback, contentType) { LastModified = lastModified, EntityTag = entityTag, @@ -426,7 +414,7 @@ public static IResult File( { if (Path.IsPathRooted(path)) { - return new PhysicalFileResult(path, contentType) + return new PhysicalFileHttpResult(path, contentType) { FileDownloadName = fileDownloadName, LastModified = lastModified, @@ -436,7 +424,7 @@ public static IResult File( } else { - return new VirtualFileResult(path, contentType) + return new VirtualFileHttpResult(path, contentType) { FileDownloadName = fileDownloadName, LastModified = lastModified, @@ -460,7 +448,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 RedirectResult(url, permanent, preserveMethod); + => new RedirectHttpResult(url, permanent, preserveMethod); /// /// Redirects to the specified . @@ -476,7 +464,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 LocalRedirectResult(localUrl, permanent, preserveMethod); + => new RedirectHttpResult(localUrl, acceptLocalUrlOnly: true, permanent, preserveMethod); /// /// Redirects to the specified route. @@ -494,7 +482,7 @@ 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 RedirectToRouteResult( + => new RedirectToRouteHttpResult( routeName: routeName, routeValues: routeValues, permanent: permanent, @@ -502,12 +490,12 @@ public static IResult RedirectToRoute(string? routeName = null, object? routeVal fragment: fragment); /// - /// Creates a object by specifying a . + /// Creates a 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) - => new StatusCodeResult(statusCode); + => new StatusCodeHttpResult(statusCode); /// /// Produces a response. @@ -515,14 +503,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) - => new NotFoundObjectResult(value); + => new NotFoundObjectHttpResult(value); /// /// Produces a response. /// /// The created for the response. public static IResult Unauthorized() - => new UnauthorizedResult(); + => new UnauthorizedHttpResult(); /// /// Produces a response. @@ -530,7 +518,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) - => new BadRequestObjectResult(error); + => new BadRequestObjectHttpResult(error); /// /// Produces a response. @@ -538,14 +526,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) - => new ConflictObjectResult(error); + => new ConflictObjectHttpResult(error); /// /// Produces a response. /// /// The created for the response. public static IResult NoContent() - => new NoContentResult(); + => new NoContentHttpResult(); /// /// Produces a response. @@ -553,7 +541,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) - => new OkObjectResult(value); + => new OkObjectHttpResult(value); /// /// Produces a response. @@ -561,7 +549,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) - => new UnprocessableEntityObjectResult(error); + => new UnprocessableEntityObjectHttpResult(error); /// /// Produces a response. @@ -598,10 +586,7 @@ public static IResult Problem( } } - return new ObjectResult(problemDetails) - { - ContentType = "application/problem+json", - }; + return new ProblemHttpResult(problemDetails); } /// @@ -611,10 +596,7 @@ public static IResult Problem( /// The created for the response. public static IResult Problem(ProblemDetails problemDetails) { - return new ObjectResult(problemDetails) - { - ContentType = "application/problem+json", - }; + return new ProblemHttpResult(problemDetails); } /// @@ -656,10 +638,7 @@ public static IResult ValidationProblem( } } - return new ObjectResult(problemDetails) - { - ContentType = "application/problem+json", - }; + return new ProblemHttpResult(problemDetails); } /// @@ -675,7 +654,7 @@ public static IResult Created(string uri, object? value) throw new ArgumentNullException(nameof(uri)); } - return new CreatedResult(uri, value); + return new CreatedHttpResult(uri, value); } /// @@ -691,7 +670,7 @@ public static IResult Created(Uri uri, object? value) throw new ArgumentNullException(nameof(uri)); } - return new CreatedResult(uri, value); + return new CreatedHttpResult(uri, value); } /// @@ -702,7 +681,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 CreatedAtRouteResult(routeName, routeValues, value); + => new CreatedAtRouteHttpResult(routeName, routeValues, value); /// /// Produces a response. @@ -711,7 +690,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 AcceptedResult(uri, value); + => new AcceptedHttpResult(uri, value); /// /// Produces a response. @@ -721,12 +700,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 AcceptedAtRouteResult(routeName, routeValues, value); + => new AcceptedAtRouteHttpResult(routeName, routeValues, value); /// /// Produces an empty result response, that when executed will do nothing. /// - public static IResult Empty { get; } = EmptyResult.Instance; + public static IResult Empty { get; } = EmptyHttpResult.Instance; /// /// Provides a container for external libraries to extend diff --git a/src/Http/Http.Results/src/SignInResult.cs b/src/Http/Http.Results/src/SignInHttpResult.cs similarity index 55% rename from src/Http/Http.Results/src/SignInResult.cs rename to src/Http/Http.Results/src/SignInHttpResult.cs index d0bd19996157..a1b0d805e867 100644 --- a/src/Http/Http.Results/src/SignInResult.cs +++ b/src/Http/Http.Results/src/SignInHttpResult.cs @@ -6,53 +6,31 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http; /// /// An that on execution invokes . /// -internal sealed partial class SignInResult : IResult +public sealed partial class SignInHttpResult : IResult { /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// default authentication scheme. /// /// The claims principal containing the user claims. - public SignInResult(ClaimsPrincipal principal) - : this(authenticationScheme: null, principal, properties: null) + internal SignInHttpResult(ClaimsPrincipal principal) + : this(principal, authenticationScheme: null, properties: null) { } /// - /// Initializes a new instance of with the - /// specified authentication scheme. - /// - /// The authentication scheme to use when signing in the user. - /// The claims principal containing the user claims. - public SignInResult(string? authenticationScheme, ClaimsPrincipal principal) - : this(authenticationScheme, principal, properties: null) - { - } - - /// - /// Initializes a new instance of with the - /// default authentication scheme and . - /// - /// The claims principal containing the user claims. - /// used to perform the sign-in operation. - public SignInResult(ClaimsPrincipal principal, AuthenticationProperties? properties) - : this(authenticationScheme: null, principal, properties) - { - } - - /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication scheme and . /// - /// The authentication schemes to use when signing in the user. /// The claims principal containing the user claims. + /// The authentication schemes to use when signing in the user. /// used to perform the sign-in operation. - public SignInResult(string? authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties? properties) + internal SignInHttpResult(ClaimsPrincipal principal, string? authenticationScheme, AuthenticationProperties? properties) { Principal = principal ?? throw new ArgumentNullException(nameof(principal)); AuthenticationScheme = authenticationScheme; @@ -62,22 +40,24 @@ public SignInResult(string? authenticationScheme, ClaimsPrincipal principal, Aut /// /// Gets or sets the authentication scheme that is used to perform the sign-in operation. /// - public string? AuthenticationScheme { get; set; } + public string? AuthenticationScheme { get; internal init; } /// /// Gets or sets the containing the user claims. /// - public ClaimsPrincipal Principal { get; set; } + public ClaimsPrincipal Principal { get; internal init; } /// /// Gets or sets the used to perform the sign-in operation. /// - public AuthenticationProperties? Properties { get; set; } + public AuthenticationProperties? Properties { get; internal init; } /// public Task ExecuteAsync(HttpContext httpContext) { - var logger = httpContext.RequestServices.GetRequiredService>(); + // 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.SignInResult"); Log.SignInResultExecuting(logger, AuthenticationScheme, Principal); diff --git a/src/Http/Http.Results/src/SignOutResult.cs b/src/Http/Http.Results/src/SignOutHttpResult.cs similarity index 63% rename from src/Http/Http.Results/src/SignOutResult.cs rename to src/Http/Http.Results/src/SignOutHttpResult.cs index 422037cfe9de..7d187fe52c15 100644 --- a/src/Http/Http.Results/src/SignOutResult.cs +++ b/src/Http/Http.Results/src/SignOutHttpResult.cs @@ -6,88 +6,95 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result; +namespace Microsoft.AspNetCore.Http; /// /// An that on execution invokes . /// -internal sealed partial class SignOutResult : IResult +public sealed partial class SignOutHttpResult : IResult { /// - /// Initializes a new instance of with the default sign out scheme. + /// Initializes a new instance of with the default sign out scheme. /// - public SignOutResult() + internal SignOutHttpResult() : this(Array.Empty()) { } /// - /// Initializes a new instance of with the default sign out scheme. + /// Initializes a new instance of with the default sign out scheme. /// specified authentication scheme and . /// /// used to perform the sign-out operation. - public SignOutResult(AuthenticationProperties properties) + internal SignOutHttpResult(AuthenticationProperties properties) : this(Array.Empty(), properties) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication scheme. /// /// The authentication scheme to use when signing out the user. - public SignOutResult(string authenticationScheme) + internal SignOutHttpResult(string authenticationScheme) : this(new[] { authenticationScheme }) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication schemes. /// /// The authentication schemes to use when signing out the user. - public SignOutResult(IList authenticationSchemes) + internal SignOutHttpResult(IList authenticationSchemes) : this(authenticationSchemes, properties: null) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication scheme and . /// /// The authentication schemes to use when signing out the user. /// used to perform the sign-out operation. - public SignOutResult(string authenticationScheme, AuthenticationProperties? properties) + internal SignOutHttpResult(string authenticationScheme, AuthenticationProperties? properties) : this(new[] { authenticationScheme }, properties) { } /// - /// Initializes a new instance of with the + /// Initializes a new instance of with the /// specified authentication schemes and . /// /// The authentication scheme to use when signing out the user. /// used to perform the sign-out operation. - public SignOutResult(IList authenticationSchemes, AuthenticationProperties? properties) + internal SignOutHttpResult(IList authenticationSchemes, AuthenticationProperties? properties) { - AuthenticationSchemes = authenticationSchemes ?? throw new ArgumentNullException(nameof(authenticationSchemes)); + if (authenticationSchemes is null) + { + throw new ArgumentNullException(nameof(authenticationSchemes)); + } + + AuthenticationSchemes = authenticationSchemes.AsReadOnly(); Properties = properties; } /// - /// Gets or sets the authentication schemes that are challenged. + /// Gets the authentication schemes that are challenged. /// - public IList AuthenticationSchemes { get; init; } + public IReadOnlyList AuthenticationSchemes { get; internal init; } /// - /// Gets or sets the used to perform the sign-out operation. + /// Gets the used to perform the sign-out operation. /// - public AuthenticationProperties? Properties { get; init; } + public AuthenticationProperties? Properties { get; internal init; } /// public async Task ExecuteAsync(HttpContext httpContext) { - var logger = httpContext.RequestServices.GetRequiredService>(); + // 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.SignOutResult"); Log.SignOutResultExecuting(logger, AuthenticationSchemes); @@ -106,7 +113,7 @@ public async Task ExecuteAsync(HttpContext httpContext) private static partial class Log { - public static void SignOutResultExecuting(ILogger logger, IList authenticationSchemes) + public static void SignOutResultExecuting(ILogger logger, IReadOnlyList authenticationSchemes) { if (logger.IsEnabled(LogLevel.Information)) { diff --git a/src/Http/Http.Results/src/StatusCodeResult.cs b/src/Http/Http.Results/src/StatusCodeHttpResult.cs similarity index 56% rename from src/Http/Http.Results/src/StatusCodeResult.cs rename to src/Http/Http.Results/src/StatusCodeHttpResult.cs index 2140eb5cc30f..3e24b45a7767 100644 --- a/src/Http/Http.Results/src/StatusCodeResult.cs +++ b/src/Http/Http.Results/src/StatusCodeHttpResult.cs @@ -1,19 +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; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result; - -internal partial class StatusCodeResult : IResult +/// +/// Represents an that when executed will +/// produce an HTTP response with the given response status code. +/// +public sealed partial class StatusCodeHttpResult : IResult { /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// with the given . /// /// The HTTP status code of the response. - public StatusCodeResult(int statusCode) + internal StatusCodeHttpResult(int statusCode) { StatusCode = statusCode; } @@ -30,20 +34,13 @@ public StatusCodeResult(int statusCode) /// A task that represents the asynchronous execute operation. public Task ExecuteAsync(HttpContext httpContext) { - var factory = httpContext.RequestServices.GetRequiredService(); - var logger = factory.CreateLogger(GetType()); - - Log.StatusCodeResultExecuting(logger, StatusCode); + // 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.StatusCodeResult"); + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); httpContext.Response.StatusCode = StatusCode; - return Task.CompletedTask; - } - private static partial class Log - { - [LoggerMessage(1, LogLevel.Information, - "Executing StatusCodeResult, setting HTTP status code {StatusCode}.", - EventName = "StatusCodeResultExecuting")] - public static partial void StatusCodeResultExecuting(ILogger logger, int statusCode); + return Task.CompletedTask; } } diff --git a/src/Http/Http.Results/src/UnauthorizedHttpResult.cs b/src/Http/Http.Results/src/UnauthorizedHttpResult.cs new file mode 100644 index 000000000000..79bc2c17dd91 --- /dev/null +++ b/src/Http/Http.Results/src/UnauthorizedHttpResult.cs @@ -0,0 +1,40 @@ +// 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; + +/// +/// Represents an that when executed will +/// produce an HTTP response with the No Unauthorized (401) status code. +/// +public sealed class UnauthorizedHttpResult : IResult +{ + /// + /// Initializes a new instance of the class. + /// + internal UnauthorizedHttpResult() + { + } + + /// + /// Gets the HTTP status code. + /// + public int StatusCode => StatusCodes.Status401Unauthorized; + + /// + 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.UnauthorizedResult"); + HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); + + httpContext.Response.StatusCode = StatusCode; + + return Task.CompletedTask; + } + +} diff --git a/src/Http/Http.Results/src/UnauthorizedResult.cs b/src/Http/Http.Results/src/UnauthorizedResult.cs deleted file mode 100644 index 28ec97eaf462..000000000000 --- a/src/Http/Http.Results/src/UnauthorizedResult.cs +++ /dev/null @@ -1,11 +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.Result; - -internal sealed class UnauthorizedResult : StatusCodeResult -{ - public UnauthorizedResult() : base(StatusCodes.Status401Unauthorized) - { - } -} diff --git a/src/Http/Http.Results/src/UnprocessableEntityObjectHttpResult.cs b/src/Http/Http.Results/src/UnprocessableEntityObjectHttpResult.cs new file mode 100644 index 000000000000..d1c8eda62dae --- /dev/null +++ b/src/Http/Http.Results/src/UnprocessableEntityObjectHttpResult.cs @@ -0,0 +1,45 @@ +// 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; + +/// +/// An that on execution will write an object to the response +/// with Unprocessable Entity (422) status code. +/// +public sealed class UnprocessableEntityObjectHttpResult : IResult +{ + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The value to format in the entity body. + internal UnprocessableEntityObjectHttpResult(object? value) + { + Value = value; + HttpResultsHelper.ApplyProblemDetailsDefaultsIfNeeded(Value, StatusCode); + } + + /// + public object? Value { get; internal init; } + + /// > + 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"); + + return HttpResultsHelper.WriteResultAsJsonAsync( + httpContext, + logger: logger, + Value, + StatusCode); + } +} diff --git a/src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs b/src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs deleted file mode 100644 index fd7b456eb887..000000000000 --- a/src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs +++ /dev/null @@ -1,12 +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.Result; - -internal sealed class UnprocessableEntityObjectResult : ObjectResult -{ - public UnprocessableEntityObjectResult(object? error) - : base(error, StatusCodes.Status422UnprocessableEntity) - { - } -} diff --git a/src/Http/Http.Results/src/VirtualFileHttpResult.cs b/src/Http/Http.Results/src/VirtualFileHttpResult.cs new file mode 100644 index 000000000000..cac64665d23d --- /dev/null +++ b/src/Http/Http.Results/src/VirtualFileHttpResult.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +/// +/// A that on execution writes the file specified +/// using a virtual path to the response using mechanisms provided by the host. +/// +public sealed class VirtualFileHttpResult : IResult +{ + private string _fileName; + + /// + /// Creates a new instance with + /// the provided and the provided . + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + internal VirtualFileHttpResult(string fileName, string? contentType) + : this(fileName, contentType, fileDownloadName: null) + { + } + + /// + /// Creates a new instance with + /// the provided , the provided + /// and the provided . + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + /// The suggested file name. + internal VirtualFileHttpResult( + string fileName, + string? contentType, + string? fileDownloadName) + : this(fileName, contentType, fileDownloadName, enableRangeProcessing: false) + { + } + + /// + /// Creates a new instance with the provided values. + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + /// 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. + internal VirtualFileHttpResult( + string fileName, + string? contentType, + string? fileDownloadName, + bool enableRangeProcessing, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + { + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + ContentType = contentType ?? "application/octet-stream"; + FileDownloadName = fileDownloadName; + EnableRangeProcessing = enableRangeProcessing; + LastModified = lastModified; + EntityTag = entityTag; + } + + /// + public string ContentType { get; internal set; } + + /// + public string? FileDownloadName { get; internal set; } + + /// + public DateTimeOffset? LastModified { get; internal set; } + + /// + public EntityTagHeaderValue? EntityTag { get; internal init; } + + /// + public bool EnableRangeProcessing { get; internal init; } + + /// + public long? FileLength { get; internal set; } + + /// + /// Gets or sets the path to the file that will be sent back as the response. + /// + public string FileName + { + get => _fileName; + [MemberNotNull(nameof(_fileName))] + internal set => _fileName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var hostingEnvironment = httpContext.RequestServices.GetRequiredService(); + + var fileInfo = GetFileInformation(hostingEnvironment.WebRootFileProvider); + if (!fileInfo.Exists) + { + throw new FileNotFoundException($"Could not find file: {FileName}.", FileName); + } + LastModified = LastModified ?? fileInfo.LastModified; + FileLength = fileInfo.Length; + + // 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.VirtualFileResult"); + + var (range, rangeLength, completed) = HttpResultsHelper.WriteResultAsFileCore( + httpContext, + logger, + FileDownloadName, + FileLength, + ContentType, + EnableRangeProcessing, + LastModified, + EntityTag); + + return completed ? + Task.CompletedTask : + ExecuteCoreAsync(httpContext, range, rangeLength, fileInfo); + } + + private static Task ExecuteCoreAsync(HttpContext httpContext, RangeItemHeaderValue? range, long rangeLength, IFileInfo fileInfo) + { + var response = httpContext.Response; + var offset = 0L; + var count = (long?)null; + if (range != null) + { + offset = range.From ?? 0L; + count = rangeLength; + } + + return response.SendFileAsync( + fileInfo!, + offset, + count); + } + + internal IFileInfo GetFileInformation(IFileProvider fileProvider) + { + var normalizedPath = FileName; + if (normalizedPath.StartsWith("~", StringComparison.Ordinal)) + { + normalizedPath = normalizedPath.Substring(1); + } + + var fileInfo = fileProvider.GetFileInfo(normalizedPath); + return fileInfo; + } +} diff --git a/src/Http/Http.Results/src/VirtualFileResult.cs b/src/Http/Http.Results/src/VirtualFileResult.cs deleted file mode 100644 index 38d0c32eaf2c..000000000000 --- a/src/Http/Http.Results/src/VirtualFileResult.cs +++ /dev/null @@ -1,95 +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.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http.Result; - -/// -/// A that on execution writes the file specified using a virtual path to the response -/// using mechanisms provided by the host. -/// -internal sealed class VirtualFileResult : FileResult, IResult -{ - private string _fileName; - private IFileInfo? _fileInfo; - - /// - /// Creates a new instance with the provided - /// and the provided . - /// - /// The path to the file. The path must be relative/virtual. - /// The Content-Type header of the response. - public VirtualFileResult(string fileName, string? contentType) - : base(contentType) - { - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - } - - /// - /// Gets or sets the path to the file that will be sent back as the response. - /// - public string FileName - { - get => _fileName; - [MemberNotNull(nameof(_fileName))] - set => _fileName = value ?? throw new ArgumentNullException(nameof(value)); - } - - protected override Task ExecuteCoreAsync(HttpContext httpContext, RangeItemHeaderValue? range, long rangeLength) - { - var response = httpContext.Response; - var offset = 0L; - var count = (long?)null; - if (range != null) - { - offset = range.From ?? 0L; - count = rangeLength; - } - - return response.SendFileAsync( - _fileInfo!, - offset, - count); - } - - protected override ILogger GetLogger(HttpContext httpContext) - { - return httpContext.RequestServices.GetRequiredService>(); - } - - /// - public override Task ExecuteAsync(HttpContext httpContext) - { - var hostingEnvironment = httpContext.RequestServices.GetRequiredService(); - - var fileInfo = GetFileInformation(hostingEnvironment.WebRootFileProvider); - if (!fileInfo.Exists) - { - throw new FileNotFoundException($"Could not find file: {FileName}.", FileName); - } - - _fileInfo = fileInfo; - LastModified = LastModified ?? fileInfo.LastModified; - FileLength = fileInfo.Length; - - return base.ExecuteAsync(httpContext); - } - - internal IFileInfo GetFileInformation(IFileProvider fileProvider) - { - var normalizedPath = FileName; - if (normalizedPath.StartsWith("~", StringComparison.Ordinal)) - { - normalizedPath = normalizedPath.Substring(1); - } - - var fileInfo = fileProvider.GetFileInfo(normalizedPath); - return fileInfo; - } -} diff --git a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs index 5b186a474c6c..ae0b8f754085 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs @@ -10,6 +10,24 @@ namespace Microsoft.AspNetCore.Http.Result; 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() { @@ -27,7 +45,7 @@ public async Task ExecuteResultAsync_FormatsData() }); // Act - var result = new AcceptedAtRouteResult( + var result = new AcceptedAtRouteHttpResult( routeName: "sample", routeValues: routeValues, value: "Hello world"); @@ -69,7 +87,7 @@ public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader(object valu var httpContext = GetHttpContext(linkGenerator); // Act - var result = new AcceptedAtRouteResult(routeValues: values, value: null); + var result = new AcceptedAtRouteHttpResult(routeValues: values, value: null); await result.ExecuteAsync(httpContext); // Assert @@ -85,7 +103,7 @@ public async Task ExecuteResultAsync_ThrowsIfRouteUrlIsNull() var httpContext = GetHttpContext(linkGenerator); // Act - var result = new AcceptedAtRouteResult( + var result = new AcceptedAtRouteHttpResult( routeName: null, routeValues: new Dictionary(), value: null); diff --git a/src/Http/Http.Results/test/AcceptedResultTests.cs b/src/Http/Http.Results/test/AcceptedResultTests.cs index 1d5f98931186..9cf5eb6bbfb8 100644 --- a/src/Http/Http.Results/test/AcceptedResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedResultTests.cs @@ -16,7 +16,7 @@ public async Task ExecuteResultAsync_FormatsData() var stream = new MemoryStream(); httpContext.Response.Body = stream; // Act - var result = new AcceptedResult("my-location", value: "Hello world"); + var result = new AcceptedHttpResult("my-location", value: "Hello world"); await result.ExecuteAsync(httpContext); // Assert @@ -32,7 +32,7 @@ public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() var httpContext = GetHttpContext(); // Act - var result = new AcceptedResult(expectedUrl, value: "some-value"); + var result = new AcceptedHttpResult(expectedUrl, value: "some-value"); await result.ExecuteAsync(httpContext); // Assert @@ -40,6 +40,20 @@ public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() 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 AcceptedHttpResult(expectedUrl, obj); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(StatusCodes.Status202Accepted, obj.Status); + Assert.Equal(obj, result.Value); + } + private static HttpContext GetHttpContext() { var httpContext = new DefaultHttpContext(); diff --git a/src/Http/Http.Results/test/BadRequestObjectResultTests.cs b/src/Http/Http.Results/test/BadRequestObjectResultTests.cs index 2cddb2616671..c72a82752af1 100644 --- a/src/Http/Http.Results/test/BadRequestObjectResultTests.cs +++ b/src/Http/Http.Results/test/BadRequestObjectResultTests.cs @@ -3,6 +3,11 @@ namespace Microsoft.AspNetCore.Http.Result; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + public class BadRequestObjectResultTests { [Fact] @@ -10,10 +15,69 @@ public void BadRequestObjectResult_SetsStatusCodeAndValue() { // Arrange & Act var obj = new object(); - var badRequestObjectResult = new BadRequestObjectResult(obj); + var badRequestObjectResult = new BadRequestObjectHttpResult(obj); // Assert Assert.Equal(StatusCodes.Status400BadRequest, badRequestObjectResult.StatusCode); Assert.Equal(obj, badRequestObjectResult.Value); } + + [Fact] + public void BadRequestObjectResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var obj = new HttpValidationProblemDetails(); + var result = new BadRequestObjectHttpResult(obj); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal(StatusCodes.Status400BadRequest, obj.Status); + Assert.Equal(obj, result.Value); + } + + [Fact] + public async Task BadRequestObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new BadRequestObjectHttpResult("Hello"); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); + } + + [Fact] + public async Task BadRequestObjectResult_ExecuteResultAsync_FormatsData() + { + // Arrange + var result = new BadRequestObjectHttpResult("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 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/ChallengeResultTest.cs index a5a869adb898..95817e6ba449 100644 --- a/src/Http/Http.Results/test/ChallengeResultTest.cs +++ b/src/Http/Http.Results/test/ChallengeResultTest.cs @@ -15,7 +15,7 @@ public class ChallengeResultTest public async Task ChallengeResult_ExecuteAsync() { // Arrange - var result = new ChallengeResult("", null); + var result = new ChallengeHttpResult("", null); var auth = new Mock(); var httpContext = GetHttpContext(auth); @@ -30,7 +30,7 @@ public async Task ChallengeResult_ExecuteAsync() public async Task ChallengeResult_ExecuteAsync_NoSchemes() { // Arrange - var result = new ChallengeResult(new string[] { }, null); + var result = new ChallengeHttpResult(new string[] { }, null); var auth = new Mock(); var httpContext = GetHttpContext(auth); @@ -54,6 +54,7 @@ private static IServiceCollection CreateServices() { var services = new ServiceCollection(); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddSingleton(NullLoggerFactory.Instance); return services; } } diff --git a/src/Http/Http.Results/test/ConflictObjectResultTest.cs b/src/Http/Http.Results/test/ConflictObjectResultTest.cs index 4310cb3a3575..091d03bc605f 100644 --- a/src/Http/Http.Results/test/ConflictObjectResultTest.cs +++ b/src/Http/Http.Results/test/ConflictObjectResultTest.cs @@ -3,6 +3,12 @@ namespace Microsoft.AspNetCore.Http.Result; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + public class ConflictObjectResultTest { [Fact] @@ -10,10 +16,69 @@ public void ConflictObjectResult_SetsStatusCodeAndValue() { // Arrange & Act var obj = new object(); - var conflictObjectResult = new ConflictObjectResult(obj); + var conflictObjectResult = new ConflictObjectHttpResult(obj); + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode); + Assert.Equal(obj, conflictObjectResult.Value); + } + + [Fact] + public void ConflictObjectResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var obj = new ProblemDetails(); + var conflictObjectResult = new ConflictObjectHttpResult(obj); // Assert Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode); + Assert.Equal(StatusCodes.Status409Conflict, obj.Status); Assert.Equal(obj, conflictObjectResult.Value); } + + [Fact] + public async Task ConflictObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new ConflictObjectHttpResult("Hello"); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status409Conflict, httpContext.Response.StatusCode); + } + + [Fact] + public async Task ConflictObjectResult_ExecuteResultAsync_FormatsData() + { + // Arrange + var result = new ConflictObjectHttpResult("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 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/ContentResultTest.cs index f373151b2b83..f45ea77e6226 100644 --- a/src/Http/Http.Results/test/ContentResultTest.cs +++ b/src/Http/Http.Results/test/ContentResultTest.cs @@ -15,14 +15,12 @@ public class ContentResultTest public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding() { // Arrange - var contentResult = new ContentResult + var contentType = new MediaTypeHeaderValue("text/plain") { - Content = null, - ContentType = new MediaTypeHeaderValue("text/plain") - { - Encoding = Encoding.Unicode - }.ToString() - }; + Encoding = Encoding.Unicode + }.ToString(); + + var contentResult = new ContentHttpResult(null, contentType); var httpContext = GetHttpContext(); // Act @@ -109,11 +107,7 @@ public async Task ContentResult_ExecuteAsync_SetContentTypeAndEncoding_OnRespons byte[] expectedContentData) { // Arrange - var contentResult = new ContentResult - { - Content = content, - ContentType = contentType?.ToString() - }; + var contentResult = new ContentHttpResult(content, contentType?.ToString()); var httpContext = GetHttpContext(); var memoryStream = new MemoryStream(); httpContext.Response.Body = memoryStream; @@ -133,6 +127,7 @@ private static IServiceCollection CreateServices() { var services = new ServiceCollection(); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddSingleton(NullLoggerFactory.Instance); return services; } diff --git a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs index e1235d763447..5fd0575a3c35 100644 --- a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs @@ -11,6 +11,23 @@ namespace Microsoft.AspNetCore.Http.Result; 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 @@ -39,7 +56,7 @@ public async Task CreatedAtRouteResult_ReturnsStatusCode_SetsLocationHeader(obje var httpContext = GetHttpContext(expectedUrl); // Act - var result = new CreatedAtRouteResult(routeName: null, routeValues: values, value: null); + var result = new CreatedAtRouteHttpResult(routeName: null, routeValues: values, value: null); await result.ExecuteAsync(httpContext); // Assert @@ -53,7 +70,7 @@ public async Task CreatedAtRouteResult_ThrowsOnNullUrl() // Arrange var httpContext = GetHttpContext(expectedUrl: null); - var result = new CreatedAtRouteResult( + var result = new CreatedAtRouteHttpResult( routeName: null, routeValues: new Dictionary(), value: null); diff --git a/src/Http/Http.Results/test/CreatedResultTest.cs b/src/Http/Http.Results/test/CreatedResultTest.cs index d662ba8a12d5..2bc4b0ffa180 100644 --- a/src/Http/Http.Results/test/CreatedResultTest.cs +++ b/src/Http/Http.Results/test/CreatedResultTest.cs @@ -9,6 +9,20 @@ namespace Microsoft.AspNetCore.Http.Result; 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() { @@ -16,7 +30,7 @@ public void CreatedResult_SetsLocation() var location = "http://test/location"; // Act - var result = new CreatedResult(location, "testInput"); + var result = new CreatedHttpResult(location, "testInput"); // Assert Assert.Same(location, result.Location); @@ -28,7 +42,7 @@ public async Task CreatedResult_ReturnsStatusCode_SetsLocationHeader() // Arrange var location = "/test/"; var httpContext = GetHttpContext(); - var result = new CreatedResult(location, "testInput"); + var result = new CreatedHttpResult(location, "testInput"); // Act await result.ExecuteAsync(httpContext); @@ -45,7 +59,7 @@ public async Task CreatedResult_OverwritesLocationHeader() var location = "/test/"; var httpContext = GetHttpContext(); httpContext.Response.Headers["Location"] = "/different/location/"; - var result = new CreatedResult(location, "testInput"); + var result = new CreatedHttpResult(location, "testInput"); // Act await result.ExecuteAsync(httpContext); diff --git a/src/Http/Http.Results/test/EmptyResultTest.cs b/src/Http/Http.Results/test/EmptyResultTest.cs index b8771fc49985..b8dd30d4dfff 100644 --- a/src/Http/Http.Results/test/EmptyResultTest.cs +++ b/src/Http/Http.Results/test/EmptyResultTest.cs @@ -11,7 +11,7 @@ public class EmptyResultTest public async Task EmptyResult_DoesNothing() { // Arrange - var emptyResult = EmptyResult.Instance; + var emptyResult = EmptyHttpResult.Instance; // Act var httpContext = GetHttpContext(); diff --git a/src/Http/Http.Results/test/FileContentResultTest.cs b/src/Http/Http.Results/test/FileContentResultTest.cs index 2ea9ccd1c6fa..1628bcc8da6d 100644 --- a/src/Http/Http.Results/test/FileContentResultTest.cs +++ b/src/Http/Http.Results/test/FileContentResultTest.cs @@ -19,7 +19,7 @@ protected override Task ExecuteAsync( EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false) { - var result = new FileContentResult(buffer, contentType) + var result = new FileContentHttpResult(buffer, contentType) { EntityTag = entityTag, LastModified = lastModified, @@ -28,6 +28,7 @@ protected override Task ExecuteAsync( httpContext.RequestServices = new ServiceCollection() .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)) + .AddSingleton() .BuildServiceProvider(); return result.ExecuteAsync(httpContext); diff --git a/src/Http/Http.Results/test/FileStreamResultTest.cs b/src/Http/Http.Results/test/FileStreamResultTest.cs index 625b32224baf..82f7939da79a 100644 --- a/src/Http/Http.Results/test/FileStreamResultTest.cs +++ b/src/Http/Http.Results/test/FileStreamResultTest.cs @@ -16,7 +16,7 @@ protected override Task ExecuteAsync( EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false) { - var fileStreamResult = new FileStreamResult(stream, contentType) + var fileStreamResult = new FileStreamHttpResult(stream, contentType) { LastModified = lastModified, EntityTag = entityTag, @@ -33,7 +33,7 @@ public void Constructor_SetsFileName() var stream = Stream.Null; // Act - var result = new FileStreamResult(stream, "text/plain"); + var result = new FileStreamHttpResult(stream, "text/plain"); // Assert Assert.Equal(stream, result.FileStream); @@ -48,7 +48,7 @@ public void Constructor_SetsContentTypeAndParameters() var expectedMediaType = contentType; // Act - var result = new FileStreamResult(stream, contentType); + var result = new FileStreamHttpResult(stream, contentType); // Assert Assert.Equal(stream, result.FileStream); @@ -66,7 +66,7 @@ public void Constructor_SetsLastModifiedAndEtag() var entityTag = new EntityTagHeaderValue("\"Etag\""); // Act - var result = new FileStreamResult(stream, contentType) + var result = new FileStreamHttpResult(stream, contentType) { LastModified = lastModified, EntityTag = entityTag, diff --git a/src/Http/Http.Results/test/ForbidResultTest.cs b/src/Http/Http.Results/test/ForbidResultTest.cs index efab37734847..d3b770178ded 100644 --- a/src/Http/Http.Results/test/ForbidResultTest.cs +++ b/src/Http/Http.Results/test/ForbidResultTest.cs @@ -22,7 +22,7 @@ public async Task ExecuteResultAsync_InvokesForbidAsyncOnAuthenticationService() .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new ForbidResult("", null); + var result = new ForbidHttpResult("", null); // Act await result.ExecuteAsync(httpContext); @@ -46,7 +46,7 @@ public async Task ExecuteResultAsync_InvokesForbidAsyncOnAllConfiguredSchemes() .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new ForbidResult(new[] { "Scheme1", "Scheme2" }, authProperties); + var result = new ForbidHttpResult(new[] { "Scheme1", "Scheme2" }, authProperties); var routeData = new RouteData(); // Act @@ -73,7 +73,7 @@ public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties(Authen .Setup(c => c.ForbidAsync(It.IsAny(), null, expected)) .Returns(Task.CompletedTask) .Verifiable(); - var result = new ForbidResult(expected); + var result = new ForbidHttpResult(expected); var httpContext = GetHttpContext(auth.Object); // Act @@ -95,7 +95,7 @@ public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties_WhenAu .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new ForbidResult(expected) + var result = new ForbidHttpResult(expected) { AuthenticationSchemes = new string[0] }; @@ -121,6 +121,7 @@ private static IServiceCollection CreateServices() { var services = new ServiceCollection(); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddSingleton(NullLoggerFactory.Instance); return services; } } diff --git a/src/Http/Http.Results/test/LocalRedirectResultTest.cs b/src/Http/Http.Results/test/LocalRedirectResultTest.cs index eff2f018bbf0..33e49752fefd 100644 --- a/src/Http/Http.Results/test/LocalRedirectResultTest.cs +++ b/src/Http/Http.Results/test/LocalRedirectResultTest.cs @@ -16,7 +16,7 @@ public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveM var url = "/test/url"; // Act - var result = new LocalRedirectResult(url); + var result = new RedirectHttpResult(url, acceptLocalUrlOnly: true, false, false); // Assert Assert.False(result.PreserveMethod); @@ -31,7 +31,7 @@ public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNo var url = "/test/url"; // Act - var result = new LocalRedirectResult(url, permanent: true); + var result = new RedirectHttpResult(url, acceptLocalUrlOnly: true, permanent: true, preserveMethod: false); // Assert Assert.False(result.PreserveMethod); @@ -46,7 +46,7 @@ public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlPermanentAndPr var url = "/test/url"; // Act - var result = new LocalRedirectResult(url, permanent: true, preserveMethod: true); + var result = new RedirectHttpResult(url, acceptLocalUrlOnly: true, permanent: true, preserveMethod: true); // Assert Assert.True(result.PreserveMethod); @@ -63,7 +63,7 @@ public async Task Execute_ReturnsExpectedValues() var expectedPath = "/Home/About"; var httpContext = GetHttpContext(appRoot); - var result = new LocalRedirectResult(contentPath); + var result = new RedirectHttpResult(contentPath, acceptLocalUrlOnly: true, false, false); // Act await result.ExecuteAsync(httpContext); @@ -86,7 +86,7 @@ public async Task Execute_Throws_ForNonLocalUrl( { // Arrange var httpContext = GetHttpContext(appRoot); - var result = new LocalRedirectResult(contentPath); + var result = new RedirectHttpResult(contentPath, acceptLocalUrlOnly: true, false, false); // Act & Assert var exception = await Assert.ThrowsAsync(() => result.ExecuteAsync(httpContext)); @@ -107,7 +107,7 @@ public async Task Execute_Throws_ForNonLocalUrlTilde( { // Arrange var httpContext = GetHttpContext(appRoot); - var result = new LocalRedirectResult(contentPath); + var result = new RedirectHttpResult(contentPath, acceptLocalUrlOnly: true, false, false); // Act & Assert var exception = await Assert.ThrowsAsync(() => result.ExecuteAsync(httpContext)); @@ -121,6 +121,7 @@ private static IServiceProvider GetServiceProvider() { var serviceCollection = new ServiceCollection(); serviceCollection.AddTransient(typeof(ILogger<>), typeof(NullLogger<>)); + serviceCollection.AddSingleton(NullLoggerFactory.Instance); return serviceCollection.BuildServiceProvider(); } diff --git a/src/Http/Http.Results/test/NoContentResultTests.cs b/src/Http/Http.Results/test/NoContentResultTests.cs new file mode 100644 index 000000000000..18b56774b6f5 --- /dev/null +++ b/src/Http/Http.Results/test/NoContentResultTests.cs @@ -0,0 +1,54 @@ +// 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; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +public class NoContentResultTests +{ + [Fact] + public void NoContentResultTests_InitializesStatusCode() + { + // Arrange & act + var result = new NoContentHttpResult(); + + // Assert + Assert.Equal(StatusCodes.Status204NoContent, result.StatusCode); + } + + [Fact] + public void NoContentResultTests_ExecuteResultSetsResponseStatusCode() + { + // Arrange + var result = new NoContentHttpResult(); + + var httpContext = GetHttpContext(); + + // Act + result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode); + } + + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; + } + + private static HttpContext GetHttpContext() + { + var services = CreateServices(); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services.BuildServiceProvider(); + + return httpContext; + } +} diff --git a/src/Http/Http.Results/test/NotFoundObjectResultTest.cs b/src/Http/Http.Results/test/NotFoundObjectResultTest.cs index 1286c69d7f6e..b9c4426e1b40 100644 --- a/src/Http/Http.Results/test/NotFoundObjectResultTest.cs +++ b/src/Http/Http.Results/test/NotFoundObjectResultTest.cs @@ -9,11 +9,24 @@ namespace Microsoft.AspNetCore.Http.Result; public class NotFoundObjectResultTest { + [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 NotFoundObjectResult(null); + var notFound = new NotFoundObjectHttpResult(null); // Assert Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); @@ -23,7 +36,7 @@ public void NotFoundObjectResult_InitializesStatusCode() public void NotFoundObjectResult_InitializesStatusCodeAndResponseContent() { // Arrange & act - var notFound = new NotFoundObjectResult("Test Content"); + var notFound = new NotFoundObjectHttpResult("Test Content"); // Assert Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); @@ -35,7 +48,7 @@ public async Task NotFoundObjectResult_ExecuteSuccessful() { // Arrange var httpContext = GetHttpContext(); - var result = new NotFoundObjectResult("Test Content"); + var result = new NotFoundObjectHttpResult("Test Content"); // Act await result.ExecuteAsync(httpContext); diff --git a/src/Http/Http.Results/test/ObjectResultTests.cs b/src/Http/Http.Results/test/ObjectResultTests.cs index 3c1f24107799..9e81802a9788 100644 --- a/src/Http/Http.Results/test/ObjectResultTests.cs +++ b/src/Http/Http.Results/test/ObjectResultTests.cs @@ -16,7 +16,7 @@ public class ObjectResultTests public async Task ObjectResult_ExecuteAsync_WithNullValue_Works() { // Arrange - var result = new ObjectResult(value: null, 411); + var result = new ObjectHttpResult(value: null, 411); var httpContext = new DefaultHttpContext() { @@ -34,7 +34,7 @@ public async Task ObjectResult_ExecuteAsync_WithNullValue_Works() public async Task ObjectResult_ExecuteAsync_SetsStatusCode() { // Arrange - var result = new ObjectResult("Hello", 407); + var result = new ObjectHttpResult("Hello", 407); var httpContext = new DefaultHttpContext() { @@ -52,7 +52,7 @@ public async Task ObjectResult_ExecuteAsync_SetsStatusCode() public async Task ObjectResult_ExecuteAsync_JsonSerializesBody() { // Arrange - var result = new ObjectResult("Hello", 407); + var result = new ObjectHttpResult("Hello", 407); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -76,7 +76,7 @@ public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() // Arrange var details = new ProblemDetails(); - var result = new ObjectResult(details); + var result = new ObjectHttpResult(details); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -105,7 +105,7 @@ public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails() // Arrange var details = new HttpValidationProblemDetails(); - var result = new ObjectResult(details); + var result = new ObjectHttpResult(details); var stream = new MemoryStream(); var httpContext = new DefaultHttpContext() { @@ -134,7 +134,7 @@ public async Task ExecuteAsync_SetsProblemDetailsStatus_ForValidationProblemDeta // Arrange var details = new HttpValidationProblemDetails(); - var result = new ObjectResult(details, StatusCodes.Status422UnprocessableEntity); + var result = new ObjectHttpResult(details, StatusCodes.Status422UnprocessableEntity); var httpContext = new DefaultHttpContext() { RequestServices = CreateServices(), @@ -153,7 +153,7 @@ public async Task ExecuteAsync_GetsStatusCodeFromProblemDetails() // Arrange var details = new ProblemDetails { Status = StatusCodes.Status413RequestEntityTooLarge, }; - var result = new ObjectResult(details); + var result = new ObjectHttpResult(details); var httpContext = new DefaultHttpContext() { @@ -165,7 +165,7 @@ public async Task ExecuteAsync_GetsStatusCodeFromProblemDetails() // Assert Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, details.Status.Value); - Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, result.StatusCode.Value); + Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, result.StatusCode); Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, httpContext.Response.StatusCode); } @@ -175,7 +175,7 @@ public async Task ExecuteAsync_UsesStatusCodeFromResultTypeForProblemDetails() // Arrange var details = new ProblemDetails { Status = StatusCodes.Status422UnprocessableEntity, }; - var result = new BadRequestObjectResult(details); + var result = new BadRequestObjectHttpResult(details); var httpContext = new DefaultHttpContext() { @@ -187,7 +187,7 @@ public async Task ExecuteAsync_UsesStatusCodeFromResultTypeForProblemDetails() // Assert Assert.Equal(StatusCodes.Status422UnprocessableEntity, details.Status.Value); - Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode.Value); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); } diff --git a/src/Http/Http.Results/test/OkObjectResultTest.cs b/src/Http/Http.Results/test/OkObjectResultTest.cs index 9a0e32b65f12..64d4d85a8a82 100644 --- a/src/Http/Http.Results/test/OkObjectResultTest.cs +++ b/src/Http/Http.Results/test/OkObjectResultTest.cs @@ -1,6 +1,7 @@ // 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; @@ -13,7 +14,7 @@ public class OkObjectResultTest public async Task OkObjectResult_SetsStatusCodeAndValue() { // Arrange - var result = new OkObjectResult("Hello world"); + var result = new OkObjectHttpResult("Hello world"); var httpContext = GetHttpContext(); // Act @@ -23,6 +24,41 @@ public async Task OkObjectResult_SetsStatusCodeAndValue() 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(); diff --git a/src/Http/Http.Results/test/PhysicalFileResultTest.cs b/src/Http/Http.Results/test/PhysicalFileResultTest.cs index 73c5eb9144b0..6ba0c2f0b64a 100644 --- a/src/Http/Http.Results/test/PhysicalFileResultTest.cs +++ b/src/Http/Http.Results/test/PhysicalFileResultTest.cs @@ -16,7 +16,7 @@ protected override Task ExecuteAsync( EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false) { - var fileResult = new PhysicalFileResult(path, contentType) + var fileResult = new PhysicalFileHttpResult(path, contentType) { LastModified = lastModified, EntityTag = entityTag, diff --git a/src/Http/Http.Results/test/ProblemResultTests.cs b/src/Http/Http.Results/test/ProblemResultTests.cs new file mode 100644 index 000000000000..28dfd0653cd8 --- /dev/null +++ b/src/Http/Http.Results/test/ProblemResultTests.cs @@ -0,0 +1,102 @@ +// 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.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Http.Result; + +public class ProblemResultTests +{ + [Fact] + public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() + { + // Arrange + var details = new ProblemDetails(); + + var result = new ProblemHttpResult(details); + var stream = new MemoryStream(); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + Response = + { + Body = stream, + }, + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status500InternalServerError, httpContext.Response.StatusCode); + stream.Position = 0; + var responseDetails = JsonSerializer.Deserialize(stream); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", responseDetails.Type); + Assert.Equal("An error occurred while processing your request.", responseDetails.Title); + Assert.Equal(StatusCodes.Status500InternalServerError, responseDetails.Status); + } + + [Fact] + public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails() + { + // Arrange + var details = new HttpValidationProblemDetails(); + + var result = new ProblemHttpResult(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); + 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 async Task ExecuteAsync_GetsStatusCodeFromProblemDetails() + { + // Arrange + var details = new ProblemDetails { Status = StatusCodes.Status413RequestEntityTooLarge, }; + + var result = new ProblemHttpResult(details); + + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, details.Status.Value); + Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, result.StatusCode); + Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, httpContext.Response.StatusCode); + } + + 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/PushStreamResultTest.cs b/src/Http/Http.Results/test/PushStreamResultTest.cs index 177f1feb578e..a4791775dc75 100644 --- a/src/Http/Http.Results/test/PushStreamResultTest.cs +++ b/src/Http/Http.Results/test/PushStreamResultTest.cs @@ -12,7 +12,7 @@ public class PushStreamResultTest [Fact] public async Task PushStreamResultsExposeTheResponseBody() { - var result = new PushStreamResult(body => body.WriteAsync(Encoding.UTF8.GetBytes("Hello World").AsMemory()).AsTask(), contentType: null); + var result = new PushStreamHttpResult(body => body.WriteAsync(Encoding.UTF8.GetBytes("Hello World").AsMemory()).AsTask(), contentType: null); var httpContext = new DefaultHttpContext { @@ -37,7 +37,7 @@ public void Constructor_SetsContentTypeAndParameters() var callback = (Stream body) => body.WriteAsync(Encoding.UTF8.GetBytes("Hello World").AsMemory()).AsTask(); // Act - var result = new PushStreamResult(callback, contentType); + var result = new PushStreamHttpResult(callback, contentType); // Assert Assert.Equal(expectedMediaType, result.ContentType); @@ -55,7 +55,7 @@ public void Constructor_SetsLastModifiedAndEtag() var callback = (Stream body) => body.WriteAsync(Encoding.UTF8.GetBytes("Hello World").AsMemory()).AsTask(); // Act - var result = new PushStreamResult(callback, contentType) + var result = new PushStreamHttpResult(callback, contentType) { LastModified = lastModified, EntityTag = entityTag, diff --git a/src/Http/Http.Results/test/RedirectResultTest.cs b/src/Http/Http.Results/test/RedirectResultTest.cs index b7ba5ba764d1..207e6c6850b9 100644 --- a/src/Http/Http.Results/test/RedirectResultTest.cs +++ b/src/Http/Http.Results/test/RedirectResultTest.cs @@ -14,7 +14,7 @@ public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMeth var url = "/test/url"; // Act - var result = new RedirectResult(url, permanent: true, preserveMethod: true); + var result = new RedirectHttpResult(url, permanent: true, preserveMethod: true); // Assert Assert.True(result.PreserveMethod); @@ -24,7 +24,7 @@ public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMeth protected override Task ExecuteAsync(HttpContext httpContext, string contentPath) { - var redirectResult = new RedirectResult(contentPath, false, false); + var redirectResult = new RedirectHttpResult(contentPath, false, false); return redirectResult.ExecuteAsync(httpContext); } } diff --git a/src/Http/Http.Results/test/RedirectToRouteResultTest.cs b/src/Http/Http.Results/test/RedirectToRouteResultTest.cs index 765df0ed7af8..99dcac7836dc 100644 --- a/src/Http/Http.Results/test/RedirectToRouteResultTest.cs +++ b/src/Http/Http.Results/test/RedirectToRouteResultTest.cs @@ -18,7 +18,7 @@ public async Task RedirectToRoute_Execute_ThrowsOnNullUrl() var httpContext = new DefaultHttpContext(); httpContext.RequestServices = CreateServices(null).BuildServiceProvider(); - var result = new RedirectToRouteResult(null, new Dictionary()); + var result = new RedirectToRouteHttpResult(null, new Dictionary()); // Act & Assert await ExceptionAssert.ThrowsAsync( @@ -38,7 +38,7 @@ public async Task ExecuteResultAsync_UsesRouteName_ToGenerateLocationHeader() var httpContext = GetHttpContext(locationUrl); - var result = new RedirectToRouteResult(routeName, new { id = 10 }); + var result = new RedirectToRouteHttpResult(routeName, new { id = 10 }); // Act await result.ExecuteAsync(httpContext); @@ -56,7 +56,7 @@ public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect( var expectedStatusCode = StatusCodes.Status301MovedPermanently; var httpContext = GetHttpContext(expectedUrl); - var result = new RedirectToRouteResult("Sample", null, true, "test"); + var result = new RedirectToRouteHttpResult("Sample", null, true, "test"); // Act await result.ExecuteAsync(httpContext); @@ -74,7 +74,7 @@ public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect_ var expectedStatusCode = StatusCodes.Status308PermanentRedirect; var httpContext = GetHttpContext(expectedUrl); - var result = new RedirectToRouteResult("Sample", null, true, true, "test"); + var result = new RedirectToRouteHttpResult("Sample", null, true, true, "test"); // Act await result.ExecuteAsync(httpContext); @@ -100,6 +100,7 @@ private static IServiceCollection CreateServices(string path) services.AddSingleton(new TestLinkGenerator { Url = path }); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddSingleton(NullLoggerFactory.Instance); return services; } } diff --git a/src/Http/Http.Results/test/SignInResultTest.cs b/src/Http/Http.Results/test/SignInResultTest.cs index dfcf42147c2d..6aef97223c54 100644 --- a/src/Http/Http.Results/test/SignInResultTest.cs +++ b/src/Http/Http.Results/test/SignInResultTest.cs @@ -24,7 +24,7 @@ public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManager() .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new SignInResult("", principal, null); + var result = new SignInHttpResult(principal, "", null); // Act await result.ExecuteAsync(httpContext); @@ -44,7 +44,7 @@ public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManagerWithDefa .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new SignInResult(principal); + var result = new SignInHttpResult(principal); // Act await result.ExecuteAsync(httpContext); @@ -65,7 +65,7 @@ public async Task ExecuteAsync_InvokesSignInAsyncOnConfiguredScheme() .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new SignInResult("Scheme1", principal, authProperties); + var result = new SignInHttpResult(principal, "Scheme1", authProperties); // Act await result.ExecuteAsync(httpContext); @@ -86,6 +86,7 @@ private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) private static IServiceCollection CreateServices() { var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); return services; } diff --git a/src/Http/Http.Results/test/SignOutResultTest.cs b/src/Http/Http.Results/test/SignOutResultTest.cs index 449bc867a439..d3cfa5073659 100644 --- a/src/Http/Http.Results/test/SignOutResultTest.cs +++ b/src/Http/Http.Results/test/SignOutResultTest.cs @@ -21,7 +21,7 @@ public async Task ExecuteAsync_NoArgsInvokesDefaultSignOut() .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new SignOutResult(); + var result = new SignOutHttpResult(); // Act await result.ExecuteAsync(httpContext); @@ -40,7 +40,7 @@ public async Task ExecuteAsync_InvokesSignOutAsyncOnAuthenticationManager() .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new SignOutResult("", null); + var result = new SignOutHttpResult("", null); // Act await result.ExecuteAsync(httpContext); @@ -64,7 +64,7 @@ public async Task ExecuteAsync_InvokesSignOutAsyncOnAllConfiguredSchemes() .Returns(Task.CompletedTask) .Verifiable(); var httpContext = GetHttpContext(auth.Object); - var result = new SignOutResult(new[] { "Scheme1", "Scheme2" }, authProperties); + var result = new SignOutHttpResult(new[] { "Scheme1", "Scheme2" }, authProperties); // Act await result.ExecuteAsync(httpContext); @@ -85,6 +85,7 @@ private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) private static IServiceCollection CreateServices() { var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); return services; } diff --git a/src/Http/Http.Results/test/StatusCodeResultTests.cs b/src/Http/Http.Results/test/StatusCodeResultTests.cs index 044712ae04a4..ae3493329307 100644 --- a/src/Http/Http.Results/test/StatusCodeResultTests.cs +++ b/src/Http/Http.Results/test/StatusCodeResultTests.cs @@ -13,7 +13,7 @@ public class StatusCodeResultTests public void StatusCodeResult_ExecuteResultSetsResponseStatusCode() { // Arrange - var result = new StatusCodeResult(StatusCodes.Status404NotFound); + var result = new StatusCodeHttpResult(StatusCodes.Status404NotFound); var httpContext = GetHttpContext(); @@ -28,6 +28,7 @@ private static IServiceCollection CreateServices() { var services = new ServiceCollection(); services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); return services; } diff --git a/src/Http/Http.Results/test/UnauthorizedResultTests.cs b/src/Http/Http.Results/test/UnauthorizedResultTests.cs index e9518adf6b04..50aab7fd7bf9 100644 --- a/src/Http/Http.Results/test/UnauthorizedResultTests.cs +++ b/src/Http/Http.Results/test/UnauthorizedResultTests.cs @@ -3,15 +3,52 @@ namespace Microsoft.AspNetCore.Http.Result; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + public class UnauthorizedResultTests { [Fact] public void UnauthorizedResult_InitializesStatusCode() { // Arrange & act - var result = new UnauthorizedResult(); + var result = new UnauthorizedHttpResult(); // Assert Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); } + + [Fact] + public void UnauthorizedResult_ExecuteResultSetsResponseStatusCode() + { + // Arrange + var result = new UnauthorizedHttpResult(); + + var httpContext = GetHttpContext(); + + // Act + result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status401Unauthorized, httpContext.Response.StatusCode); + } + + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; + } + + private static HttpContext GetHttpContext() + { + var services = CreateServices(); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services.BuildServiceProvider(); + + return httpContext; + } } diff --git a/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs index 6633e4f49ed9..f04410925e29 100644 --- a/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs +++ b/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs @@ -3,17 +3,81 @@ namespace Microsoft.AspNetCore.Http.Result; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + public class UnprocessableEntityObjectResultTests { + [Fact] + public void NotFoundObjectResult_ProblemDetails_SetsStatusCodeAndValue() + { + // Arrange & Act + var obj = new HttpValidationProblemDetails(); + var result = new UnprocessableEntityObjectHttpResult(obj); + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); + Assert.Equal(StatusCodes.Status422UnprocessableEntity, obj.Status); + Assert.Equal(obj, result.Value); + } + [Fact] public void UnprocessableEntityObjectResult_SetsStatusCodeAndValue() { // Arrange & Act var obj = new object(); - var result = new UnprocessableEntityObjectResult(obj); + var result = new UnprocessableEntityObjectHttpResult(obj); // Assert Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); Assert.Equal(obj, result.Value); } + + [Fact] + public async Task UnprocessableEntityObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new UnprocessableEntityObjectHttpResult("Hello"); + var httpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, httpContext.Response.StatusCode); + } + + [Fact] + public async Task UnprocessableEntityObjectResult_ExecuteResultAsync_FormatsData() + { + // Arrange + var result = new UnprocessableEntityObjectHttpResult("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 IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } } diff --git a/src/Http/Http.Results/test/VirtualFileResultTest.cs b/src/Http/Http.Results/test/VirtualFileResultTest.cs index 8d73b5f82cc7..d910676c5365 100644 --- a/src/Http/Http.Results/test/VirtualFileResultTest.cs +++ b/src/Http/Http.Results/test/VirtualFileResultTest.cs @@ -10,7 +10,7 @@ public class VirtualFileResultTest : VirtualFileResultTestBase { protected override Task ExecuteAsync(HttpContext httpContext, string path, string contentType, DateTimeOffset? lastModified = null, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false) { - var result = new VirtualFileResult(path, contentType) + var result = new VirtualFileHttpResult(path, contentType) { LastModified = lastModified, EntityTag = entityTag,