From 15126b5ade66cbcb1a04b9534972ac322a640ef2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 11:41:57 -0700 Subject: [PATCH 01/59] MVC Changes --- .../ApiBehaviorOptionsSetup.cs | 2 +- .../MvcCoreProblemDetailsOptionsSetup.cs | 42 ++++++++++ .../MvcCoreServiceCollectionExtensions.cs | 8 +- .../src/Formatters/TextOutputFormatter.cs | 12 ++- .../DefaultHttpProblemDetailsFactory.cs | 80 +++++++++++++++++++ .../Infrastructure/ObjectResultExecutor.cs | 11 ++- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 3 - .../MvcCoreServiceCollectionExtensionsTest.cs | 8 ++ .../Formatters/TextOutputFormatterTests.cs | 3 +- .../ObjectResultExecutorTest.cs | 4 +- src/Mvc/Mvc.slnf | 3 +- 11 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs create mode 100644 src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs index ffc39f12a92e..38c956123523 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Options; diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs new file mode 100644 index 000000000000..5b96485f1d05 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs @@ -0,0 +1,42 @@ +// 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; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Sets up MVC default options for . +/// +internal sealed class MvcCoreProblemDetailsOptionsSetup : IConfigureOptions +{ + private readonly ApiBehaviorOptions _apiBehaviorOptions; + + public MvcCoreProblemDetailsOptionsSetup(IOptions options) + { + _apiBehaviorOptions = options.Value; + } + + /// + /// Configures the . + /// + /// The . + public void Configure(ProblemDetailsOptions options) + { + ArgumentNullException.ThrowIfNull(nameof(options)); + + options.SuppressMapClientErrors = _apiBehaviorOptions.SuppressMapClientErrors; + options.SuppressMapExceptions = _apiBehaviorOptions.SuppressMapClientErrors; + + foreach (var item in _apiBehaviorOptions.ClientErrorMapping) + { + options.ProblemDetailsErrorMapping[item.Key] = new() + { + Title = item.Value.Title, + Link = item.Value.Link + }; + } + } +} diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 7aaa42d5afc4..05215c92d400 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Linq; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; @@ -148,6 +149,8 @@ internal static void AddMvcCoreServices(IServiceCollection services) ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreRouteOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcCoreProblemDetailsOptionsSetup>()); // // Action Discovery @@ -254,7 +257,6 @@ internal static void AddMvcCoreServices(IServiceCollection services) services.TryAddSingleton, ContentResultExecutor>(); services.TryAddSingleton, SystemTextJsonResultExecutor>(); services.TryAddSingleton(); - services.TryAddSingleton(); // // Route Handlers @@ -281,6 +283,10 @@ internal static void AddMvcCoreServices(IServiceCollection services) services.TryAddSingleton(); // Sets ApplicationBuilder on MiddlewareFilterBuilder services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + // ProblemDetails + services.TryAddSingleton(); + services.TryAddSingleton(); } private static void ConfigureDefaultServices(IServiceCollection services) diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index 4bca0413db8a..0c3f96fd7bb1 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -132,8 +133,15 @@ public override Task WriteAsync(OutputFormatterWriteContext context) } else { - var response = context.HttpContext.Response; - response.StatusCode = StatusCodes.Status406NotAcceptable; + const int statusCode = StatusCodes.Status406NotAcceptable; + + var endpointProvider = context.HttpContext.RequestServices.GetService(); + if (endpointProvider != null && endpointProvider.CanWrite(statusCode)) + { + return endpointProvider.WriteResponse(context.HttpContext, statusCode); + } + + context.HttpContext.Response.StatusCode = statusCode; return Task.CompletedTask; } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs new file mode 100644 index 000000000000..2334fbc26c94 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.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. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Core.Infrastructure; + +internal class DefaultHttpProblemDetailsFactory : IHttpProblemDetailsFactory +{ + private readonly OutputFormatterSelector _formatterSelector; + private readonly IHttpResponseStreamWriterFactory _writerFactory; + + public DefaultHttpProblemDetailsFactory( + OutputFormatterSelector formatterSelector, + IHttpResponseStreamWriterFactory writerFactory) + { + _formatterSelector = formatterSelector; + _writerFactory = writerFactory; + } + + public ProblemDetails CreateProblemDetails( + HttpContext httpContext, + ProblemDetailsOptions options, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null) + { + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance, + }; + + if (extensions is not null) + { + foreach (var extension in extensions) + { + problemDetails.Extensions.Add(extension); + } + } + + ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, options.ProblemDetailsErrorMapping); + + return problemDetails; + } + + public Task WriteAsync(HttpContext context, ProblemDetails problemDetails) + { + var contentTypes = new MediaTypeCollection() + { + "application/problem+json", + "application/problem+xml" + }; + + var formatterContext = new OutputFormatterWriteContext( + context, + _writerFactory.CreateWriter, + typeof(ProblemDetails), + problemDetails); + + var selectedFormatter = _formatterSelector.SelectFormatter( + formatterContext, + Array.Empty(), + contentTypes); + if (selectedFormatter == null) + { + return Task.CompletedTask; + } + + return selectedFormatter.WriteAsync(formatterContext); + } +} diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 1c4ddb95ab88..37cf86f2f566 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -6,6 +6,7 @@ using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -114,7 +115,15 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? // No formatter supports this. Log.NoFormatter(Logger, formatterContext, result.ContentTypes); - context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable; + const int statusCode = StatusCodes.Status406NotAcceptable; + + var endpointProvider = context.HttpContext.RequestServices.GetService(); + if (endpointProvider != null && endpointProvider.CanWrite(statusCode)) + { + return endpointProvider.WriteResponse(context.HttpContext, statusCode); + } + + context.HttpContext.Response.StatusCode = statusCode; return Task.CompletedTask; } diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 9d9b2ac58e91..0e07e4447087 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -28,9 +28,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - - - diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index b3eb866ea841..7b566c8b556f 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -251,6 +252,13 @@ private Dictionary MultiRegistrationServiceTypes typeof(MvcCoreRouteOptionsSetup), } }, + { + typeof(IConfigureOptions), + new Type[] + { + typeof(MvcCoreProblemDetailsOptionsSetup), + } + }, { typeof(IConfigureOptions), new Type[] diff --git a/src/Mvc/Mvc.Core/test/Formatters/TextOutputFormatterTests.cs b/src/Mvc/Mvc.Core/test/Formatters/TextOutputFormatterTests.cs index 426e286cb886..fb4b1b34484f 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/TextOutputFormatterTests.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/TextOutputFormatterTests.cs @@ -3,6 +3,7 @@ using System.Text; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; @@ -215,7 +216,7 @@ public async Task WriteAsync_ReturnsNotAcceptable_IfSelectCharacterEncodingRetur formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json")); var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), + new DefaultHttpContext() { RequestServices = new ServiceCollection().BuildServiceProvider() }, new TestHttpResponseStreamWriterFactory().CreateWriter, objectType: null, @object: null) diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ObjectResultExecutorTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ObjectResultExecutorTest.cs index 8850174cf95d..be2724e73f40 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ObjectResultExecutorTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ObjectResultExecutorTest.cs @@ -94,7 +94,7 @@ public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentTyp // Arrange var executor = CreateExecutor(); - var httpContext = new DefaultHttpContext(); + var httpContext = GetHttpContext(); var actionContext = new ActionContext() { HttpContext = httpContext }; httpContext.Request.Headers.Accept = "application/xml"; // This will not be used httpContext.Response.ContentType = "application/json"; @@ -258,7 +258,7 @@ public async Task ExecuteAsync_NoFormatterFound_Returns406() var actionContext = new ActionContext() { - HttpContext = new DefaultHttpContext(), + HttpContext = GetHttpContext(), }; var result = new ObjectResult("input"); diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index 1dee0bc428eb..e0648cee7ef0 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -29,6 +29,7 @@ "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", + "src\\Http\\Http.ProblemDetails\\src\\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", @@ -147,4 +148,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} From 00e761513b6a562609631444bd72f6c0c3fa555a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 11:42:24 -0700 Subject: [PATCH 02/59] ExceptionHandler changes --- .../ExceptionHandlerExtensions.cs | 3 +- .../ExceptionHandlerMiddleware.cs | 40 ++++++++++++++++++- .../ExceptionHandlerOptions.cs | 6 +++ .../Microsoft.AspNetCore.Diagnostics.csproj | 1 + .../Diagnostics/src/PublicAPI.Unshipped.txt | 5 ++- src/Middleware/Diagnostics/src/Resources.resx | 4 +- 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index 602b1bc2172d..cca9787cd118 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -131,7 +131,8 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui options.Value.ExceptionHandler = builder.Build(); } - return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener).Invoke; + var provider = app.ApplicationServices.GetService(); + return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, provider).Invoke; }); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index d7dd4ff90e1d..976418ae728d 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -22,6 +24,7 @@ public class ExceptionHandlerMiddleware private readonly ILogger _logger; private readonly Func _clearCacheHeadersDelegate; private readonly DiagnosticListener _diagnosticListener; + private readonly ProblemDetailsEndpointProvider? _problemDetailsEndpointProvider; /// /// Creates a new @@ -30,22 +33,30 @@ public class ExceptionHandlerMiddleware /// The used for logging. /// The options for configuring the middleware. /// The used for writing diagnostic messages. + /// public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions options, - DiagnosticListener diagnosticListener) + DiagnosticListener diagnosticListener, + ProblemDetailsEndpointProvider? problemDetailsEndpointProvider = null) { _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger(); _clearCacheHeadersDelegate = ClearCacheHeaders; _diagnosticListener = diagnosticListener; + _problemDetailsEndpointProvider = problemDetailsEndpointProvider; if (_options.ExceptionHandler == null) { if (_options.ExceptionHandlingPath == null) { - throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); + if (_problemDetailsEndpointProvider == null) + { + throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); + } + + _options.ExceptionHandler = CreatProblemDetailsHandler(); } else { @@ -190,4 +201,29 @@ private static Task ClearCacheHeaders(object state) headers.ETag = default; return Task.CompletedTask; } + + private RequestDelegate CreatProblemDetailsHandler() + { + return _problemDetailsEndpointProvider!.CreateRequestDelegate( + StatusCodes.Status500InternalServerError, + configureDetails: (context, problemDetails) => + { + var exceptionFeature = context.Features.GetRequiredFeature(); + var hostEnvironment = context.RequestServices.GetRequiredService(); + + if (hostEnvironment.IsDevelopment()) + { + problemDetails.Detail = exceptionFeature.Error.Message; + problemDetails.Extensions.Add("exception", new + { + Details = exceptionFeature.Error.ToString(), + exceptionFeature.Path, + exceptionFeature.RouteValues, + Endpoint = exceptionFeature.Endpoint?.ToString() + }); + } + + _options.ConfigureDetails?.Invoke(exceptionFeature, problemDetails); + }); + } } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs index d776813f29d1..88f654701d15 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; namespace Microsoft.AspNetCore.Builder; @@ -31,4 +32,9 @@ public class ExceptionHandlerOptions /// the original exception. /// public bool AllowStatusCode404Response { get; set; } + + /// + /// + /// + public Action? ConfigureDetails { get; set; } } diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index ebf506b0a0ae..584056b42a44 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index a8796b96379e..f2042aff8ca1 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.ConfigureDetails.get -> System.Action? +Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.ConfigureDetails.set -> void +Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider? problemDetailsEndpointProvider = null) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? @@ -8,6 +11,6 @@ Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.set -> v *REMOVED*~Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.StatusCodePagesMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void *REMOVED*~Microsoft.AspNetCore.Diagnostics.WelcomePageMiddleware.WelcomePageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters) -> void -Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener) -> void Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.StatusCodePagesMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.Diagnostics.WelcomePageMiddleware.WelcomePageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void +static Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Action? configureDetails = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! diff --git a/src/Middleware/Diagnostics/src/Resources.resx b/src/Middleware/Diagnostics/src/Resources.resx index 6e62f494942e..608af43ce852 100644 --- a/src/Middleware/Diagnostics/src/Resources.resx +++ b/src/Middleware/Diagnostics/src/Resources.resx @@ -248,7 +248,7 @@ Environment: - An error occurred when configuring the exception handler middleware. Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'. Alternatively, set one of the aforementioned properties in 'Startup.ConfigureServices' as follows: 'services.AddExceptionHandler(options => { ... });'. + An error occurred when configuring the exception handler middleware. Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'. Alternatively, set one of the aforementioned properties in 'Startup.ConfigureServices' as follows: 'services.AddExceptionHandler(options => { ... });' or configure to generate a 'ProblemDetails' response in 'service.AddProblemDetails(options => options.SuppressMapExceptions = false)'. No route values. @@ -280,4 +280,4 @@ Name - + \ No newline at end of file From 9adc2691fbfabdcadf4b154e40824da8d2bedcc9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 11:43:08 -0700 Subject: [PATCH 03/59] Routing changes --- src/Http/Http/src/Builder/ApplicationBuilder.cs | 12 +++++++++++- src/Http/Http/src/Microsoft.AspNetCore.Http.csproj | 1 + .../Routing/src/Matching/AcceptsMatcherPolicy.cs | 14 ++++++++++++-- .../src/Matching/HttpMethodMatcherPolicy.cs | 13 +++++++++++-- .../src/Microsoft.AspNetCore.Routing.csproj | 1 + 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index 9e6298af839c..e8addd712fe2 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Builder; @@ -129,7 +130,16 @@ public RequestDelegate Build() throw new InvalidOperationException(message); } - context.Response.StatusCode = StatusCodes.Status404NotFound; + const int statusCode = StatusCodes.Status404NotFound; + + var endpointProvider = context.RequestServices.GetService(); + if (endpointProvider != null && + endpointProvider.CanWrite(statusCode, isRouting: true)) + { + return endpointProvider.WriteResponse(context, statusCode); + } + + context.Response.StatusCode = statusCode; return Task.CompletedTask; }; diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index e5e1ce15a3d9..48a4fc03c2af 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 67c032148016..5d58332b19cd 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Routing.Matching; @@ -259,9 +260,18 @@ public IReadOnlyList GetEdges(IReadOnlyList endpoints) private static Endpoint CreateRejectionEndpoint() { return new Endpoint( - (context) => + context => { - context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; + const int statusCode = StatusCodes.Status415UnsupportedMediaType; + + var endpointProvider = context.RequestServices.GetService(); + if (endpointProvider != null && + endpointProvider.CanWrite(statusCode, isRouting: true)) + { + return endpointProvider.WriteResponse(context, statusCode); + } + + context.Response.StatusCode = statusCode; return Task.CompletedTask; }, EndpointMetadataCollection.Empty, diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index cc89d26a26a9..71757dd1d184 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -404,12 +405,20 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods return new Endpoint( (context) => { - context.Response.StatusCode = 405; - // Prevent ArgumentException from duplicate key if header already added, such as when the // request is re-executed by an error handler (see https://github.com/dotnet/aspnetcore/issues/6415) context.Response.Headers.Allow = allow; + const int statusCode = StatusCodes.Status405MethodNotAllowed; + + var endpointProvider = context.RequestServices.GetService(); + if (endpointProvider != null && + endpointProvider.CanWrite(statusCode, isRouting: true)) + { + return endpointProvider.WriteResponse(context, statusCode); + } + + context.Response.StatusCode = statusCode; return Task.CompletedTask; }, EndpointMetadataCollection.Empty, diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 0e1f47d2ab67..1b0d89ac2852 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -37,6 +37,7 @@ + From c430ae2dd91dbe8801ef387f9c880ca6edaa424a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 11:44:24 -0700 Subject: [PATCH 04/59] Http.Extensions changes --- .../src/HttpValidationProblemDetails.cs | 42 ------------- ...icrosoft.AspNetCore.Http.Extensions.csproj | 3 +- .../Http.Extensions/src/ProblemDetails.cs | 63 ------------------- .../src/Properties/AssemblyInfo.cs | 3 + 4 files changed, 4 insertions(+), 107 deletions(-) delete mode 100644 src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs delete mode 100644 src/Http/Http.Extensions/src/ProblemDetails.cs diff --git a/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs b/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs deleted file mode 100644 index 12ccd028babf..000000000000 --- a/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs +++ /dev/null @@ -1,42 +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.Serialization; -using Microsoft.AspNetCore.Mvc; - -namespace Microsoft.AspNetCore.Http; - -/// -/// A for validation errors. -/// -[JsonConverter(typeof(HttpValidationProblemDetailsJsonConverter))] -public class HttpValidationProblemDetails : ProblemDetails -{ - /// - /// Initializes a new instance of . - /// - public HttpValidationProblemDetails() - : this(new Dictionary(StringComparer.Ordinal)) - { - } - - /// - /// Initializes a new instance of using the specified . - /// - /// The validation errors. - public HttpValidationProblemDetails(IDictionary errors) - : this(new Dictionary(errors ?? throw new ArgumentNullException(nameof(errors)), StringComparer.Ordinal)) - { - } - - private HttpValidationProblemDetails(Dictionary errors) - { - Title = "One or more validation errors occurred."; - Errors = errors; - } - - /// - /// Gets the validation errors associated with this instance of . - /// - public IDictionary Errors { get; } = new Dictionary(StringComparer.Ordinal); -} diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index ba4e362f4db3..50d916062618 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -15,14 +15,13 @@ - - + diff --git a/src/Http/Http.Extensions/src/ProblemDetails.cs b/src/Http/Http.Extensions/src/ProblemDetails.cs deleted file mode 100644 index 2d01289cdf19..000000000000 --- a/src/Http/Http.Extensions/src/ProblemDetails.cs +++ /dev/null @@ -1,63 +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.Serialization; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Mvc; - -/// -/// A machine-readable format for specifying errors in HTTP API responses based on . -/// -[JsonConverter(typeof(ProblemDetailsJsonConverter))] -public class ProblemDetails -{ - /// - /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when - /// dereferenced, it provide human-readable documentation for the problem type - /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be - /// "about:blank". - /// - [JsonPropertyName("type")] - public string? Type { get; set; } - - /// - /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence - /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; - /// see[RFC7231], Section 3.4). - /// - [JsonPropertyName("title")] - public string? Title { get; set; } - - /// - /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. - /// - [JsonPropertyName("status")] - public int? Status { get; set; } - - /// - /// A human-readable explanation specific to this occurrence of the problem. - /// - [JsonPropertyName("detail")] - public string? Detail { get; set; } - - /// - /// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. - /// - [JsonPropertyName("instance")] - public string? Instance { get; set; } - - /// - /// Gets the for extension members. - /// - /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as - /// other members of a problem type. - /// - /// - /// - /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. - /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. - /// - [JsonExtensionData] - public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); -} diff --git a/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs b/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs index f7cd6ab89feb..2122b7773ae1 100644 --- a/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs +++ b/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs @@ -3,4 +3,7 @@ using System.Runtime.CompilerServices; +[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ProblemDetails))] +[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Http.HttpValidationProblemDetails))] + [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] From a8939e5dc20571eca08b8c4303d12e637612006f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 11:44:45 -0700 Subject: [PATCH 05/59] Minimal APi draft changes --- .../Http.Extensions/src/RequestDelegateFactory.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 68f2ea3a8586..ff50cb4d901f 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -56,6 +56,7 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo ValueTaskOfTToValueTaskOfObjectMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ValueTaskOfTToValueTaskOfObject), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo CreateProblemDetailsMethod = typeof(RequestDelegateFactory).GetMethod(nameof(CreateProblemDetails), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ArrayEmptyMethod = typeof(Array).GetMethod(nameof(Array.Empty), BindingFlags.Public | BindingFlags.Static)!; // Call WriteAsJsonAsync() to serialize the runtime return type rather than the declared return type. @@ -107,6 +108,17 @@ public static partial class RequestDelegateFactory private static readonly string[] DefaultAcceptsContentType = new[] { "application/json" }; private static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + private static Task CreateProblemDetails(HttpContext context) + { + var endpointProvider = context.RequestServices.GetService(); + if (endpointProvider != null && endpointProvider.CanWrite(context.Response.StatusCode)) + { + return endpointProvider.WriteResponse(context, context.Response.StatusCode); + } + + return Task.CompletedTask; + } + /// /// Creates a implementation for . /// @@ -802,7 +814,7 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(Type retu WasParamCheckFailureExpr, Expression.Block( Expression.Assign(StatusCodeExpr, Expression.Constant(400)), - CompletedTaskExpr), + Expression.Call(CreateProblemDetailsMethod, HttpContextExpr)), AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType)); checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure; } From 79690d802bfffe01dd0d77fd7f8cec02b95607e9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 11:45:10 -0700 Subject: [PATCH 06/59] HttpResults changes --- src/Http/Http.Results/src/HttpResultsHelper.cs | 1 - .../Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index b899467b1d80..21134618f897 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -3,7 +3,6 @@ using System.Text; using System.Text.Json; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index de0934e22002..a097f859d585 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -16,7 +16,6 @@ - @@ -25,6 +24,7 @@ + From c548e2b8b36662859ef5b53c4d20641ac9f6867b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 11:45:45 -0700 Subject: [PATCH 07/59] New ProblemDetails project --- AspNetCore.sln | 27 ++++++ eng/ProjectReferences.props | 1 + eng/SharedFramework.Local.props | 1 + eng/TrimmableProjects.props | 1 + .../src/DefaultProblemDetailsFactory.cs | 55 +++++++++++ .../ProblemDetailsOptionsSetup.cs | 29 ++++++ ...oblemDetailsServiceCollectionExtensions.cs | 62 ++++++++++++ .../src/HttpValidationProblemDetails.cs | 42 ++++++++ .../src/IProblemDetailsFactory.cs | 44 +++++++++ ...soft.AspNetCore.Http.ProblemDetails.csproj | 37 +++++++ .../Http.ProblemDetails/src/ProblemDetails.cs | 63 ++++++++++++ .../src/ProblemDetailsEndpointProvider.cs | 96 +++++++++++++++++++ .../src/ProblemDetailsErrorData.cs | 30 ++++++ .../src/ProblemDetailsOptions.cs | 27 ++++++ .../src/PublicAPI.Shipped.txt | 1 + .../src/PublicAPI.Unshipped.txt | 34 +++++++ src/Shared/ProblemDetailsDefaults.cs | 40 +++++++- 17 files changed, 589 insertions(+), 1 deletion(-) create mode 100644 src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs create mode 100644 src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.cs create mode 100644 src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs create mode 100644 src/Http/Http.ProblemDetails/src/HttpValidationProblemDetails.cs create mode 100644 src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs create mode 100644 src/Http/Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj create mode 100644 src/Http/Http.ProblemDetails/src/ProblemDetails.cs create mode 100644 src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs create mode 100644 src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs create mode 100644 src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs create mode 100644 src/Http/Http.ProblemDetails/src/PublicAPI.Shipped.txt create mode 100644 src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt diff --git a/AspNetCore.sln b/AspNetCore.sln index 88ed078aec9e..35270705be85 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1710,6 +1710,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.ProblemDetails", "Http.ProblemDetails", "{41AF137D-4181-42F9-9B53-BEDB9532F29B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.ProblemDetails", "src\Http\Http.ProblemDetails\src\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "{2333E682-F9E9-4235-BF63-6403C114EA76}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7F014D2B-CC88-4BBE-8A0E-04FCF490F855}" + ProjectSection(SolutionItems) = preProject + Graph1.dgml = Graph1.dgml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10247,6 +10256,22 @@ Global {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x64.Build.0 = Release|Any CPU {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.ActiveCfg = Release|Any CPU {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.Build.0 = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|arm64.ActiveCfg = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|arm64.Build.0 = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x64.ActiveCfg = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x64.Build.0 = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x86.ActiveCfg = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x86.Build.0 = Debug|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|Any CPU.Build.0 = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|arm64.ActiveCfg = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|arm64.Build.0 = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x64.ActiveCfg = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x64.Build.0 = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.ActiveCfg = Release|Any CPU + {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11094,6 +11119,8 @@ Global {51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F} {487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088} {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} + {41AF137D-4181-42F9-9B53-BEDB9532F29B} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724} + {2333E682-F9E9-4235-BF63-6403C114EA76} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index d59faa1b0daa..e642ca47d67a 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -28,6 +28,7 @@ + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index f32bf007b8a4..0e9f8d2518be 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -48,6 +48,7 @@ + diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index 5f86c590f41a..fc92654ec2b4 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -24,6 +24,7 @@ + diff --git a/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs b/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs new file mode 100644 index 000000000000..17b7f4f9d5eb --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.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. + +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultHttpProblemDetailsFactory : IHttpProblemDetailsFactory +{ + public ProblemDetails CreateProblemDetails( + HttpContext httpContext, + ProblemDetailsOptions options, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null) + { + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance, + }; + + if (extensions is not null) + { + foreach (var extension in extensions) + { + problemDetails.Extensions.Add(extension); + } + } + + ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, options.ProblemDetailsErrorMapping); + + return problemDetails; + } + + public async Task WriteAsync( + HttpContext context, + ProblemDetails problemDetails) + { + //TODO: JsonOptions??? CancellationToken?? + try + { + context.Response.ContentType = "application/problem+json"; + await JsonSerializer.SerializeAsync(context.Response.Body, problemDetails, typeof(ProblemDetails), cancellationToken: context.RequestAborted); + } + catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) { } + } +} diff --git a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.cs b/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.cs new file mode 100644 index 000000000000..e5bb81c0b2a7 --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.cs @@ -0,0 +1,29 @@ +// 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; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class ProblemDetailsOptionsSetup : IConfigureOptions +{ + public void Configure(ProblemDetailsOptions options) + { + ArgumentNullException.ThrowIfNull(nameof(options)); + ConfigureProblemDetailsErrorMapping(options); + } + + // Internal for unit testing + internal static void ConfigureProblemDetailsErrorMapping(ProblemDetailsOptions options) + { + foreach (var (statusCode, value) in ProblemDetailsDefaults.Defaults) + { + options.ProblemDetailsErrorMapping[statusCode] = new() + { + Link = value.Type, + Title = value.Title, + }; + } + } +} diff --git a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs new file mode 100644 index 000000000000..48feb89da680 --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs @@ -0,0 +1,62 @@ +// 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; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Contains extension methods to . +/// +public static class ProblemDetailsServiceCollectionExtensions +{ + /// + /// Adds services required for creation of for failed requests. + /// + /// The to add the services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddProblemDetails( + this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(nameof(services)); + + // Adding default services + services.TryAddSingleton(); + services.TryAddSingleton(s => + { + var options = s.GetRequiredService>().Value; + var factory = s.GetRequiredService(); + return new ProblemDetailsEndpointProvider(options, factory); + }); + + // Adding options configurations + services.TryAddEnumerable( + ServiceDescriptor.Transient, ProblemDetailsOptionsSetup>()); + + return services; + } + + /// + /// Adds services required for creation of for failed requests. + /// + /// The to add the services to. + /// The routing options to configure the middleware with. + /// The so that additional calls can be chained. + public static IServiceCollection AddProblemDetails( + this IServiceCollection services, + Action configureOptions) + { + ArgumentNullException.ThrowIfNull(nameof(services)); + ArgumentNullException.ThrowIfNull(nameof(configureOptions)); + + // Adding default services + services.AddProblemDetails(); + + services.Configure(configureOptions); + + return services; + } +} diff --git a/src/Http/Http.ProblemDetails/src/HttpValidationProblemDetails.cs b/src/Http/Http.ProblemDetails/src/HttpValidationProblemDetails.cs new file mode 100644 index 000000000000..12ccd028babf --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/HttpValidationProblemDetails.cs @@ -0,0 +1,42 @@ +// 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.Serialization; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.AspNetCore.Http; + +/// +/// A for validation errors. +/// +[JsonConverter(typeof(HttpValidationProblemDetailsJsonConverter))] +public class HttpValidationProblemDetails : ProblemDetails +{ + /// + /// Initializes a new instance of . + /// + public HttpValidationProblemDetails() + : this(new Dictionary(StringComparer.Ordinal)) + { + } + + /// + /// Initializes a new instance of using the specified . + /// + /// The validation errors. + public HttpValidationProblemDetails(IDictionary errors) + : this(new Dictionary(errors ?? throw new ArgumentNullException(nameof(errors)), StringComparer.Ordinal)) + { + } + + private HttpValidationProblemDetails(Dictionary errors) + { + Title = "One or more validation errors occurred."; + Errors = errors; + } + + /// + /// Gets the validation errors associated with this instance of . + /// + public IDictionary Errors { get; } = new Dictionary(StringComparer.Ordinal); +} diff --git a/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs b/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs new file mode 100644 index 000000000000..7e51a0baf74b --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs @@ -0,0 +1,44 @@ +// 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; + +/// +/// +/// +internal interface IHttpProblemDetailsFactory +{ + /// + /// + /// + /// + /// + /// + Task WriteAsync( + HttpContext context, + ProblemDetails problemDetails); + + /// + /// Creates a instance that configures defaults based on values specified in . + /// + /// The . + /// + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The instance. + ProblemDetails CreateProblemDetails( + HttpContext httpContext, + ProblemDetailsOptions options, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null); +} diff --git a/src/Http/Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj new file mode 100644 index 000000000000..5949a2f4c4da --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj @@ -0,0 +1,37 @@ + + + + ASP.NET Core. + $(DefaultNetCoreTargetFramework) + true + true + aspnetcore + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetails.cs b/src/Http/Http.ProblemDetails/src/ProblemDetails.cs new file mode 100644 index 000000000000..2d01289cdf19 --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/ProblemDetails.cs @@ -0,0 +1,63 @@ +// 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.Serialization; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc; + +/// +/// A machine-readable format for specifying errors in HTTP API responses based on . +/// +[JsonConverter(typeof(ProblemDetailsJsonConverter))] +public class ProblemDetails +{ + /// + /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the problem type + /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + /// "about:blank". + /// + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence + /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; + /// see[RFC7231], Section 3.4). + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + /// + [JsonPropertyName("status")] + public int? Status { get; set; } + + /// + /// A human-readable explanation specific to this occurrence of the problem. + /// + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// + /// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. + /// + [JsonPropertyName("instance")] + public string? Instance { get; set; } + + /// + /// Gets the for extension members. + /// + /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as + /// other members of a problem type. + /// + /// + /// + /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. + /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. + /// + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); +} diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs new file mode 100644 index 000000000000..72f79d661fc4 --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs @@ -0,0 +1,96 @@ +// 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.Mvc; + +namespace Microsoft.AspNetCore.Http; + +/// +/// +/// +public sealed class ProblemDetailsEndpointProvider +{ + private readonly ProblemDetailsOptions _options; + private readonly IHttpProblemDetailsFactory _factory; + + internal ProblemDetailsEndpointProvider( + ProblemDetailsOptions options, + IHttpProblemDetailsFactory factory) + { + _options = options; + _factory = factory; + } + + /// + /// + /// + /// + /// + /// + public RequestDelegate CreateRequestDelegate( + int defaultStatusCode, + Action? configureDetails = null) + { + return (HttpContext context) => + { + context.Response.StatusCode = defaultStatusCode; + + if (CanWrite(defaultStatusCode)) + { + var details = _factory.CreateProblemDetails(context, _options, statusCode: context.Response.StatusCode); + configureDetails?.Invoke(context, details); + + return _factory.WriteAsync(context, details); + } + + return Task.CompletedTask; + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Task WriteResponse( + HttpContext context, + int statusCode, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null) + { + context.Response.StatusCode = statusCode; + var details = _factory.CreateProblemDetails( + context, + _options, + statusCode: context.Response.StatusCode, + title, + type, + detail, + instance, + extensions); + return _factory.WriteAsync(context, details); + } + + /// + /// + /// + /// + /// + /// + public bool CanWrite(int statusCode, bool isRouting = false) + => isRouting ? !_options.SuppressMapRoutingErrors : statusCode switch + { + (>= 400) and (<= 499) => !_options.SuppressMapClientErrors, + (>= 500) => !_options.SuppressMapExceptions, + _ => false, + }; +} diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs new file mode 100644 index 000000000000..9aa90e6234e6 --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Information for producing client errors. This type is used to configure client errors +/// produced by consumers of . +/// +internal class ProblemDetailsErrorData +{ + /// + /// Gets or sets a link (URI) that describes the client error. + /// + /// + /// By default, this maps to . + /// + public string? Link { get; set; } + + /// + /// Gets or sets the summary of the client error. + /// + /// + /// By default, this maps to and should not change + /// between multiple occurrences of the same error. + /// + public string? Title { get; set; } +} diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs new file mode 100644 index 000000000000..fa098f88b0cf --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http; + +/// +/// +/// +public class ProblemDetailsOptions +{ + /// + /// + /// + public bool SuppressMapRoutingErrors { get; set; } = true; + + /// + /// + /// + public bool SuppressMapClientErrors { get; set; } = true; + + /// + /// + /// + public bool SuppressMapExceptions { get; set; } = true; + + internal Dictionary ProblemDetailsErrorMapping { get; } = new Dictionary(); +} diff --git a/src/Http/Http.ProblemDetails/src/PublicAPI.Shipped.txt b/src/Http/Http.ProblemDetails/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..7dc5c58110bf --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..562b37ea0551 --- /dev/null +++ b/src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt @@ -0,0 +1,34 @@ +#nullable enable +Microsoft.AspNetCore.Http.HttpValidationProblemDetails +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void +Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider +Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.CanWrite(int statusCode, bool isRouting = false) -> bool +Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.CreateRequestDelegate(int defaultStatusCode, System.Action? configureDetails = null) -> Microsoft.AspNetCore.Http.RequestDelegate! +Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.CreateRouteResponse(Microsoft.AspNetCore.Http.HttpContext! context, int statusCode, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.WriteResponse(Microsoft.AspNetCore.Http.HttpContext! context, int statusCode, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.ProblemDetailsOptions +Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapClientErrors.get -> bool +Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapClientErrors.set -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapExceptions.get -> bool +Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapExceptions.set -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapRoutingErrors.get -> bool +Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapRoutingErrors.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void +Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Shared/ProblemDetailsDefaults.cs b/src/Shared/ProblemDetailsDefaults.cs index 2e13e1bfd1c9..065f8ae2a002 100644 --- a/src/Shared/ProblemDetailsDefaults.cs +++ b/src/Shared/ProblemDetailsDefaults.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Mvc; -namespace Microsoft.AspNetCore.Http.Extensions; +namespace Microsoft.AspNetCore.Http; internal static class ProblemDetailsDefaults { @@ -64,4 +66,40 @@ internal static class ProblemDetailsDefaults "An error occurred while processing your request." ), }; + + public static void Apply( + HttpContext httpContext, + ProblemDetails problemDetails, + int? statusCode, + Dictionary errorMapping) + { + // 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 (errorMapping.TryGetValue(problemDetails.Status.Value, out var mapData)) + { + problemDetails.Title ??= mapData.Title; + problemDetails.Type ??= mapData.Link; + } + + var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; + if (traceId != null) + { + problemDetails.Extensions["traceId"] = traceId; + } + } } From 7f368a880df65d51082feebd90060853895e2f27 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 23:58:04 -0700 Subject: [PATCH 08/59] Using ProblemDetailsOptions in mvc --- .../DefaultHttpProblemDetailsFactory.cs | 17 ++++-------- .../DefaultProblemDetailsFactory.cs | 26 +++---------------- .../ProblemDetailsClientErrorFactory.cs | 16 ++++++++++-- 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs index 2334fbc26c94..fd09003f3228 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs @@ -11,18 +11,20 @@ internal class DefaultHttpProblemDetailsFactory : IHttpProblemDetailsFactory { private readonly OutputFormatterSelector _formatterSelector; private readonly IHttpResponseStreamWriterFactory _writerFactory; + private readonly ProblemDetailsFactory _problemDetailsFactory; public DefaultHttpProblemDetailsFactory( OutputFormatterSelector formatterSelector, - IHttpResponseStreamWriterFactory writerFactory) + IHttpResponseStreamWriterFactory writerFactory, + ProblemDetailsFactory problemDetailsFactory) { _formatterSelector = formatterSelector; _writerFactory = writerFactory; + _problemDetailsFactory = problemDetailsFactory; } public ProblemDetails CreateProblemDetails( HttpContext httpContext, - ProblemDetailsOptions options, int? statusCode = null, string? title = null, string? type = null, @@ -30,14 +32,7 @@ public ProblemDetails CreateProblemDetails( string? instance = null, IDictionary? extensions = null) { - var problemDetails = new ProblemDetails - { - Status = statusCode, - Title = title, - Type = type, - Detail = detail, - Instance = instance, - }; + var problemDetails = _problemDetailsFactory.CreateProblemDetails(httpContext, statusCode, title, type, detail); if (extensions is not null) { @@ -47,8 +42,6 @@ public ProblemDetails CreateProblemDetails( } } - ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, options.ProblemDetailsErrorMapping); - return problemDetails; } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs index f77eed19ecd1..48abccdd29de 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs @@ -3,7 +3,6 @@ #nullable enable -using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; @@ -12,9 +11,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory { - private readonly ApiBehaviorOptions _options; + private readonly ProblemDetailsOptions _options; - public DefaultProblemDetailsFactory(IOptions options) + public DefaultProblemDetailsFactory(IOptions options) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } @@ -38,7 +37,7 @@ public override ProblemDetails CreateProblemDetails( Instance = instance, }; - ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, _options.ProblemDetailsErrorMapping); return problemDetails; } @@ -73,25 +72,8 @@ public override ValidationProblemDetails CreateValidationProblemDetails( problemDetails.Title = title; } - ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, _options.ProblemDetailsErrorMapping); return problemDetails; } - - private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) - { - problemDetails.Status ??= statusCode; - - if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) - { - problemDetails.Title ??= clientErrorData.Title; - problemDetails.Type ??= clientErrorData.Link; - } - - var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; - if (traceId != null) - { - problemDetails.Extensions["traceId"] = traceId; - } - } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index 75bfeae22093..379bfac7cb70 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -3,17 +3,29 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; + private readonly ProblemDetailsOptions _options; - public ProblemDetailsClientErrorFactory(ProblemDetailsFactory problemDetailsFactory) + public ProblemDetailsClientErrorFactory( + ProblemDetailsFactory problemDetailsFactory, + IOptions options) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } - public IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) + public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { + if (!_options.IsEnabled(clientError.StatusCode!.Value)) + { + return null; + } + var problemDetails = _problemDetailsFactory.CreateProblemDetails(actionContext.HttpContext, clientError.StatusCode); return new ObjectResult(problemDetails) From acb590b39ec587b021224cd84d36cc849a0ff28e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Jun 2022 23:58:33 -0700 Subject: [PATCH 09/59] ProblemDetails project simplification --- .../src/DefaultProblemDetailsFactory.cs | 11 +++++++++-- .../ProblemDetailsServiceCollectionExtensions.cs | 2 +- .../src/IProblemDetailsFactory.cs | 1 - .../src/ProblemDetailsEndpointProvider.cs | 15 +++++---------- .../src/ProblemDetailsOptions.cs | 8 ++++++++ 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs b/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs index 17b7f4f9d5eb..51c599e2f71e 100644 --- a/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs +++ b/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs @@ -3,14 +3,21 @@ using System.Text.Json; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http; internal sealed class DefaultHttpProblemDetailsFactory : IHttpProblemDetailsFactory { + private readonly ProblemDetailsOptions _options; + + public DefaultHttpProblemDetailsFactory(IOptions options) + { + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + public ProblemDetails CreateProblemDetails( HttpContext httpContext, - ProblemDetailsOptions options, int? statusCode = null, string? title = null, string? type = null, @@ -35,7 +42,7 @@ public ProblemDetails CreateProblemDetails( } } - ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, options.ProblemDetailsErrorMapping); + ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, _options.ProblemDetailsErrorMapping); return problemDetails; } diff --git a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs index 48feb89da680..99df677cc3f2 100644 --- a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs @@ -29,7 +29,7 @@ public static IServiceCollection AddProblemDetails( { var options = s.GetRequiredService>().Value; var factory = s.GetRequiredService(); - return new ProblemDetailsEndpointProvider(options, factory); + return new ProblemDetailsEndpointProvider(Options.Options.Create(options), factory); }); // Adding options configurations diff --git a/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs b/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs index 7e51a0baf74b..e07fd6fd08ba 100644 --- a/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs +++ b/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs @@ -34,7 +34,6 @@ Task WriteAsync( /// The instance. ProblemDetails CreateProblemDetails( HttpContext httpContext, - ProblemDetailsOptions options, int? statusCode = null, string? title = null, string? type = null, diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs index 72f79d661fc4..c8f8357e6917 100644 --- a/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs +++ b/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http; @@ -14,10 +15,10 @@ public sealed class ProblemDetailsEndpointProvider private readonly IHttpProblemDetailsFactory _factory; internal ProblemDetailsEndpointProvider( - ProblemDetailsOptions options, + IOptions options, IHttpProblemDetailsFactory factory) { - _options = options; + _options = options.Value; _factory = factory; } @@ -37,7 +38,7 @@ public RequestDelegate CreateRequestDelegate( if (CanWrite(defaultStatusCode)) { - var details = _factory.CreateProblemDetails(context, _options, statusCode: context.Response.StatusCode); + var details = _factory.CreateProblemDetails(context, statusCode: context.Response.StatusCode); configureDetails?.Invoke(context, details); return _factory.WriteAsync(context, details); @@ -70,7 +71,6 @@ public Task WriteResponse( context.Response.StatusCode = statusCode; var details = _factory.CreateProblemDetails( context, - _options, statusCode: context.Response.StatusCode, title, type, @@ -87,10 +87,5 @@ public Task WriteResponse( /// /// public bool CanWrite(int statusCode, bool isRouting = false) - => isRouting ? !_options.SuppressMapRoutingErrors : statusCode switch - { - (>= 400) and (<= 499) => !_options.SuppressMapClientErrors, - (>= 500) => !_options.SuppressMapExceptions, - _ => false, - }; + => _options.IsEnabled(statusCode, isRouting); } diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs index fa098f88b0cf..74ca3d8c4073 100644 --- a/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs +++ b/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs @@ -24,4 +24,12 @@ public class ProblemDetailsOptions public bool SuppressMapExceptions { get; set; } = true; internal Dictionary ProblemDetailsErrorMapping { get; } = new Dictionary(); + + internal bool IsEnabled(int statusCode, bool isRouting = false) + => isRouting ? !SuppressMapRoutingErrors : statusCode switch + { + (>= 400) and (<= 499) => !SuppressMapClientErrors, + (>= 500) => !SuppressMapExceptions, + _ => false, + }; } From 62eb53ed0aa9ddfc72778555d450d6a05c95e9b2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Jun 2022 10:34:42 -0700 Subject: [PATCH 10/59] Using metadata --- AspNetCore.sln | 21 ++++- eng/ProjectReferences.props | 3 +- eng/SharedFramework.Local.props | 3 +- eng/TrimmableProjects.props | 3 +- .../IProducesResponseErrorTypeMetadata.cs | 25 +++++ ...icrosoft.AspNetCore.Http.Extensions.csproj | 2 +- .../src/RequestDelegateFactory.cs | 6 -- .../src/DefaultProblemDetailsFactory.cs | 62 ------------- .../ProblemDetailsOptionsSetup.cs | 29 ------ .../src/IProblemDetailsFactory.cs | 43 --------- .../src/ProblemDetailsEndpointProvider.cs | 91 ------------------- .../src/ProblemDetailsErrorData.cs | 30 ------ .../src/ProblemDetailsOptions.cs | 35 ------- .../src/PublicAPI.Unshipped.txt | 34 ------- .../Http.Results/src/HttpResultsHelper.cs | 28 +----- .../Http.Results/src/JsonHttpResultOfT.cs | 2 +- .../Microsoft.AspNetCore.Http.Results.csproj | 2 +- .../Http.Results/src/ProblemHttpResult.cs | 2 +- .../Http.Results/src/ValidationProblem.cs | 2 +- .../Http/src/Builder/ApplicationBuilder.cs | 14 +-- .../src/HttpValidationProblemDetails.cs | 0 .../src/IProblemDetailsEndpointWriter.cs | 38 ++++++++ .../src/IProblemDetailsMapPolicy.cs | 9 ++ ...e.Http.ProblemDetails.Abstractions.csproj} | 22 ++--- .../src/ProblemDetails.cs | 0 .../src/PublicAPI.Shipped.txt | 0 .../src/PublicAPI.Unshipped.txt | 20 ++++ .../DefaultProblemDetailsEndpointWriter.cs | 65 +++++++++++++ ...oblemDetailsConventionBuilderExtensions.cs | 12 +++ .../ProblemDetailsOptions.cs | 58 ++++++++++++ ...oblemDetailsServiceCollectionExtensions.cs | 16 +--- .../src/Mapping/ProblemDetailsMapper.cs | 67 ++++++++++++++ ...soft.AspNetCore.Http.ProblemDetails.csproj | 23 +++++ .../src/ProblemDetailsResponseMetadata.cs | 12 +++ .../ProblemDetails/src/PublicAPI.Shipped.txt | 1 + .../src/PublicAPI.Unshipped.txt | 6 ++ .../src/Matching/AcceptsMatcherPolicy.cs | 14 +-- .../src/Matching/HttpMethodMatcherPolicy.cs | 14 +-- .../ExceptionHandlerExtensions.cs | 3 +- .../ExceptionHandlerMiddleware.cs | 61 ++++++------- .../ExceptionHandlerOptions.cs | 6 -- .../Diagnostics/src/PublicAPI.Unshipped.txt | 5 +- .../MvcCoreProblemDetailsOptionsSetup.cs | 42 --------- .../MvcCoreServiceCollectionExtensions.cs | 5 +- .../src/Formatters/TextOutputFormatter.cs | 13 +-- .../DefaultProblemDetailsFactory.cs | 26 +++++- ...tory.cs => DefaultProblemDetailsWriter.cs} | 58 +++++++----- .../Infrastructure/ObjectResultExecutor.cs | 13 +-- .../ProblemDetailsClientErrorFactory.cs | 14 +-- .../src/ProblemDetailsApiMapPolicy.cs | 34 +++++++ .../src/ProducesErrorResponseTypeAttribute.cs | 6 +- .../MvcCoreServiceCollectionExtensionsTest.cs | 14 +-- src/Mvc/Mvc.slnf | 5 +- src/Shared/ProblemDetailsDefaults.cs | 18 +--- 54 files changed, 555 insertions(+), 582 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs delete mode 100644 src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs delete mode 100644 src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.cs delete mode 100644 src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs delete mode 100644 src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs delete mode 100644 src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs delete mode 100644 src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs delete mode 100644 src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt rename src/Http/{Http.ProblemDetails => ProblemDetails.Abstractions}/src/HttpValidationProblemDetails.cs (100%) create mode 100644 src/Http/ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs create mode 100644 src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs rename src/Http/{Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj => ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj} (73%) rename src/Http/{Http.ProblemDetails => ProblemDetails.Abstractions}/src/ProblemDetails.cs (100%) rename src/Http/{Http.ProblemDetails => ProblemDetails.Abstractions}/src/PublicAPI.Shipped.txt (100%) create mode 100644 src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt create mode 100644 src/Http/ProblemDetails/src/DefaultProblemDetailsEndpointWriter.cs create mode 100644 src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs create mode 100644 src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs rename src/Http/{Http.ProblemDetails => ProblemDetails}/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs (73%) create mode 100644 src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs create mode 100644 src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj create mode 100644 src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs create mode 100644 src/Http/ProblemDetails/src/PublicAPI.Shipped.txt create mode 100644 src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt delete mode 100644 src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs rename src/Mvc/Mvc.Core/src/Infrastructure/{DefaultHttpProblemDetailsFactory.cs => DefaultProblemDetailsWriter.cs} (51%) create mode 100644 src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 35270705be85..d2b507991d29 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1712,13 +1712,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimitin EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.ProblemDetails", "Http.ProblemDetails", "{41AF137D-4181-42F9-9B53-BEDB9532F29B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.ProblemDetails", "src\Http\Http.ProblemDetails\src\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "{2333E682-F9E9-4235-BF63-6403C114EA76}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.ProblemDetails", "src\Http\ProblemDetails\src\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "{2333E682-F9E9-4235-BF63-6403C114EA76}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7F014D2B-CC88-4BBE-8A0E-04FCF490F855}" ProjectSection(SolutionItems) = preProject Graph1.dgml = Graph1.dgml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspnetCore.Http.ProblemDetails.Abstractions", "src\Http\ProblemDetails.Abstractions\src\Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj", "{F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10272,6 +10274,22 @@ Global {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x64.Build.0 = Release|Any CPU {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.ActiveCfg = Release|Any CPU {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.Build.0 = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|arm64.ActiveCfg = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|arm64.Build.0 = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x64.ActiveCfg = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x64.Build.0 = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x86.ActiveCfg = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x86.Build.0 = Debug|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|Any CPU.Build.0 = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|arm64.ActiveCfg = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|arm64.Build.0 = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x64.ActiveCfg = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x64.Build.0 = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x86.ActiveCfg = Release|Any CPU + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11121,6 +11139,7 @@ Global {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} {41AF137D-4181-42F9-9B53-BEDB9532F29B} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724} {2333E682-F9E9-4235-BF63-6403C114EA76} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} + {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index e642ca47d67a..24eafa2544ea 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -28,11 +28,12 @@ - + + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 0e9f8d2518be..a71901d4beed 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -48,9 +48,10 @@ - + + diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index fc92654ec2b4..57806e79da0a 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -24,9 +24,10 @@ - + + diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs new file mode 100644 index 000000000000..72afc6cadcae --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Metadata; + + +/// +/// Specifies the type returned by default by controllers annotated with . +/// +/// specifies the error model type associated with a +/// for a client error (HTTP Status Code 4xx) when no value is provided. When no value is specified, MVC assumes the +/// client error type to be , if mapping client errors () +/// is used. +/// +/// +/// Use this to configure the default error type if your application uses a custom error type to respond. +/// +/// +public interface IProducesErrorResponseMetadata +{ + /// + /// Gets the optimistic return type of the action. + /// + Type? Type { get; } +} diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 50d916062618..4f86bd083fc8 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index ff50cb4d901f..5bda77abd7f0 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -110,12 +110,6 @@ public static partial class RequestDelegateFactory private static Task CreateProblemDetails(HttpContext context) { - var endpointProvider = context.RequestServices.GetService(); - if (endpointProvider != null && endpointProvider.CanWrite(context.Response.StatusCode)) - { - return endpointProvider.WriteResponse(context, context.Response.StatusCode); - } - return Task.CompletedTask; } diff --git a/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs b/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.cs deleted file mode 100644 index 51c599e2f71e..000000000000 --- a/src/Http/Http.ProblemDetails/src/DefaultProblemDetailsFactory.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 System.Text.Json; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -internal sealed class DefaultHttpProblemDetailsFactory : IHttpProblemDetailsFactory -{ - private readonly ProblemDetailsOptions _options; - - public DefaultHttpProblemDetailsFactory(IOptions options) - { - _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - } - - public ProblemDetails CreateProblemDetails( - HttpContext httpContext, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null) - { - var problemDetails = new ProblemDetails - { - Status = statusCode, - Title = title, - Type = type, - Detail = detail, - Instance = instance, - }; - - if (extensions is not null) - { - foreach (var extension in extensions) - { - problemDetails.Extensions.Add(extension); - } - } - - ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, _options.ProblemDetailsErrorMapping); - - return problemDetails; - } - - public async Task WriteAsync( - HttpContext context, - ProblemDetails problemDetails) - { - //TODO: JsonOptions??? CancellationToken?? - try - { - context.Response.ContentType = "application/problem+json"; - await JsonSerializer.SerializeAsync(context.Response.Body, problemDetails, typeof(ProblemDetails), cancellationToken: context.RequestAborted); - } - catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) { } - } -} diff --git a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.cs b/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.cs deleted file mode 100644 index e5bb81c0b2a7..000000000000 --- a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsOptionsSetup.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.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection; - -internal sealed class ProblemDetailsOptionsSetup : IConfigureOptions -{ - public void Configure(ProblemDetailsOptions options) - { - ArgumentNullException.ThrowIfNull(nameof(options)); - ConfigureProblemDetailsErrorMapping(options); - } - - // Internal for unit testing - internal static void ConfigureProblemDetailsErrorMapping(ProblemDetailsOptions options) - { - foreach (var (statusCode, value) in ProblemDetailsDefaults.Defaults) - { - options.ProblemDetailsErrorMapping[statusCode] = new() - { - Link = value.Type, - Title = value.Title, - }; - } - } -} diff --git a/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs b/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs deleted file mode 100644 index e07fd6fd08ba..000000000000 --- a/src/Http/Http.ProblemDetails/src/IProblemDetailsFactory.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http; - -using Microsoft.AspNetCore.Mvc; - -/// -/// -/// -internal interface IHttpProblemDetailsFactory -{ - /// - /// - /// - /// - /// - /// - Task WriteAsync( - HttpContext context, - ProblemDetails problemDetails); - - /// - /// Creates a instance that configures defaults based on values specified in . - /// - /// The . - /// - /// The value for . - /// The value for . - /// The value for . - /// The value for . - /// The value for . - /// The value for . - /// The instance. - ProblemDetails CreateProblemDetails( - HttpContext httpContext, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null); -} diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs deleted file mode 100644 index c8f8357e6917..000000000000 --- a/src/Http/Http.ProblemDetails/src/ProblemDetailsEndpointProvider.cs +++ /dev/null @@ -1,91 +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.Mvc; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -/// -/// -/// -public sealed class ProblemDetailsEndpointProvider -{ - private readonly ProblemDetailsOptions _options; - private readonly IHttpProblemDetailsFactory _factory; - - internal ProblemDetailsEndpointProvider( - IOptions options, - IHttpProblemDetailsFactory factory) - { - _options = options.Value; - _factory = factory; - } - - /// - /// - /// - /// - /// - /// - public RequestDelegate CreateRequestDelegate( - int defaultStatusCode, - Action? configureDetails = null) - { - return (HttpContext context) => - { - context.Response.StatusCode = defaultStatusCode; - - if (CanWrite(defaultStatusCode)) - { - var details = _factory.CreateProblemDetails(context, statusCode: context.Response.StatusCode); - configureDetails?.Invoke(context, details); - - return _factory.WriteAsync(context, details); - } - - return Task.CompletedTask; - }; - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public Task WriteResponse( - HttpContext context, - int statusCode, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null) - { - context.Response.StatusCode = statusCode; - var details = _factory.CreateProblemDetails( - context, - statusCode: context.Response.StatusCode, - title, - type, - detail, - instance, - extensions); - return _factory.WriteAsync(context, details); - } - - /// - /// - /// - /// - /// - /// - public bool CanWrite(int statusCode, bool isRouting = false) - => _options.IsEnabled(statusCode, isRouting); -} diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs deleted file mode 100644 index 9aa90e6234e6..000000000000 --- a/src/Http/Http.ProblemDetails/src/ProblemDetailsErrorData.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http; - -using Microsoft.AspNetCore.Mvc; - -/// -/// Information for producing client errors. This type is used to configure client errors -/// produced by consumers of . -/// -internal class ProblemDetailsErrorData -{ - /// - /// Gets or sets a link (URI) that describes the client error. - /// - /// - /// By default, this maps to . - /// - public string? Link { get; set; } - - /// - /// Gets or sets the summary of the client error. - /// - /// - /// By default, this maps to and should not change - /// between multiple occurrences of the same error. - /// - public string? Title { get; set; } -} diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs b/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs deleted file mode 100644 index 74ca3d8c4073..000000000000 --- a/src/Http/Http.ProblemDetails/src/ProblemDetailsOptions.cs +++ /dev/null @@ -1,35 +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; - -/// -/// -/// -public class ProblemDetailsOptions -{ - /// - /// - /// - public bool SuppressMapRoutingErrors { get; set; } = true; - - /// - /// - /// - public bool SuppressMapClientErrors { get; set; } = true; - - /// - /// - /// - public bool SuppressMapExceptions { get; set; } = true; - - internal Dictionary ProblemDetailsErrorMapping { get; } = new Dictionary(); - - internal bool IsEnabled(int statusCode, bool isRouting = false) - => isRouting ? !SuppressMapRoutingErrors : statusCode switch - { - (>= 400) and (<= 499) => !SuppressMapClientErrors, - (>= 500) => !SuppressMapExceptions, - _ => false, - }; -} diff --git a/src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt deleted file mode 100644 index 562b37ea0551..000000000000 --- a/src/Http/Http.ProblemDetails/src/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,34 +0,0 @@ -#nullable enable -Microsoft.AspNetCore.Http.HttpValidationProblemDetails -Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! -Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void -Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void -Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider -Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.CanWrite(int statusCode, bool isRouting = false) -> bool -Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.CreateRequestDelegate(int defaultStatusCode, System.Action? configureDetails = null) -> Microsoft.AspNetCore.Http.RequestDelegate! -Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.CreateRouteResponse(Microsoft.AspNetCore.Http.HttpContext! context, int statusCode, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider.WriteResponse(Microsoft.AspNetCore.Http.HttpContext! context, int statusCode, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.ProblemDetailsOptions -Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void -Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapClientErrors.get -> bool -Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapClientErrors.set -> void -Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapExceptions.get -> bool -Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapExceptions.set -> void -Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapRoutingErrors.get -> bool -Microsoft.AspNetCore.Http.ProblemDetailsOptions.SuppressMapRoutingErrors.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails -Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! -Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? -Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void -Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index 21134618f897..b3c45500f27c 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -118,33 +118,7 @@ public static void ApplyProblemDetailsDefaultsIfNeeded(object? value, int? statu { 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; + ProblemDetailsDefaults.Apply(problemDetails, statusCode); } } diff --git a/src/Http/Http.Results/src/JsonHttpResultOfT.cs b/src/Http/Http.Results/src/JsonHttpResultOfT.cs index 58496fbb8c1f..0e2eadc111b5 100644 --- a/src/Http/Http.Results/src/JsonHttpResultOfT.cs +++ b/src/Http/Http.Results/src/JsonHttpResultOfT.cs @@ -49,7 +49,7 @@ internal JsonHttpResult(TValue? value, int? statusCode, string? contentType, Jso if (value is ProblemDetails problemDetails) { - HttpResultsHelper.ApplyProblemDetailsDefaults(problemDetails, statusCode); + ProblemDetailsDefaults.Apply(problemDetails, statusCode); statusCode ??= problemDetails.Status; } diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index a097f859d585..3bc5c99c163d 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/Http/Http.Results/src/ProblemHttpResult.cs b/src/Http/Http.Results/src/ProblemHttpResult.cs index 9fdf1f2c1d10..3646f13b9ce8 100644 --- a/src/Http/Http.Results/src/ProblemHttpResult.cs +++ b/src/Http/Http.Results/src/ProblemHttpResult.cs @@ -21,7 +21,7 @@ public sealed class ProblemHttpResult : IResult internal ProblemHttpResult(ProblemDetails problemDetails) { ProblemDetails = problemDetails; - HttpResultsHelper.ApplyProblemDetailsDefaults(ProblemDetails, statusCode: null); + ProblemDetailsDefaults.Apply(ProblemDetails, statusCode: null); } /// diff --git a/src/Http/Http.Results/src/ValidationProblem.cs b/src/Http/Http.Results/src/ValidationProblem.cs index 4c5206c4b881..b7e4e3db3d98 100644 --- a/src/Http/Http.Results/src/ValidationProblem.cs +++ b/src/Http/Http.Results/src/ValidationProblem.cs @@ -22,7 +22,7 @@ internal ValidationProblem(HttpValidationProblemDetails problemDetails) } ProblemDetails = problemDetails; - HttpResultsHelper.ApplyProblemDetailsDefaults(ProblemDetails, statusCode: StatusCodes.Status400BadRequest); + ProblemDetailsDefaults.Apply(ProblemDetails, statusCode: StatusCodes.Status400BadRequest); } /// diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index e8addd712fe2..063ec3ef3645 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -131,16 +131,12 @@ public RequestDelegate Build() } const int statusCode = StatusCodes.Status404NotFound; - - var endpointProvider = context.RequestServices.GetService(); - if (endpointProvider != null && - endpointProvider.CanWrite(statusCode, isRouting: true)) - { - return endpointProvider.WriteResponse(context, statusCode); - } - context.Response.StatusCode = statusCode; - return Task.CompletedTask; + + var endpointWriter = context.RequestServices.GetService(); + return endpointWriter == null ? + Task.CompletedTask : + endpointWriter.WriteAsync(context); }; for (var c = _components.Count - 1; c >= 0; c--) diff --git a/src/Http/Http.ProblemDetails/src/HttpValidationProblemDetails.cs b/src/Http/ProblemDetails.Abstractions/src/HttpValidationProblemDetails.cs similarity index 100% rename from src/Http/Http.ProblemDetails/src/HttpValidationProblemDetails.cs rename to src/Http/ProblemDetails.Abstractions/src/HttpValidationProblemDetails.cs diff --git a/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs b/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs new file mode 100644 index 000000000000..de9c79ee978d --- /dev/null +++ b/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs @@ -0,0 +1,38 @@ +// 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.Mvc; + +namespace Microsoft.AspNetCore.Http; + +/// +/// +/// +public interface IProblemDetailsEndpointWriter +{ + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task WriteAsync( + HttpContext context, + EndpointMetadataCollection? metadata = null, + bool isRouting = false, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null, + Action? configureDetails = null); +} diff --git a/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs b/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs new file mode 100644 index 000000000000..86d3d7a15a34 --- /dev/null +++ b/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs @@ -0,0 +1,9 @@ +// 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; + +public interface IProblemDetailsMapPolicy +{ + bool CanMap(HttpContext context, EndpointMetadataCollection? metadata, int? statusCode, bool isRouting); +} diff --git a/src/Http/Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj similarity index 73% rename from src/Http/Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj rename to src/Http/ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj index 5949a2f4c4da..11a3e6c3bfb3 100644 --- a/src/Http/Http.ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj +++ b/src/Http/ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj @@ -10,28 +10,20 @@ true - - - - - - - + - + + + - - - + - - + - - + diff --git a/src/Http/Http.ProblemDetails/src/ProblemDetails.cs b/src/Http/ProblemDetails.Abstractions/src/ProblemDetails.cs similarity index 100% rename from src/Http/Http.ProblemDetails/src/ProblemDetails.cs rename to src/Http/ProblemDetails.Abstractions/src/ProblemDetails.cs diff --git a/src/Http/Http.ProblemDetails/src/PublicAPI.Shipped.txt b/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Shipped.txt similarity index 100% rename from src/Http/Http.ProblemDetails/src/PublicAPI.Shipped.txt rename to src/Http/ProblemDetails.Abstractions/src/PublicAPI.Shipped.txt diff --git a/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..9c9b3e81e77c --- /dev/null +++ b/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +#nullable enable +Microsoft.AspNetCore.Http.HttpValidationProblemDetails +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void +Microsoft.AspNetCore.Http.IProblemDetailsEndpointWriter +Microsoft.AspNetCore.Http.IProblemDetailsEndpointWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata = null, bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null, System.Action? configureDetails = null) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Mvc.ProblemDetails +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsEndpointWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsEndpointWriter.cs new file mode 100644 index 000000000000..830f5461253c --- /dev/null +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsEndpointWriter.cs @@ -0,0 +1,65 @@ +// 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.Mvc; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultProblemDetailsEndpointWriter : IProblemDetailsEndpointWriter +{ + private readonly ProblemDetailsMapper? _mapper; + private readonly ProblemDetailsOptions _options; + + public DefaultProblemDetailsEndpointWriter( + IOptions options, + ProblemDetailsMapper? mapper = null) + { + _mapper = mapper; + _options = options.Value; + } + + public async Task WriteAsync( + HttpContext context, + EndpointMetadataCollection? metadata = null, + bool isRouting = false, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null, + Action? configureDetails = null) + { + if (_mapper == null || + !_mapper.CanMap(context, metadata: metadata, isRouting: isRouting)) + { + return false; + } + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance + }; + + if (extensions is not null) + { + foreach (var extension in extensions) + { + problemDetails.Extensions[extension.Key] = extension.Value; + } + } + + ProblemDetailsDefaults.Apply(problemDetails, context.Response.StatusCode); + + _options.ConfigureDetails?.Invoke(context, problemDetails); + configureDetails?.Invoke(context, problemDetails); + + await context.Response.WriteAsJsonAsync(problemDetails, options: null, "application/problem+json"); + return true; + } +} diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs new file mode 100644 index 000000000000..1c855fdf1372 --- /dev/null +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Http.ProblemDetails.DependencyInjection; + +public static class ProblemDetailsConventionBuilderExtensions +{ + public static TBuilder WithProblemDetails(this TBuilder builder) where TBuilder : IEndpointConventionBuilder + => builder.WithMetadata(new TagsAttribute(tags)); +} diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs new file mode 100644 index 000000000000..f0b2c35b696e --- /dev/null +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs @@ -0,0 +1,58 @@ +// 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; + +/// +/// +/// +public class ProblemDetailsOptions +{ + /// + /// + /// + public MappingOptions Mapping { get; set; } = MappingOptions.ClientErrors | MappingOptions.Exceptions; + public Action? ConfigureDetails { get; set; } + + public bool IsEnabled(int statusCode, bool isRouting = false) + => isRouting ? Mapping.HasFlag(MappingOptions.Routing) : statusCode switch + { + >= 400 and <= 499 => Mapping.HasFlag(MappingOptions.ClientErrors), + >= 500 => Mapping.HasFlag(MappingOptions.Exceptions), + _ => false, + }; +} + +/// +/// +/// +[Flags] +public enum MappingOptions : uint +{ + /// + /// + /// + None = 0, + + /// + /// + /// + ClientErrors = 1, + + /// + /// + /// + Routing = 2, + + /// + /// + /// + Exceptions = 4, + + /// + /// + /// + All = ClientErrors | Routing | Exceptions +} diff --git a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs similarity index 73% rename from src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs rename to src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs index 99df677cc3f2..9ce232c9966c 100644 --- a/src/Http/Http.ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -23,18 +22,9 @@ public static IServiceCollection AddProblemDetails( { ArgumentNullException.ThrowIfNull(nameof(services)); - // Adding default services - services.TryAddSingleton(); - services.TryAddSingleton(s => - { - var options = s.GetRequiredService>().Value; - var factory = s.GetRequiredService(); - return new ProblemDetailsEndpointProvider(Options.Options.Create(options), factory); - }); - - // Adding options configurations - services.TryAddEnumerable( - ServiceDescriptor.Transient, ProblemDetailsOptionsSetup>()); + // Adding default services; + services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs b/src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs new file mode 100644 index 000000000000..14759ce67202 --- /dev/null +++ b/src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +internal class ProblemDetailsMapper +{ + private readonly IProblemDetailsMapPolicy[] _matchPolicies; + private readonly ProblemDetailsOptions _options; + + private static readonly MediaTypeHeaderValue _problemMediaType = new("application/problem+*"); + + public ProblemDetailsMapper( + IEnumerable matchPolicies, + IOptions options) + { + _matchPolicies = matchPolicies.ToArray(); + _options = options.Value; + } + + public bool CanMap( + HttpContext context, + EndpointMetadataCollection? metadata = null, + int? statusCode = null, + bool isRouting = false) + { + metadata ??= context.GetEndpoint()?.Metadata; + + if (!_options.IsEnabled(statusCode ?? context.Response.StatusCode, isRouting)) + { + return false; + } + + var headers = context.Request.GetTypedHeaders(); + var acceptHeader = headers.Accept; + + if (acceptHeader != null && + !acceptHeader.Any(h => _problemMediaType.IsSubsetOf(h))) + { + return false; + } + + // What if we don't have the endpoint (eg. Routing) + + var responseType = metadata?.GetMetadata(); + if (responseType == null || !typeof(ProblemDetails).IsAssignableFrom(responseType.Type)) + { + return false; + } + + for (var i = _matchPolicies.Length; i > 0; i--) + { + if (!_matchPolicies[i - 1].CanMap(context, metadata, statusCode, isRouting)) + { + return false; + } + } + + return true; + } +} diff --git a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj new file mode 100644 index 000000000000..1541356c78bb --- /dev/null +++ b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj @@ -0,0 +1,23 @@ + + + + ASP.NET Core. + $(DefaultNetCoreTargetFramework) + true + true + aspnetcore + false + true + + + + + + + + + + + + + diff --git a/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs b/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs new file mode 100644 index 000000000000..800aa87bf017 --- /dev/null +++ b/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.AspNetCore.Http; + +internal class ProblemDetailsResponseMetadata : IProducesErrorResponseMetadata +{ + public Type? Type => typeof(ProblemDetails); +} diff --git a/src/Http/ProblemDetails/src/PublicAPI.Shipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..7dc5c58110bf --- /dev/null +++ b/src/Http/ProblemDetails/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..e48291d6d669 --- /dev/null +++ b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +#nullable enable +Microsoft.AspNetCore.Http.ProblemDetailsOptions +Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void +Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 5d58332b19cd..ee4d26cb995e 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -263,16 +263,12 @@ private static Endpoint CreateRejectionEndpoint() context => { const int statusCode = StatusCodes.Status415UnsupportedMediaType; - - var endpointProvider = context.RequestServices.GetService(); - if (endpointProvider != null && - endpointProvider.CanWrite(statusCode, isRouting: true)) - { - return endpointProvider.WriteResponse(context, statusCode); - } - context.Response.StatusCode = statusCode; - return Task.CompletedTask; + + var endpointWriter = context.RequestServices.GetService(); + return endpointWriter == null ? + Task.CompletedTask : + endpointWriter.WriteAsync(context); }, EndpointMetadataCollection.Empty, Http415EndpointDisplayName); diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 71757dd1d184..b924d220299a 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -410,16 +410,12 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods context.Response.Headers.Allow = allow; const int statusCode = StatusCodes.Status405MethodNotAllowed; - - var endpointProvider = context.RequestServices.GetService(); - if (endpointProvider != null && - endpointProvider.CanWrite(statusCode, isRouting: true)) - { - return endpointProvider.WriteResponse(context, statusCode); - } - context.Response.StatusCode = statusCode; - return Task.CompletedTask; + + var endpointWriter = context.RequestServices.GetService(); + return endpointWriter == null ? + Task.CompletedTask : + endpointWriter.WriteAsync(context); }, EndpointMetadataCollection.Empty, Http405EndpointDisplayName); diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index cca9787cd118..602b1bc2172d 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -131,8 +131,7 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui options.Value.ExceptionHandler = builder.Build(); } - var provider = app.ApplicationServices.GetService(); - return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, provider).Invoke; + return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener).Invoke; }); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index 976418ae728d..48434f74ce3d 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -24,7 +25,6 @@ public class ExceptionHandlerMiddleware private readonly ILogger _logger; private readonly Func _clearCacheHeadersDelegate; private readonly DiagnosticListener _diagnosticListener; - private readonly ProblemDetailsEndpointProvider? _problemDetailsEndpointProvider; /// /// Creates a new @@ -33,30 +33,22 @@ public class ExceptionHandlerMiddleware /// The used for logging. /// The options for configuring the middleware. /// The used for writing diagnostic messages. - /// public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions options, - DiagnosticListener diagnosticListener, - ProblemDetailsEndpointProvider? problemDetailsEndpointProvider = null) + DiagnosticListener diagnosticListener) { _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger(); _clearCacheHeadersDelegate = ClearCacheHeaders; _diagnosticListener = diagnosticListener; - _problemDetailsEndpointProvider = problemDetailsEndpointProvider; if (_options.ExceptionHandler == null) { if (_options.ExceptionHandlingPath == null) { - if (_problemDetailsEndpointProvider == null) - { - throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); - } - - _options.ExceptionHandler = CreatProblemDetailsHandler(); + throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); } else { @@ -142,7 +134,10 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response); - await _options.ExceptionHandler!(context); + if (!await TryWriteProblemDetails(context, exceptionHandlerFeature)) + { + await _options.ExceptionHandler!(context); + } // If the response has already started, assume exception handler was successful. if (context.Response.HasStarted || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response) @@ -202,28 +197,32 @@ private static Task ClearCacheHeaders(object state) return Task.CompletedTask; } - private RequestDelegate CreatProblemDetailsHandler() + private Task TryWriteProblemDetails(HttpContext context, ExceptionHandlerFeature exceptionFeature) { - return _problemDetailsEndpointProvider!.CreateRequestDelegate( - StatusCodes.Status500InternalServerError, - configureDetails: (context, problemDetails) => - { - var exceptionFeature = context.Features.GetRequiredFeature(); - var hostEnvironment = context.RequestServices.GetRequiredService(); + void ConfigureDetails(HttpContext context, ProblemDetails problemDetails) + { + var hostEnvironment = context.RequestServices.GetRequiredService(); - if (hostEnvironment.IsDevelopment()) + if (hostEnvironment.IsDevelopment()) + { + // TODO: Fix + problemDetails.Detail = exceptionFeature!.Error.Message; + problemDetails.Extensions.Add("exception", new { - problemDetails.Detail = exceptionFeature.Error.Message; - problemDetails.Extensions.Add("exception", new - { - Details = exceptionFeature.Error.ToString(), - exceptionFeature.Path, - exceptionFeature.RouteValues, - Endpoint = exceptionFeature.Endpoint?.ToString() - }); - } + Details = exceptionFeature.Error.ToString(), + exceptionFeature.Path, + exceptionFeature.RouteValues, + Endpoint = exceptionFeature.Endpoint?.ToString() + }); + } + } + + var endpointWriter = context.RequestServices.GetService(); + if (endpointWriter != null) + { + return endpointWriter.WriteAsync(context, exceptionFeature.Endpoint?.Metadata, configureDetails: ConfigureDetails); + } - _options.ConfigureDetails?.Invoke(exceptionFeature, problemDetails); - }); + return Task.FromResult(false); } } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs index 88f654701d15..d776813f29d1 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; namespace Microsoft.AspNetCore.Builder; @@ -32,9 +31,4 @@ public class ExceptionHandlerOptions /// the original exception. /// public bool AllowStatusCode404Response { get; set; } - - /// - /// - /// - public Action? ConfigureDetails { get; set; } } diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index f2042aff8ca1..a8796b96379e 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,7 +1,4 @@ #nullable enable -Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.ConfigureDetails.get -> System.Action? -Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.ConfigureDetails.set -> void -Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.ProblemDetailsEndpointProvider? problemDetailsEndpointProvider = null) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? @@ -11,6 +8,6 @@ Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.set -> v *REMOVED*~Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.StatusCodePagesMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void *REMOVED*~Microsoft.AspNetCore.Diagnostics.WelcomePageMiddleware.WelcomePageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters) -> void +Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener) -> void Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.StatusCodePagesMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.Diagnostics.WelcomePageMiddleware.WelcomePageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options) -> void -static Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Action? configureDetails = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs deleted file mode 100644 index 5b96485f1d05..000000000000 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreProblemDetailsOptionsSetup.cs +++ /dev/null @@ -1,42 +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; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Sets up MVC default options for . -/// -internal sealed class MvcCoreProblemDetailsOptionsSetup : IConfigureOptions -{ - private readonly ApiBehaviorOptions _apiBehaviorOptions; - - public MvcCoreProblemDetailsOptionsSetup(IOptions options) - { - _apiBehaviorOptions = options.Value; - } - - /// - /// Configures the . - /// - /// The . - public void Configure(ProblemDetailsOptions options) - { - ArgumentNullException.ThrowIfNull(nameof(options)); - - options.SuppressMapClientErrors = _apiBehaviorOptions.SuppressMapClientErrors; - options.SuppressMapExceptions = _apiBehaviorOptions.SuppressMapClientErrors; - - foreach (var item in _apiBehaviorOptions.ClientErrorMapping) - { - options.ProblemDetailsErrorMapping[item.Key] = new() - { - Title = item.Value.Title, - Link = item.Value.Link - }; - } - } -} diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 05215c92d400..5ef6f62372a3 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -149,8 +149,6 @@ internal static void AddMvcCoreServices(IServiceCollection services) ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreRouteOptionsSetup>()); - services.TryAddEnumerable( - ServiceDescriptor.Transient, MvcCoreProblemDetailsOptionsSetup>()); // // Action Discovery @@ -286,7 +284,8 @@ internal static void AddMvcCoreServices(IServiceCollection services) // ProblemDetails services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddSingleton(); } private static void ConfigureDefaultServices(IServiceCollection services) diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index 0c3f96fd7bb1..afc2863a4733 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -134,15 +134,12 @@ public override Task WriteAsync(OutputFormatterWriteContext context) else { const int statusCode = StatusCodes.Status406NotAcceptable; - - var endpointProvider = context.HttpContext.RequestServices.GetService(); - if (endpointProvider != null && endpointProvider.CanWrite(statusCode)) - { - return endpointProvider.WriteResponse(context.HttpContext, statusCode); - } - context.HttpContext.Response.StatusCode = statusCode; - return Task.CompletedTask; + + var endpointWriter = context.HttpContext.RequestServices.GetService(); + return endpointWriter == null ? + Task.CompletedTask : + endpointWriter.WriteAsync(context.HttpContext); } context.ContentType = selectedMediaType; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs index 48abccdd29de..f77eed19ecd1 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; @@ -11,9 +12,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory { - private readonly ProblemDetailsOptions _options; + private readonly ApiBehaviorOptions _options; - public DefaultProblemDetailsFactory(IOptions options) + public DefaultProblemDetailsFactory(IOptions options) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } @@ -37,7 +38,7 @@ public override ProblemDetails CreateProblemDetails( Instance = instance, }; - ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, _options.ProblemDetailsErrorMapping); + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); return problemDetails; } @@ -72,8 +73,25 @@ public override ValidationProblemDetails CreateValidationProblemDetails( problemDetails.Title = title; } - ProblemDetailsDefaults.Apply(httpContext, problemDetails, statusCode, _options.ProblemDetailsErrorMapping); + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); return problemDetails; } + + private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) + { + problemDetails.Status ??= statusCode; + + if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) + { + problemDetails.Title ??= clientErrorData.Title; + problemDetails.Type ??= clientErrorData.Link; + } + + var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; + if (traceId != null) + { + problemDetails.Extensions["traceId"] = traceId; + } + } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsWriter.cs similarity index 51% rename from src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs rename to src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsWriter.cs index fd09003f3228..33d87b38ad09 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultHttpProblemDetailsFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsWriter.cs @@ -4,54 +4,68 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Core.Infrastructure; -internal class DefaultHttpProblemDetailsFactory : IHttpProblemDetailsFactory +internal class DefaultProblemDetailsWriter : IProblemDetailsEndpointWriter { private readonly OutputFormatterSelector _formatterSelector; private readonly IHttpResponseStreamWriterFactory _writerFactory; private readonly ProblemDetailsFactory _problemDetailsFactory; + private readonly ProblemDetailsOptions _options; + private readonly ProblemDetailsMapper? _mapper; - public DefaultHttpProblemDetailsFactory( + private static readonly MediaTypeCollection _problemContentTypes = new() + { + "application/problem+json", + "application/problem+xml" + }; + + public DefaultProblemDetailsWriter( OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, - ProblemDetailsFactory problemDetailsFactory) + ProblemDetailsFactory problemDetailsFactory, + IOptions options, + ProblemDetailsMapper? mapper = null) { _formatterSelector = formatterSelector; _writerFactory = writerFactory; _problemDetailsFactory = problemDetailsFactory; + _options = options.Value; + _mapper = mapper; } - public ProblemDetails CreateProblemDetails( - HttpContext httpContext, + public async Task WriteAsync( + HttpContext context, + EndpointMetadataCollection? metadata = null, + bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, - IDictionary? extensions = null) + IDictionary? extensions = null, + Action? configureDetails = null) { - var problemDetails = _problemDetailsFactory.CreateProblemDetails(httpContext, statusCode, title, type, detail); + if (_mapper == null || + !_mapper.CanMap(context, metadata: metadata, isRouting: isRouting)) + { + return false; + } + + var problemDetails = _problemDetailsFactory.CreateProblemDetails(context, statusCode ?? context.Response.StatusCode, title, type, detail, instance); if (extensions is not null) { foreach (var extension in extensions) { - problemDetails.Extensions.Add(extension); + problemDetails.Extensions[extension.Key] = extension.Value; } } - return problemDetails; - } - - public Task WriteAsync(HttpContext context, ProblemDetails problemDetails) - { - var contentTypes = new MediaTypeCollection() - { - "application/problem+json", - "application/problem+xml" - }; + _options.ConfigureDetails?.Invoke(context, problemDetails); + configureDetails?.Invoke(context, problemDetails); var formatterContext = new OutputFormatterWriteContext( context, @@ -62,12 +76,14 @@ public Task WriteAsync(HttpContext context, ProblemDetails problemDetails) var selectedFormatter = _formatterSelector.SelectFormatter( formatterContext, Array.Empty(), - contentTypes); + _problemContentTypes); + if (selectedFormatter == null) { - return Task.CompletedTask; + return false; } - return selectedFormatter.WriteAsync(formatterContext); + await selectedFormatter.WriteAsync(formatterContext); + return true; } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 37cf86f2f566..9350111b0c92 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -116,15 +116,12 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? Log.NoFormatter(Logger, formatterContext, result.ContentTypes); const int statusCode = StatusCodes.Status406NotAcceptable; - - var endpointProvider = context.HttpContext.RequestServices.GetService(); - if (endpointProvider != null && endpointProvider.CanWrite(statusCode)) - { - return endpointProvider.WriteResponse(context.HttpContext, statusCode); - } - context.HttpContext.Response.StatusCode = statusCode; - return Task.CompletedTask; + + var endpointWriter = context.HttpContext.RequestServices.GetService(); + return endpointWriter == null ? + Task.CompletedTask : + endpointWriter.WriteAsync(context.HttpContext); } Log.ObjectResultExecuting(Logger, result, value); diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index 379bfac7cb70..e5fc38a9d36f 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -1,27 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Mvc.Infrastructure; - using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; - private readonly ProblemDetailsOptions _options; + private readonly ProblemDetailsMapper? _matcher; public ProblemDetailsClientErrorFactory( ProblemDetailsFactory problemDetailsFactory, - IOptions options) + ProblemDetailsMapper? matcher = null) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); - _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _matcher = matcher; } public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { - if (!_options.IsEnabled(clientError.StatusCode!.Value)) + if (_matcher != null && + !_matcher.CanMap(actionContext.HttpContext, statusCode: clientError.StatusCode)) { return null; } diff --git a/src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs b/src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs new file mode 100644 index 000000000000..861b9769af3d --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs @@ -0,0 +1,34 @@ +// 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; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc; + +internal sealed class ProblemDetailsApiMapPolicy : IProblemDetailsMapPolicy +{ + private readonly ApiBehaviorOptions _options; + + public ProblemDetailsApiMapPolicy(IOptions options) + { + _options = options.Value; + } + + public bool CanMap(HttpContext context, EndpointMetadataCollection? metadata, int? statusCode, bool isRouting) + { + if (metadata != null) + { + // It is a Controller but not declared as ApiController behavior + // or the SuppressMapClientErrors is true. In this case we will + // not allow ProblemDetails mapping + if (metadata.GetMetadata() != null) + { + return !(metadata.GetMetadata() == null || + _options.SuppressMapClientErrors); + } + } + + return true; + } +} diff --git a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs index 68e78af149bc..3e32e1e79e57 100644 --- a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs @@ -1,8 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Mvc; +using Microsoft.AspNetCore.Http.Metadata; + /// /// Specifies the type returned by default by controllers annotated with . /// @@ -16,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc; /// /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public sealed class ProducesErrorResponseTypeAttribute : Attribute +public sealed class ProducesErrorResponseTypeAttribute : Attribute, IProducesErrorResponseMetadata { /// /// Initializes a new instance of . diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index 7b566c8b556f..a6b64adea423 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -252,13 +252,6 @@ private Dictionary MultiRegistrationServiceTypes typeof(MvcCoreRouteOptionsSetup), } }, - { - typeof(IConfigureOptions), - new Type[] - { - typeof(MvcCoreProblemDetailsOptionsSetup), - } - }, { typeof(IConfigureOptions), new Type[] @@ -331,6 +324,13 @@ private Dictionary MultiRegistrationServiceTypes typeof(DynamicControllerEndpointMatcherPolicy), } }, + { + typeof(IProblemDetailsMapPolicy), + new Type[] + { + typeof(ProblemDetailsApiMapPolicy) + } + }, }; } } diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index e0648cee7ef0..48a0381b6d55 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -29,10 +29,11 @@ "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", - "src\\Http\\Http.ProblemDetails\\src\\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", + "src\\Http\\ProblemDetails.Abstractions\\src\\Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj", + "src\\Http\\ProblemDetails\\src\\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", @@ -148,4 +149,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} +} \ No newline at end of file diff --git a/src/Shared/ProblemDetailsDefaults.cs b/src/Shared/ProblemDetailsDefaults.cs index 065f8ae2a002..6a6deb674e3f 100644 --- a/src/Shared/ProblemDetailsDefaults.cs +++ b/src/Shared/ProblemDetailsDefaults.cs @@ -67,11 +67,7 @@ internal static class ProblemDetailsDefaults ), }; - public static void Apply( - HttpContext httpContext, - ProblemDetails problemDetails, - int? statusCode, - Dictionary errorMapping) + public static void Apply(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")) @@ -90,16 +86,10 @@ public static void Apply( } } - if (errorMapping.TryGetValue(problemDetails.Status.Value, out var mapData)) + if (Defaults.TryGetValue(problemDetails.Status.Value, out var defaults)) { - problemDetails.Title ??= mapData.Title; - problemDetails.Type ??= mapData.Link; - } - - var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; - if (traceId != null) - { - problemDetails.Extensions["traceId"] = traceId; + problemDetails.Title ??= defaults.Title; + problemDetails.Type ??= defaults.Type; } } } From 961830269a3a9a066a93cfe708c5602ef96fb997 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Jun 2022 10:35:08 -0700 Subject: [PATCH 11/59] Using metadata --- .../ProblemDetailsConventionBuilderExtensions.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs index 1c855fdf1372..a4f949a2634b 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs @@ -2,11 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Http.ProblemDetails.DependencyInjection; +namespace Microsoft.Extensions.DependencyInjection; +/// +/// +/// public static class ProblemDetailsConventionBuilderExtensions { + /// + /// + /// + /// + /// + /// public static TBuilder WithProblemDetails(this TBuilder builder) where TBuilder : IEndpointConventionBuilder - => builder.WithMetadata(new TagsAttribute(tags)); + => builder.WithMetadata(new ProblemDetailsResponseMetadata()); } From 8e2591092ca6859ed1ac64d2caeab34a577de887 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Jun 2022 16:35:17 -0700 Subject: [PATCH 12/59] Latest version --- AspNetCore.sln | 19 -- eng/ProjectReferences.props | 1 - eng/SharedFramework.Local.props | 1 - eng/TrimmableProjects.props | 1 - .../IProducesResponseErrorTypeMetadata.cs | 25 -- ...rosoft.AspNetCore.Http.Abstractions.csproj | 4 +- .../HttpValidationProblemDetails.cs | 0 .../ProblemDetails/IProblemDetailsProvider.cs | 30 +++ .../ProblemDetails/IProblemDetailsWriter.cs} | 20 +- .../src/ProblemDetails}/ProblemDetails.cs | 0 .../src/PublicAPI.Unshipped.txt | 8 + ...icrosoft.AspNetCore.Http.Extensions.csproj | 1 - .../src/RequestDelegateFactory.cs | 223 ++++++++++-------- .../Microsoft.AspNetCore.Http.Results.csproj | 2 +- .../Http/src/Builder/ApplicationBuilder.cs | 12 +- .../src/IProblemDetailsMapPolicy.cs | 9 - ...re.Http.ProblemDetails.Abstractions.csproj | 29 --- .../src/PublicAPI.Shipped.txt | 1 - .../src/PublicAPI.Unshipped.txt | 20 -- .../src/DefaultIProblemDetailsProvider.cs | 54 +++++ ...iter.cs => DefaultProblemDetailsWriter.cs} | 42 ++-- ...oblemDetailsConventionBuilderExtensions.cs | 22 -- .../ProblemDetailsOptions.cs | 26 +- ...oblemDetailsServiceCollectionExtensions.cs | 10 +- .../src/Mapping/ProblemDetailsMapper.cs | 67 ------ ...soft.AspNetCore.Http.ProblemDetails.csproj | 6 +- .../src/ProblemDetailsJsonContext.cs | 11 + .../src/ProblemDetailsResponseMetadata.cs | 13 +- .../src/PublicAPI.Unshipped.txt | 12 +- .../OpenApiRouteHandlerBuilderExtensions.cs | 1 + .../src/Matching/AcceptsMatcherPolicy.cs | 12 +- .../src/Matching/HttpMethodMatcherPolicy.cs | 12 +- .../DeveloperExceptionPageMiddleware.cs | 60 +++-- .../ExceptionHandlerExtensions.cs | 3 +- .../ExceptionHandlerMiddleware.cs | 68 +++--- .../Microsoft.AspNetCore.Diagnostics.csproj | 1 - .../Diagnostics/src/PublicAPI.Unshipped.txt | 2 + .../ApiBehaviorApplicationModelProvider.cs | 5 +- ...ApiConventionApplicationModelConvention.cs | 2 +- .../EndpointMetadataConvention.cs | 12 +- .../MvcCoreServiceCollectionExtensions.cs | 3 +- .../src/Formatters/TextOutputFormatter.cs | 13 +- ...r.cs => DefaultApiProblemDetailsWriter.cs} | 56 +++-- .../DefaultProblemDetailsFactory.cs | 13 +- .../Infrastructure/ObjectResultExecutor.cs | 10 +- .../ProblemDetailsClientErrorFactory.cs | 10 +- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 3 + .../src/ProblemDetailsApiMapPolicy.cs | 34 --- .../src/ProducesErrorResponseTypeAttribute.cs | 2 +- .../EndpointMetadataConventionTest.cs | 2 +- .../MvcCoreServiceCollectionExtensionsTest.cs | 7 - src/OpenApi/src/OpenApiGenerator.cs | 8 +- 52 files changed, 522 insertions(+), 486 deletions(-) delete mode 100644 src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs rename src/Http/{ProblemDetails.Abstractions/src => Http.Abstractions/src/ProblemDetails}/HttpValidationProblemDetails.cs (100%) create mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs rename src/Http/{ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs => Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs} (72%) rename src/Http/{ProblemDetails.Abstractions/src => Http.Abstractions/src/ProblemDetails}/ProblemDetails.cs (100%) delete mode 100644 src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs delete mode 100644 src/Http/ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj delete mode 100644 src/Http/ProblemDetails.Abstractions/src/PublicAPI.Shipped.txt delete mode 100644 src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt create mode 100644 src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.cs rename src/Http/ProblemDetails/src/{DefaultProblemDetailsEndpointWriter.cs => DefaultProblemDetailsWriter.cs} (57%) delete mode 100644 src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs delete mode 100644 src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs create mode 100644 src/Http/ProblemDetails/src/ProblemDetailsJsonContext.cs rename src/Mvc/Mvc.Core/src/Infrastructure/{DefaultProblemDetailsWriter.cs => DefaultApiProblemDetailsWriter.cs} (65%) delete mode 100644 src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 35c0b44d1f4b..e0d26dab4a8e 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1722,8 +1722,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.ProblemDetails", "Http EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.ProblemDetails", "src\Http\ProblemDetails\src\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "{2333E682-F9E9-4235-BF63-6403C114EA76}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspnetCore.Http.ProblemDetails.Abstractions", "src\Http\ProblemDetails.Abstractions\src\Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj", "{F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression.Microbenchmarks", "src\Middleware\RequestDecompression\perf\Microbenchmarks\Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj", "{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}" @@ -10343,22 +10341,6 @@ Global {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x64.Build.0 = Release|Any CPU {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.ActiveCfg = Release|Any CPU {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.Build.0 = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|arm64.ActiveCfg = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|arm64.Build.0 = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x64.ActiveCfg = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x64.Build.0 = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x86.ActiveCfg = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Debug|x86.Build.0 = Debug|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|Any CPU.Build.0 = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|arm64.ActiveCfg = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|arm64.Build.0 = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x64.ActiveCfg = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x64.Build.0 = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x86.ActiveCfg = Release|Any CPU - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A}.Release|x86.Build.0 = Release|Any CPU {3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.Build.0 = Debug|Any CPU {3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -11276,7 +11258,6 @@ Global {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} {41AF137D-4181-42F9-9B53-BEDB9532F29B} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724} {2333E682-F9E9-4235-BF63-6403C114EA76} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} - {F3B3A64A-FF8D-43E1-84BE-CA5AB39F683A} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} {3309FA1E-4E95-49E9-BE2A-827D01FD63C0} = {5465F96F-33D5-454E-9C40-494E58AEEE5D} {B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730} {AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 6781e1cacedd..dd26f2ad26ed 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -32,7 +32,6 @@ - diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 2c2c29ee1386..c8c693a59162 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -50,7 +50,6 @@ - diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index 57806e79da0a..e344ab9c9a2a 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -26,7 +26,6 @@ - diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs deleted file mode 100644 index 72afc6cadcae..000000000000 --- a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseErrorTypeMetadata.cs +++ /dev/null @@ -1,25 +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.Metadata; - - -/// -/// Specifies the type returned by default by controllers annotated with . -/// -/// specifies the error model type associated with a -/// for a client error (HTTP Status Code 4xx) when no value is provided. When no value is specified, MVC assumes the -/// client error type to be , if mapping client errors () -/// is used. -/// -/// -/// Use this to configure the default error type if your application uses a custom error type to respond. -/// -/// -public interface IProducesErrorResponseMetadata -{ - /// - /// Gets the optimistic return type of the action. - /// - Type? Type { get; } -} diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 4c75d68b0ea1..1a8f0ff06b71 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -1,4 +1,4 @@ - + @@ -26,6 +26,8 @@ Microsoft.AspNetCore.Http.HttpResponse + + diff --git a/src/Http/ProblemDetails.Abstractions/src/HttpValidationProblemDetails.cs b/src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.cs similarity index 100% rename from src/Http/ProblemDetails.Abstractions/src/HttpValidationProblemDetails.cs rename to src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.cs diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs new file mode 100644 index 000000000000..ee120e2b8380 --- /dev/null +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// +/// +public interface IProblemDetailsProvider +{ + /// + /// + /// + /// + /// + /// + bool IsEnabled(int statusCode, bool isRouting = false); + + /// + /// + /// + /// + /// + /// + /// + IProblemDetailsWriter? GetWriter( + HttpContext context, + EndpointMetadataCollection? currentMetadata = null, + bool isRouting = false); +} diff --git a/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs similarity index 72% rename from src/Http/ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs rename to src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index de9c79ee978d..3ee0a0613821 100644 --- a/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsEndpointWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -1,15 +1,25 @@ // 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.Mvc; - namespace Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + /// /// /// -public interface IProblemDetailsEndpointWriter +public interface IProblemDetailsWriter { + /// + /// + /// + /// + /// + /// + /// + /// + bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting); + /// /// /// @@ -24,10 +34,8 @@ public interface IProblemDetailsEndpointWriter /// /// /// - Task WriteAsync( + Task WriteAsync( HttpContext context, - EndpointMetadataCollection? metadata = null, - bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, diff --git a/src/Http/ProblemDetails.Abstractions/src/ProblemDetails.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs similarity index 100% rename from src/Http/ProblemDetails.Abstractions/src/ProblemDetails.cs rename to src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index b8d0ab94d187..685a581c78ce 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -9,11 +9,17 @@ Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext.DefaultRouteHandlerInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! arguments) -> void Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator.Current.get -> object! Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T! +Microsoft.AspNetCore.Http.IProblemDetailsMapPolicy +Microsoft.AspNetCore.Http.IProblemDetailsMapPolicy.CanMap(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, int statusCode, bool isRouting) -> bool Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long? +Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter +Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.CanWrite(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata = null, int? statusCode = null, bool isRouting = false) -> bool +Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.Policies.get -> Microsoft.AspNetCore.Http.IProblemDetailsMapPolicy![]! +Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.ProblemDetailsEndpointWriter(System.Collections.Generic.IEnumerable! policies) -> void Microsoft.AspNetCore.Http.RouteHandlerContext Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection! @@ -32,6 +38,8 @@ Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata.Description.get -> string! Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata.Summary.get -> string! +abstract Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.IsEnabled(int statusCode, bool isRouting = false) -> bool +abstract Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata = null, bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null, System.Action? configureDetails = null) -> System.Threading.Tasks.Task! abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.Arguments.get -> System.Collections.Generic.IList! abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.GetArgument(int index) -> T abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 4f86bd083fc8..dd196773100a 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -21,7 +21,6 @@ - diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 265351805ef1..3a27b784b187 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -57,6 +57,8 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ArrayEmptyOfObjectMethod = typeof(Array).GetMethod(nameof(Array.Empty), BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(new Type[] { typeof(object) }); + private static readonly MethodInfo ProblemDetailsResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WriteProblemResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + // Call WriteAsJsonAsync() to serialize the runtime return type rather than the declared return type. // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism @@ -194,6 +196,8 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions context.Metadata.AddRange(options.InitialEndpointMetadata); } + context.CanGenerateProblemDetails = context.ServiceProviderIsService?.IsService(typeof(IProblemDetailsProvider)) == true; + return context; } @@ -252,7 +256,7 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions var responseWritingMethodCall = factoryContext.ParamCheckExpressions.Count > 0 ? CreateParamCheckingResponseWritingMethodCall(returnType, factoryContext) : - AddResponseWritingToMethodCall(factoryContext.MethodCall, returnType); + AddResponseWritingToMethodCall(factoryContext.MethodCall, returnType, factoryContext.CanGenerateProblemDetails); if (factoryContext.UsingTempSourceString) { @@ -785,8 +789,10 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(Type retu var checkWasParamCheckFailureWithFilters = Expression.Block( Expression.IfThen( WasParamCheckFailureExpr, - Expression.Assign(StatusCodeExpr, Expression.Constant(400))), - AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType) + Expression.Block( + Expression.Assign(StatusCodeExpr, Expression.Constant(400)), + Expression.Call(ProblemDetailsResponseAsyncMethod, HttpContextExpr))), + AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext.CanGenerateProblemDetails) ); checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailureWithFilters; @@ -803,133 +809,157 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(Type retu WasParamCheckFailureExpr, Expression.Block( Expression.Assign(StatusCodeExpr, Expression.Constant(400)), - CompletedTaskExpr), - AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType)); + Expression.Call(ProblemDetailsResponseAsyncMethod, HttpContextExpr)), + AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext.CanGenerateProblemDetails)); checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure; } return Expression.Block(localVariables, checkParamAndCallMethod); } - private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType) + private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType, bool canGenerateProblemDetails) { - // Exact request delegate match - if (returnType == typeof(void)) - { - return Expression.Block(methodCall, CompletedTaskExpr); - } - else if (returnType == typeof(object)) - { - return Expression.Call(ExecuteObjectReturnMethod, methodCall, HttpContextExpr); - } - else if (returnType == typeof(ValueTask)) + static Expression ResponseWriteMethod(Expression methodCall, Type returnType) { - return Expression.Call(ExecuteValueTaskOfObjectMethod, - methodCall, - HttpContextExpr); - } - else if (returnType == typeof(Task)) - { - return Expression.Call(ExecuteTaskOfObjectMethod, - methodCall, - HttpContextExpr); - } - else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) - { - if (returnType == typeof(Task)) + // Exact request delegate match + if (returnType == typeof(void)) { - return methodCall; + return Expression.Block(methodCall, CompletedTaskExpr); } - else if (returnType == typeof(ValueTask)) + else if (returnType == typeof(object)) { - return Expression.Call( - ExecuteValueTaskMethod, - methodCall); + return Expression.Call(ExecuteObjectReturnMethod, methodCall, HttpContextExpr); } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(Task<>)) + else if (returnType == typeof(ValueTask)) { - var typeArg = returnType.GetGenericArguments()[0]; - - if (typeof(IResult).IsAssignableFrom(typeArg)) + return Expression.Call(ExecuteValueTaskOfObjectMethod, + methodCall, + HttpContextExpr); + } + else if (returnType == typeof(Task)) + { + return Expression.Call(ExecuteTaskOfObjectMethod, + methodCall, + HttpContextExpr); + } + else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) + { + if (returnType == typeof(Task)) { - return Expression.Call( - ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); + return methodCall; } - // ExecuteTask(handler(..), httpContext); - else if (typeArg == typeof(string)) + else if (returnType == typeof(ValueTask)) { return Expression.Call( - ExecuteTaskOfStringMethod, - methodCall, - HttpContextExpr); + ExecuteValueTaskMethod, + methodCall); } - else + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(Task<>)) { - return Expression.Call( - ExecuteTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } - } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - var typeArg = returnType.GetGenericArguments()[0]; + var typeArg = returnType.GetGenericArguments()[0]; - if (typeof(IResult).IsAssignableFrom(typeArg)) - { - return Expression.Call( - ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); + if (typeof(IResult).IsAssignableFrom(typeArg)) + { + return Expression.Call( + ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); + } + // ExecuteTask(handler(..), httpContext); + else if (typeArg == typeof(string)) + { + return Expression.Call( + ExecuteTaskOfStringMethod, + methodCall, + HttpContextExpr); + } + else + { + return Expression.Call( + ExecuteTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); + } } - // ExecuteTask(handler(..), httpContext); - else if (typeArg == typeof(string)) + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) { - return Expression.Call( - ExecuteValueTaskOfStringMethod, - methodCall, - HttpContextExpr); + var typeArg = returnType.GetGenericArguments()[0]; + + if (typeof(IResult).IsAssignableFrom(typeArg)) + { + return Expression.Call( + ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); + } + // ExecuteTask(handler(..), httpContext); + else if (typeArg == typeof(string)) + { + return Expression.Call( + ExecuteValueTaskOfStringMethod, + methodCall, + HttpContextExpr); + } + else + { + return Expression.Call( + ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); + } } else { - return Expression.Call( - ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); + // TODO: Handle custom awaitables + throw new NotSupportedException($"Unsupported return type: {returnType}"); } } - else + else if (typeof(IResult).IsAssignableFrom(returnType)) { - // TODO: Handle custom awaitables - throw new NotSupportedException($"Unsupported return type: {returnType}"); + if (returnType.IsValueType) + { + var box = Expression.TypeAs(methodCall, typeof(IResult)); + return Expression.Call(ResultWriteResponseAsyncMethod, box, HttpContextExpr); + } + return Expression.Call(ResultWriteResponseAsyncMethod, methodCall, HttpContextExpr); } - } - else if (typeof(IResult).IsAssignableFrom(returnType)) - { - if (returnType.IsValueType) + else if (returnType == typeof(string)) { - var box = Expression.TypeAs(methodCall, typeof(IResult)); - return Expression.Call(ResultWriteResponseAsyncMethod, box, HttpContextExpr); + return Expression.Call(StringResultWriteResponseAsyncMethod, HttpContextExpr, methodCall); + } + else if (returnType.IsValueType) + { + var box = Expression.TypeAs(methodCall, typeof(object)); + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); + } + else + { + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); } - return Expression.Call(ResultWriteResponseAsyncMethod, methodCall, HttpContextExpr); - } - else if (returnType == typeof(string)) - { - return Expression.Call(StringResultWriteResponseAsyncMethod, HttpContextExpr, methodCall); - } - else if (returnType.IsValueType) - { - var box = Expression.TypeAs(methodCall, typeof(object)); - return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); } - else + + var responseWriteCall = ResponseWriteMethod(methodCall, returnType); + + return canGenerateProblemDetails ? + Expression.Block(responseWriteCall, Expression.Call(ProblemDetailsResponseAsyncMethod, HttpContextExpr)) : + responseWriteCall; + } + + private static Task WriteProblemResponseAsync(HttpContext context) + { + if (!context.Response.HasStarted && + context.Response.StatusCode >= 400) { - return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + return Task.CompletedTask; + //var endpointWriter = context.RequestServices.GetService(); + //return endpointWriter == null ? + // Task.CompletedTask : + // endpointWriter.WriteAsync(context); } + + return Task.CompletedTask; } private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) @@ -2063,6 +2093,7 @@ private sealed class FactoryContext public List? RouteParameters { get; init; } public bool ThrowOnBadRequest { get; init; } public bool DisableInferredFromBody { get; init; } + public bool CanGenerateProblemDetails { get; set; } // Temporary State public ParameterInfo? JsonRequestBodyParameter { get; set; } diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index 3bc5c99c163d..de0934e22002 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -16,6 +16,7 @@ + @@ -24,7 +25,6 @@ - diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index 063ec3ef3645..d0642f4d55cd 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -133,10 +133,14 @@ public RequestDelegate Build() const int statusCode = StatusCodes.Status404NotFound; context.Response.StatusCode = statusCode; - var endpointWriter = context.RequestServices.GetService(); - return endpointWriter == null ? - Task.CompletedTask : - endpointWriter.WriteAsync(context); + var endpointProvider = context.RequestServices.GetService(); + if (endpointProvider != null && + endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) + { + return problemDetailsEndpoint.WriteAsync(context); + } + + return Task.CompletedTask; }; for (var c = _components.Count - 1; c >= 0; c--) diff --git a/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs b/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs deleted file mode 100644 index 86d3d7a15a34..000000000000 --- a/src/Http/ProblemDetails.Abstractions/src/IProblemDetailsMapPolicy.cs +++ /dev/null @@ -1,9 +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; - -public interface IProblemDetailsMapPolicy -{ - bool CanMap(HttpContext context, EndpointMetadataCollection? metadata, int? statusCode, bool isRouting); -} diff --git a/src/Http/ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj b/src/Http/ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj deleted file mode 100644 index 11a3e6c3bfb3..000000000000 --- a/src/Http/ProblemDetails.Abstractions/src/Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - ASP.NET Core. - $(DefaultNetCoreTargetFramework) - true - true - aspnetcore - false - true - - - - - - - - - - - - - - - - - - - diff --git a/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Shipped.txt b/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Shipped.txt deleted file mode 100644 index 7dc5c58110bf..000000000000 --- a/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt deleted file mode 100644 index 9c9b3e81e77c..000000000000 --- a/src/Http/ProblemDetails.Abstractions/src/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,20 +0,0 @@ -#nullable enable -Microsoft.AspNetCore.Http.HttpValidationProblemDetails -Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! -Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void -Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void -Microsoft.AspNetCore.Http.IProblemDetailsEndpointWriter -Microsoft.AspNetCore.Http.IProblemDetailsEndpointWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata = null, bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null, System.Action? configureDetails = null) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Mvc.ProblemDetails -Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! -Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? -Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void -Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? -Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void diff --git a/src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.cs b/src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.cs new file mode 100644 index 000000000000..6021ca17f8f4 --- /dev/null +++ b/src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.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. + +using System.Linq; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultProblemDetailsEndpointProvider : IProblemDetailsProvider +{ + private readonly ProblemDetailsOptions _options; + private readonly IProblemDetailsWriter[] _writers; + + public DefaultProblemDetailsEndpointProvider( + IEnumerable writers, + IOptions options) + { + _options = options.Value; + _writers = writers.ToArray(); + } + + public IProblemDetailsWriter? GetWriter(HttpContext context, EndpointMetadataCollection? currentMetadata = null, bool isRouting = false) + { + if (IsEnabled(context.Response.StatusCode, isRouting)) + { + currentMetadata ??= context.GetEndpoint()?.Metadata; + + for (var i = 0; i < _writers.Length; i++) + { + if (_writers[i].CanWrite(context, currentMetadata, isRouting: isRouting)) + { + return _writers[i]; + } + } + } + + return null; + } + + public bool IsEnabled(int statusCode, bool isRouting = false) + { + if (_options.AllowedMapping == MappingOptions.Unspecified) + { + return false; + } + + return isRouting ? _options.AllowedMapping.HasFlag(MappingOptions.RoutingFailures) : statusCode switch + { + >= 400 and <= 499 => _options.AllowedMapping.HasFlag(MappingOptions.ClientErrors), + >= 500 => _options.AllowedMapping.HasFlag(MappingOptions.Exceptions), + _ => false, + }; + } +} diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsEndpointWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs similarity index 57% rename from src/Http/ProblemDetails/src/DefaultProblemDetailsEndpointWriter.cs rename to src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index 830f5461253c..cc0b5c889d76 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsEndpointWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -1,28 +1,42 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http; -internal sealed class DefaultProblemDetailsEndpointWriter : IProblemDetailsEndpointWriter +internal sealed class DefaultProblemDetailsWriter : IProblemDetailsWriter { - private readonly ProblemDetailsMapper? _mapper; private readonly ProblemDetailsOptions _options; - public DefaultProblemDetailsEndpointWriter( - IOptions options, - ProblemDetailsMapper? mapper = null) + public DefaultProblemDetailsWriter(IOptions options) { - _mapper = mapper; _options = options.Value; } - public async Task WriteAsync( + public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) + { + if (isRouting || context.Response.StatusCode >= 500) + { + return true; + } + + var problemDetailsMetadata = metadata?.GetMetadata(); + return problemDetailsMetadata != null; + + //var headers = context.Request.GetTypedHeaders(); + //var acceptHeader = headers.Accept; + //if (acceptHeader != null && + // !acceptHeader.Any(h => _problemMediaType.IsSubsetOf(h))) + //{ + // return false; + //} + } + + public Task WriteAsync( HttpContext context, - EndpointMetadataCollection? metadata = null, - bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, @@ -31,12 +45,6 @@ public async Task WriteAsync( IDictionary? extensions = null, Action? configureDetails = null) { - if (_mapper == null || - !_mapper.CanMap(context, metadata: metadata, isRouting: isRouting)) - { - return false; - } - var problemDetails = new ProblemDetails { Status = statusCode, @@ -59,7 +67,7 @@ public async Task WriteAsync( _options.ConfigureDetails?.Invoke(context, problemDetails); configureDetails?.Invoke(context, problemDetails); - await context.Response.WriteAsJsonAsync(problemDetails, options: null, "application/problem+json"); - return true; + return context.Response.WriteAsJsonAsync(problemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); } } + diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs deleted file mode 100644 index a4f949a2634b..000000000000 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsConventionBuilderExtensions.cs +++ /dev/null @@ -1,22 +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.Builder; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// -/// -public static class ProblemDetailsConventionBuilderExtensions -{ - /// - /// - /// - /// - /// - /// - public static TBuilder WithProblemDetails(this TBuilder builder) where TBuilder : IEndpointConventionBuilder - => builder.WithMetadata(new ProblemDetailsResponseMetadata()); -} diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs index f0b2c35b696e..91e8e8c5e928 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs @@ -13,16 +13,12 @@ public class ProblemDetailsOptions /// /// /// - public MappingOptions Mapping { get; set; } = MappingOptions.ClientErrors | MappingOptions.Exceptions; - public Action? ConfigureDetails { get; set; } + public MappingOptions AllowedMapping { get; set; } = MappingOptions.Unspecified; - public bool IsEnabled(int statusCode, bool isRouting = false) - => isRouting ? Mapping.HasFlag(MappingOptions.Routing) : statusCode switch - { - >= 400 and <= 499 => Mapping.HasFlag(MappingOptions.ClientErrors), - >= 500 => Mapping.HasFlag(MappingOptions.Exceptions), - _ => false, - }; + /// + /// + /// + public Action? ConfigureDetails { get; set; } } /// @@ -34,25 +30,25 @@ public enum MappingOptions : uint /// /// /// - None = 0, + Unspecified = 0, /// /// /// - ClientErrors = 1, + Exceptions = 1, /// - /// + /// 404 / 405 / 415 /// - Routing = 2, + RoutingFailures = 2, /// /// /// - Exceptions = 4, + ClientErrors = 4, /// /// /// - All = ClientErrors | Routing | Exceptions + All = RoutingFailures | Exceptions | ClientErrors, } diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs index 9ce232c9966c..0657c17c0501 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs @@ -16,15 +16,19 @@ public static class ProblemDetailsServiceCollectionExtensions /// Adds services required for creation of for failed requests. /// /// The to add the services to. + /// /// The so that additional calls can be chained. public static IServiceCollection AddProblemDetails( - this IServiceCollection services) + this IServiceCollection services, + MappingOptions allowedMapping = MappingOptions.All) { ArgumentNullException.ThrowIfNull(nameof(services)); // Adding default services; - services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + services.Configure(options => options.AllowedMapping = allowedMapping); return services; } diff --git a/src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs b/src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs deleted file mode 100644 index 14759ce67202..000000000000 --- a/src/Http/ProblemDetails/src/Mapping/ProblemDetailsMapper.cs +++ /dev/null @@ -1,67 +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.Linq; -using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Http; - -internal class ProblemDetailsMapper -{ - private readonly IProblemDetailsMapPolicy[] _matchPolicies; - private readonly ProblemDetailsOptions _options; - - private static readonly MediaTypeHeaderValue _problemMediaType = new("application/problem+*"); - - public ProblemDetailsMapper( - IEnumerable matchPolicies, - IOptions options) - { - _matchPolicies = matchPolicies.ToArray(); - _options = options.Value; - } - - public bool CanMap( - HttpContext context, - EndpointMetadataCollection? metadata = null, - int? statusCode = null, - bool isRouting = false) - { - metadata ??= context.GetEndpoint()?.Metadata; - - if (!_options.IsEnabled(statusCode ?? context.Response.StatusCode, isRouting)) - { - return false; - } - - var headers = context.Request.GetTypedHeaders(); - var acceptHeader = headers.Accept; - - if (acceptHeader != null && - !acceptHeader.Any(h => _problemMediaType.IsSubsetOf(h))) - { - return false; - } - - // What if we don't have the endpoint (eg. Routing) - - var responseType = metadata?.GetMetadata(); - if (responseType == null || !typeof(ProblemDetails).IsAssignableFrom(responseType.Type)) - { - return false; - } - - for (var i = _matchPolicies.Length; i > 0; i--) - { - if (!_matchPolicies[i - 1].CanMap(context, metadata, statusCode, isRouting)) - { - return false; - } - } - - return true; - } -} diff --git a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj index 1541356c78bb..a1537ec05fb3 100644 --- a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj +++ b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj @@ -12,12 +12,12 @@ - - + + - + diff --git a/src/Http/ProblemDetails/src/ProblemDetailsJsonContext.cs b/src/Http/ProblemDetails/src/ProblemDetailsJsonContext.cs new file mode 100644 index 000000000000..64f5f07d6815 --- /dev/null +++ b/src/Http/ProblemDetails/src/ProblemDetailsJsonContext.cs @@ -0,0 +1,11 @@ +// 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.Serialization; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.AspNetCore.Http; + +[JsonSerializable(typeof(ProblemDetails))] +internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext +{ } diff --git a/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs b/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs index 800aa87bf017..43d24ce6f0c4 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs @@ -1,12 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; namespace Microsoft.AspNetCore.Http; -internal class ProblemDetailsResponseMetadata : IProducesErrorResponseMetadata +internal sealed class ProblemDetailsResponseMetadata { - public Type? Type => typeof(ProblemDetails); + /// + /// Gets the default error type. + /// + public Type Type { get; } = typeof(ProblemDetails); + + /// + /// + /// + public IEnumerable ContentTypes { get; } = new string[] { "application/problem+json" }; } diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt index e48291d6d669..1f041d56a149 100644 --- a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt +++ b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt @@ -1,6 +1,16 @@ #nullable enable +Microsoft.AspNetCore.Http.MappingOptions +Microsoft.AspNetCore.Http.MappingOptions.All = Microsoft.AspNetCore.Http.MappingOptions.Exceptions | Microsoft.AspNetCore.Http.MappingOptions.RoutingFailures | Microsoft.AspNetCore.Http.MappingOptions.ClientErrors -> Microsoft.AspNetCore.Http.MappingOptions +Microsoft.AspNetCore.Http.MappingOptions.ClientErrors = 4 -> Microsoft.AspNetCore.Http.MappingOptions +Microsoft.AspNetCore.Http.MappingOptions.Exceptions = 1 -> Microsoft.AspNetCore.Http.MappingOptions +Microsoft.AspNetCore.Http.MappingOptions.RoutingFailures = 2 -> Microsoft.AspNetCore.Http.MappingOptions +Microsoft.AspNetCore.Http.MappingOptions.Unspecified = 0 -> Microsoft.AspNetCore.Http.MappingOptions Microsoft.AspNetCore.Http.ProblemDetailsOptions +Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.get -> Microsoft.AspNetCore.Http.MappingOptions +Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.set -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.get -> System.Action? +Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Http.MappingOptions allowedMapping = Microsoft.AspNetCore.Http.MappingOptions.All) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs index 2615e70e1ac9..e9c3145b3e93 100644 --- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.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.Net.Mime; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index ee4d26cb995e..92c7a6b8d4bf 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -265,10 +265,14 @@ private static Endpoint CreateRejectionEndpoint() const int statusCode = StatusCodes.Status415UnsupportedMediaType; context.Response.StatusCode = statusCode; - var endpointWriter = context.RequestServices.GetService(); - return endpointWriter == null ? - Task.CompletedTask : - endpointWriter.WriteAsync(context); + var endpointProvider = context.RequestServices.GetService(); + if (endpointProvider != null && + endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) + { + return problemDetailsEndpoint.WriteAsync(context); + } + + return Task.CompletedTask; }, EndpointMetadataCollection.Empty, Http415EndpointDisplayName); diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index b924d220299a..961c4420bb51 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -412,10 +412,14 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods const int statusCode = StatusCodes.Status405MethodNotAllowed; context.Response.StatusCode = statusCode; - var endpointWriter = context.RequestServices.GetService(); - return endpointWriter == null ? - Task.CompletedTask : - endpointWriter.WriteAsync(context); + var endpointProvider = context.RequestServices.GetService(); + if (endpointProvider != null && + endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) + { + return problemDetailsEndpoint.WriteAsync(context); + } + + return Task.CompletedTask; }, EndpointMetadataCollection.Empty, Http405EndpointDisplayName); diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 979c71a2ad24..38d12811239b 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -9,8 +9,12 @@ using Microsoft.AspNetCore.Diagnostics.RazorViews; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.StackTrace.Sources; @@ -31,6 +35,7 @@ public class DeveloperExceptionPageMiddleware private readonly ExceptionDetailsProvider _exceptionDetailsProvider; private readonly Func _exceptionHandler; private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); + private readonly IProblemDetailsProvider? _problemDetailsProvider; /// /// Initializes a new instance of the class @@ -41,13 +46,15 @@ public class DeveloperExceptionPageMiddleware /// /// /// + /// public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions options, ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, - IEnumerable filters) + IEnumerable filters, + IProblemDetailsProvider? problemDetailsProvider = null) { if (next == null) { @@ -71,6 +78,7 @@ public DeveloperExceptionPageMiddleware( _diagnosticSource = diagnosticSource; _exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _logger, _options.SourceCodeLineCount); _exceptionHandler = DisplayException; + _problemDetailsProvider = problemDetailsProvider; foreach (var filter in filters.Reverse()) { @@ -148,19 +156,7 @@ private Task DisplayException(ErrorContext errorContext) // If the client does not ask for HTML just format the exception as plain text if (acceptHeader == null || !acceptHeader.Any(h => h.IsSubsetOf(_textHtmlMediaType))) { - httpContext.Response.ContentType = "text/plain; charset=utf-8"; - - var sb = new StringBuilder(); - sb.AppendLine(errorContext.Exception.ToString()); - sb.AppendLine(); - sb.AppendLine("HEADERS"); - sb.AppendLine("======="); - foreach (var pair in httpContext.Request.Headers) - { - sb.AppendLine(FormattableString.Invariant($"{pair.Key}: {pair.Value}")); - } - - return httpContext.Response.WriteAsync(sb.ToString()); + return DisplayExceptionContent(errorContext); } if (errorContext.Exception is ICompilationException compilationException) @@ -171,6 +167,42 @@ private Task DisplayException(ErrorContext errorContext) return DisplayRuntimeException(httpContext, errorContext.Exception); } + private Task DisplayExceptionContent(ErrorContext errorContext) + { + var httpContext = errorContext.HttpContext; + + if (_problemDetailsProvider?.GetWriter(httpContext) is { } writer) + { + void ConfigureDetails(HttpContext context, ProblemDetails problemDetails) + { + problemDetails.Extensions.Add("exception", new + { + Headers = httpContext.Request.Headers, + Error = errorContext.Exception.ToString(), + Path = context.Request.Path, + Endpoint = context.GetEndpoint()?.ToString(), + RouteValues = context.Features.Get()?.RouteValues, + }); + } + + return writer.WriteAsync(httpContext, configureDetails: ConfigureDetails); + } + + httpContext.Response.ContentType = "text/plain; charset=utf-8"; + + var sb = new StringBuilder(); + sb.AppendLine(errorContext.Exception.ToString()); + sb.AppendLine(); + sb.AppendLine("HEADERS"); + sb.AppendLine("======="); + foreach (var pair in httpContext.Request.Headers) + { + sb.AppendLine(FormattableString.Invariant($"{pair.Key}: {pair.Value}")); + } + + return httpContext.Response.WriteAsync(sb.ToString()); + } + private Task DisplayCompilationException( HttpContext context, ICompilationException compilationException) diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index 602b1bc2172d..d59d3cbc5bf5 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -131,7 +131,8 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui options.Value.ExceptionHandler = builder.Build(); } - return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener).Invoke; + var problemDetailsProvider = app.ApplicationServices.GetService(); + return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsProvider).Invoke; }); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index 48434f74ce3d..f677be0cc69e 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -20,11 +20,14 @@ namespace Microsoft.AspNetCore.Diagnostics; /// public class ExceptionHandlerMiddleware { + private const int DefaultStatusCode = StatusCodes.Status500InternalServerError; + private readonly RequestDelegate _next; private readonly ExceptionHandlerOptions _options; private readonly ILogger _logger; private readonly Func _clearCacheHeadersDelegate; private readonly DiagnosticListener _diagnosticListener; + private readonly IProblemDetailsProvider? _problemDetailsProvider; /// /// Creates a new @@ -38,17 +41,39 @@ public ExceptionHandlerMiddleware( ILoggerFactory loggerFactory, IOptions options, DiagnosticListener diagnosticListener) + : this(next, loggerFactory, options, diagnosticListener, null) + { } + + /// + /// Creates a new + /// + /// The representing the next middleware in the pipeline. + /// The used for logging. + /// The options for configuring the middleware. + /// The used for writing diagnostic messages. + /// The used for writing messages. + public ExceptionHandlerMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions options, + DiagnosticListener diagnosticListener, + IProblemDetailsProvider? problemDetailsProvider = null) { _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger(); _clearCacheHeadersDelegate = ClearCacheHeaders; _diagnosticListener = diagnosticListener; + _problemDetailsProvider = problemDetailsProvider; + if (_options.ExceptionHandler == null) { if (_options.ExceptionHandlingPath == null) { - throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); + if (_problemDetailsProvider == null || _problemDetailsProvider.IsEnabled(statusCode: DefaultStatusCode) == false) + { + throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); + } } else { @@ -131,13 +156,21 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed context.Features.Set(exceptionHandlerFeature); context.Features.Set(exceptionHandlerFeature); - context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.StatusCode = DefaultStatusCode; context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response); - if (!await TryWriteProblemDetails(context, exceptionHandlerFeature)) + if (_options.ExceptionHandler != null) { await _options.ExceptionHandler!(context); } + else + { + var writer = _problemDetailsProvider!.GetWriter(context, exceptionHandlerFeature.Endpoint?.Metadata); + if (writer != null) + { + await writer.WriteAsync(context); + } + } // If the response has already started, assume exception handler was successful. if (context.Response.HasStarted || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response) @@ -196,33 +229,4 @@ private static Task ClearCacheHeaders(object state) headers.ETag = default; return Task.CompletedTask; } - - private Task TryWriteProblemDetails(HttpContext context, ExceptionHandlerFeature exceptionFeature) - { - void ConfigureDetails(HttpContext context, ProblemDetails problemDetails) - { - var hostEnvironment = context.RequestServices.GetRequiredService(); - - if (hostEnvironment.IsDevelopment()) - { - // TODO: Fix - problemDetails.Detail = exceptionFeature!.Error.Message; - problemDetails.Extensions.Add("exception", new - { - Details = exceptionFeature.Error.ToString(), - exceptionFeature.Path, - exceptionFeature.RouteValues, - Endpoint = exceptionFeature.Endpoint?.ToString() - }); - } - } - - var endpointWriter = context.RequestServices.GetService(); - if (endpointWriter != null) - { - return endpointWriter.WriteAsync(context, exceptionFeature.Endpoint?.Metadata, configureDetails: ConfigureDetails); - } - - return Task.FromResult(false); - } } diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index 584056b42a44..ebf506b0a0ae 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -21,7 +21,6 @@ - diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index a8796b96379e..125cf3fef42e 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter? problemDetailsWriter = null) -> void +Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.IProblemDetailsProvider? problemDetailsProvider = null) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs index 09e0d633f395..2f70ddb08544 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs @@ -20,10 +20,12 @@ public ApiBehaviorApplicationModelProvider( { var options = apiBehaviorOptions.Value; + var defaultErrorType = options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails); + ActionModelConventions = new List() { new ApiVisibilityConvention(), - new EndpointMetadataConvention(serviceProvider) + new EndpointMetadataConvention(serviceProvider, defaultErrorType) }; if (!options.SuppressMapClientErrors) @@ -41,7 +43,6 @@ public ApiBehaviorApplicationModelProvider( ActionModelConventions.Add(new ConsumesConstraintForFormFileParameterConvention()); } - var defaultErrorType = options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails); var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType); ActionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute)); diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiConventionApplicationModelConvention.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiConventionApplicationModelConvention.cs index a89c06c34fe4..cba692149f27 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiConventionApplicationModelConvention.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiConventionApplicationModelConvention.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs index b830993746ab..3668139126f3 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs @@ -12,14 +12,24 @@ internal sealed class EndpointMetadataConvention : IActionModelConvention private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointMetadataConvention).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointMetadataConvention).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; private readonly IServiceProvider _serviceProvider; + private readonly Type _defaultErrorType; - public EndpointMetadataConvention(IServiceProvider serviceProvider) + public EndpointMetadataConvention(IServiceProvider serviceProvider, Type defaultErrorType) { _serviceProvider = serviceProvider; + _defaultErrorType = defaultErrorType; } public void Apply(ActionModel action) { + if (_defaultErrorType != typeof(void)) + { + for (var i = 0; i < action.Selectors.Count; i++) + { + action.Selectors[i].EndpointMetadata.Add(new ProducesErrorResponseTypeAttribute(_defaultErrorType)); + } + } + // Get metadata from parameter types ApplyParametersMetadata(action); diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 5ef6f62372a3..f81bd4df0bb3 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -284,8 +284,7 @@ internal static void AddMvcCoreServices(IServiceCollection services) // ProblemDetails services.TryAddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); } private static void ConfigureDefaultServices(IServiceCollection services) diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index afc2863a4733..676e5ba63c4d 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -136,10 +137,14 @@ public override Task WriteAsync(OutputFormatterWriteContext context) const int statusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = statusCode; - var endpointWriter = context.HttpContext.RequestServices.GetService(); - return endpointWriter == null ? - Task.CompletedTask : - endpointWriter.WriteAsync(context.HttpContext); + var provider = context.HttpContext.RequestServices.GetService(); + if (provider != null && + provider.GetWriter(context.HttpContext) is { } problemDetailsWriter) + { + return problemDetailsWriter.WriteAsync(context.HttpContext); + } + + return Task.CompletedTask; } context.ContentType = selectedMediaType; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs similarity index 65% rename from src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsWriter.cs rename to src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index 33d87b38ad09..efa7d6b7debd 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -3,18 +3,16 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Mvc.Core.Infrastructure; +namespace Microsoft.AspNetCore.Mvc.Infrastructure; -internal class DefaultProblemDetailsWriter : IProblemDetailsEndpointWriter +internal class DefaultApiProblemDetailsWriter : IProblemDetailsWriter { private readonly OutputFormatterSelector _formatterSelector; private readonly IHttpResponseStreamWriterFactory _writerFactory; private readonly ProblemDetailsFactory _problemDetailsFactory; private readonly ProblemDetailsOptions _options; - private readonly ProblemDetailsMapper? _mapper; private static readonly MediaTypeCollection _problemContentTypes = new() { @@ -22,24 +20,49 @@ internal class DefaultProblemDetailsWriter : IProblemDetailsEndpointWriter "application/problem+xml" }; - public DefaultProblemDetailsWriter( + public DefaultApiProblemDetailsWriter( OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, ProblemDetailsFactory problemDetailsFactory, - IOptions options, - ProblemDetailsMapper? mapper = null) + IOptions options) { _formatterSelector = formatterSelector; _writerFactory = writerFactory; _problemDetailsFactory = problemDetailsFactory; _options = options.Value; - _mapper = mapper; } - public async Task WriteAsync( + public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) + { + if (isRouting || context.Response.StatusCode >= 500) + { + return true; + } + + if (metadata != null) + { + var responseType = metadata.GetMetadata(); + var apiControllerAttribute = metadata.GetMetadata(); + + if (apiControllerAttribute != null && responseType?.Type == typeof(ProblemDetails)) + { + return true; + } + } + + return false; + + //var headers = context.Request.GetTypedHeaders(); + //var acceptHeader = headers.Accept; + //if (acceptHeader != null && + // !acceptHeader.Any(h => _problemMediaType.IsSubsetOf(h))) + //{ + // return false; + //} + } + + public Task WriteAsync( HttpContext context, - EndpointMetadataCollection? metadata = null, - bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, @@ -48,12 +71,6 @@ public async Task WriteAsync( IDictionary? extensions = null, Action? configureDetails = null) { - if (_mapper == null || - !_mapper.CanMap(context, metadata: metadata, isRouting: isRouting)) - { - return false; - } - var problemDetails = _problemDetailsFactory.CreateProblemDetails(context, statusCode ?? context.Response.StatusCode, title, type, detail, instance); if (extensions is not null) @@ -80,10 +97,9 @@ public async Task WriteAsync( if (selectedFormatter == null) { - return false; + return Task.CompletedTask; } - await selectedFormatter.WriteAsync(formatterContext); - return true; + return selectedFormatter.WriteAsync(formatterContext); } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs index f77eed19ecd1..2caf14cb21ad 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs @@ -13,10 +13,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory { private readonly ApiBehaviorOptions _options; + private readonly Action? _configure; - public DefaultProblemDetailsFactory(IOptions options) + public DefaultProblemDetailsFactory( + IOptions options, + IOptions? problemDetailsOptions = null) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _configure = problemDetailsOptions?.Value?.ConfigureDetails; + } + + public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) + { + throw new NotImplementedException(); } public override ProblemDetails CreateProblemDetails( @@ -93,5 +102,7 @@ private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails { problemDetails.Extensions["traceId"] = traceId; } + + _configure?.Invoke(httpContext!, problemDetails); } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 9350111b0c92..94b0f864594c 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -118,10 +118,12 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? const int statusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = statusCode; - var endpointWriter = context.HttpContext.RequestServices.GetService(); - return endpointWriter == null ? - Task.CompletedTask : - endpointWriter.WriteAsync(context.HttpContext); + var provider = context.HttpContext.RequestServices.GetService(); + if (provider != null && + provider.GetWriter(context.HttpContext) is { } problemDetailsWriter) + { + return problemDetailsWriter.WriteAsync(context.HttpContext); + } } Log.ObjectResultExecuting(Logger, result, value); diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index e5fc38a9d36f..7a360ec86819 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -8,20 +8,20 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; - private readonly ProblemDetailsMapper? _matcher; + private readonly IProblemDetailsProvider? _problemDetailsProvider; public ProblemDetailsClientErrorFactory( ProblemDetailsFactory problemDetailsFactory, - ProblemDetailsMapper? matcher = null) + IProblemDetailsProvider? problemDetailsProvider = null) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); - _matcher = matcher; + _problemDetailsProvider = problemDetailsProvider; } public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { - if (_matcher != null && - !_matcher.CanMap(actionContext.HttpContext, statusCode: clientError.StatusCode)) + if (_problemDetailsProvider != null && + !_problemDetailsProvider.IsEnabled(clientError.StatusCode ?? 500)) { return null; } diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 0e07e4447087..9d9b2ac58e91 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -28,6 +28,9 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + + + diff --git a/src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs b/src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs deleted file mode 100644 index 861b9769af3d..000000000000 --- a/src/Mvc/Mvc.Core/src/ProblemDetailsApiMapPolicy.cs +++ /dev/null @@ -1,34 +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; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Mvc; - -internal sealed class ProblemDetailsApiMapPolicy : IProblemDetailsMapPolicy -{ - private readonly ApiBehaviorOptions _options; - - public ProblemDetailsApiMapPolicy(IOptions options) - { - _options = options.Value; - } - - public bool CanMap(HttpContext context, EndpointMetadataCollection? metadata, int? statusCode, bool isRouting) - { - if (metadata != null) - { - // It is a Controller but not declared as ApiController behavior - // or the SuppressMapClientErrors is true. In this case we will - // not allow ProblemDetails mapping - if (metadata.GetMetadata() != null) - { - return !(metadata.GetMetadata() == null || - _options.SuppressMapClientErrors); - } - } - - return true; - } -} diff --git a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs index 3e32e1e79e57..fd656e9ef473 100644 --- a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc; /// /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public sealed class ProducesErrorResponseTypeAttribute : Attribute, IProducesErrorResponseMetadata +public sealed class ProducesErrorResponseTypeAttribute : Attribute { /// /// Initializes a new instance of . diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs index f0a483355fcd..7c590f2b6640 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs @@ -188,7 +188,7 @@ public void Apply_AllowsRemovalOfMetadata_ByParameterTypeImplementingIEndpointPa private static EndpointMetadataConvention GetConvention(IServiceProvider services = null) { services ??= Mock.Of(); - return new EndpointMetadataConvention(services); + return new EndpointMetadataConvention(services, typeof(void)); } private static ApplicationModelProviderContext GetContext(Type type) diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index a6b64adea423..35a2b6fa71c7 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -324,13 +324,6 @@ private Dictionary MultiRegistrationServiceTypes typeof(DynamicControllerEndpointMatcherPolicy), } }, - { - typeof(IProblemDetailsMapPolicy), - new Type[] - { - typeof(ProblemDetailsApiMapPolicy) - } - }, }; } } diff --git a/src/OpenApi/src/OpenApiGenerator.cs b/src/OpenApi/src/OpenApiGenerator.cs index eb80857184b3..fccbf193d8c1 100644 --- a/src/OpenApi/src/OpenApiGenerator.cs +++ b/src/OpenApi/src/OpenApiGenerator.cs @@ -107,8 +107,9 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM responseType = typeof(void); } - var errorMetadata = metadata.GetMetadata(); + var errorMetadata = metadata.GetMetadata(); var defaultErrorType = errorMetadata?.Type; + var defaultErrorContentType = errorMetadata?.ContentType; var responseProviderMetadata = metadata.GetOrderedMetadata(); var producesResponseMetadata = metadata.GetOrderedMetadata(); @@ -128,6 +129,11 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM { discoveredTypeAnnotation = responseType; } + else if (defaultErrorType != null && defaultErrorContentType != null && statusCode >= 400 && statusCode < 500) + { + discoveredContentTypeAnnotation.Add(defaultErrorContentType); + discoveredTypeAnnotation = defaultErrorType; + } } foreach (var contentType in responseMetadata.ContentTypes) From 22b0932ab67e4036f11f2284e62346ad74ba151e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Jun 2022 16:44:34 -0700 Subject: [PATCH 13/59] Removing changes --- .../src/RequestDelegateFactory.cs | 225 ++++++++---------- 1 file changed, 97 insertions(+), 128 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 3a27b784b187..5497aa78f49c 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -57,8 +57,6 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ArrayEmptyOfObjectMethod = typeof(Array).GetMethod(nameof(Array.Empty), BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(new Type[] { typeof(object) }); - private static readonly MethodInfo ProblemDetailsResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WriteProblemResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; - // Call WriteAsJsonAsync() to serialize the runtime return type rather than the declared return type. // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism @@ -196,8 +194,6 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions context.Metadata.AddRange(options.InitialEndpointMetadata); } - context.CanGenerateProblemDetails = context.ServiceProviderIsService?.IsService(typeof(IProblemDetailsProvider)) == true; - return context; } @@ -256,7 +252,7 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions var responseWritingMethodCall = factoryContext.ParamCheckExpressions.Count > 0 ? CreateParamCheckingResponseWritingMethodCall(returnType, factoryContext) : - AddResponseWritingToMethodCall(factoryContext.MethodCall, returnType, factoryContext.CanGenerateProblemDetails); + AddResponseWritingToMethodCall(factoryContext.MethodCall, returnType); if (factoryContext.UsingTempSourceString) { @@ -789,10 +785,8 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(Type retu var checkWasParamCheckFailureWithFilters = Expression.Block( Expression.IfThen( WasParamCheckFailureExpr, - Expression.Block( - Expression.Assign(StatusCodeExpr, Expression.Constant(400)), - Expression.Call(ProblemDetailsResponseAsyncMethod, HttpContextExpr))), - AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext.CanGenerateProblemDetails) + Expression.Assign(StatusCodeExpr, Expression.Constant(400))), + AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType) ); checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailureWithFilters; @@ -809,157 +803,133 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(Type retu WasParamCheckFailureExpr, Expression.Block( Expression.Assign(StatusCodeExpr, Expression.Constant(400)), - Expression.Call(ProblemDetailsResponseAsyncMethod, HttpContextExpr)), - AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext.CanGenerateProblemDetails)); + CompletedTaskExpr), + AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType)); checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure; } return Expression.Block(localVariables, checkParamAndCallMethod); } - private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType, bool canGenerateProblemDetails) + private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType) { - static Expression ResponseWriteMethod(Expression methodCall, Type returnType) + // Exact request delegate match + if (returnType == typeof(void)) { - // Exact request delegate match - if (returnType == typeof(void)) - { - return Expression.Block(methodCall, CompletedTaskExpr); - } - else if (returnType == typeof(object)) - { - return Expression.Call(ExecuteObjectReturnMethod, methodCall, HttpContextExpr); - } - else if (returnType == typeof(ValueTask)) + return Expression.Block(methodCall, CompletedTaskExpr); + } + else if (returnType == typeof(object)) + { + return Expression.Call(ExecuteObjectReturnMethod, methodCall, HttpContextExpr); + } + else if (returnType == typeof(ValueTask)) + { + return Expression.Call(ExecuteValueTaskOfObjectMethod, + methodCall, + HttpContextExpr); + } + else if (returnType == typeof(Task)) + { + return Expression.Call(ExecuteTaskOfObjectMethod, + methodCall, + HttpContextExpr); + } + else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) + { + if (returnType == typeof(Task)) { - return Expression.Call(ExecuteValueTaskOfObjectMethod, - methodCall, - HttpContextExpr); + return methodCall; } - else if (returnType == typeof(Task)) + else if (returnType == typeof(ValueTask)) { - return Expression.Call(ExecuteTaskOfObjectMethod, - methodCall, - HttpContextExpr); + return Expression.Call( + ExecuteValueTaskMethod, + methodCall); } - else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(Task<>)) { - if (returnType == typeof(Task)) + var typeArg = returnType.GetGenericArguments()[0]; + + if (typeof(IResult).IsAssignableFrom(typeArg)) { - return methodCall; + return Expression.Call( + ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - else if (returnType == typeof(ValueTask)) + // ExecuteTask(handler(..), httpContext); + else if (typeArg == typeof(string)) { return Expression.Call( - ExecuteValueTaskMethod, - methodCall); + ExecuteTaskOfStringMethod, + methodCall, + HttpContextExpr); } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(Task<>)) + else { - var typeArg = returnType.GetGenericArguments()[0]; - - if (typeof(IResult).IsAssignableFrom(typeArg)) - { - return Expression.Call( - ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } - // ExecuteTask(handler(..), httpContext); - else if (typeArg == typeof(string)) - { - return Expression.Call( - ExecuteTaskOfStringMethod, - methodCall, - HttpContextExpr); - } - else - { - return Expression.Call( - ExecuteTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } + return Expression.Call( + ExecuteTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - var typeArg = returnType.GetGenericArguments()[0]; + } + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var typeArg = returnType.GetGenericArguments()[0]; - if (typeof(IResult).IsAssignableFrom(typeArg)) - { - return Expression.Call( - ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } - // ExecuteTask(handler(..), httpContext); - else if (typeArg == typeof(string)) - { - return Expression.Call( - ExecuteValueTaskOfStringMethod, - methodCall, - HttpContextExpr); - } - else - { - return Expression.Call( - ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } + if (typeof(IResult).IsAssignableFrom(typeArg)) + { + return Expression.Call( + ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - else + // ExecuteTask(handler(..), httpContext); + else if (typeArg == typeof(string)) { - // TODO: Handle custom awaitables - throw new NotSupportedException($"Unsupported return type: {returnType}"); + return Expression.Call( + ExecuteValueTaskOfStringMethod, + methodCall, + HttpContextExpr); } - } - else if (typeof(IResult).IsAssignableFrom(returnType)) - { - if (returnType.IsValueType) + else { - var box = Expression.TypeAs(methodCall, typeof(IResult)); - return Expression.Call(ResultWriteResponseAsyncMethod, box, HttpContextExpr); + return Expression.Call( + ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - return Expression.Call(ResultWriteResponseAsyncMethod, methodCall, HttpContextExpr); - } - else if (returnType == typeof(string)) - { - return Expression.Call(StringResultWriteResponseAsyncMethod, HttpContextExpr, methodCall); } - else if (returnType.IsValueType) + else { - var box = Expression.TypeAs(methodCall, typeof(object)); - return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); + // TODO: Handle custom awaitables + throw new NotSupportedException($"Unsupported return type: {returnType}"); } - else + } + else if (typeof(IResult).IsAssignableFrom(returnType)) + { + if (returnType.IsValueType) { - return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + var box = Expression.TypeAs(methodCall, typeof(IResult)); + return Expression.Call(ResultWriteResponseAsyncMethod, box, HttpContextExpr); } + return Expression.Call(ResultWriteResponseAsyncMethod, methodCall, HttpContextExpr); } - - var responseWriteCall = ResponseWriteMethod(methodCall, returnType); - - return canGenerateProblemDetails ? - Expression.Block(responseWriteCall, Expression.Call(ProblemDetailsResponseAsyncMethod, HttpContextExpr)) : - responseWriteCall; - } - - private static Task WriteProblemResponseAsync(HttpContext context) - { - if (!context.Response.HasStarted && - context.Response.StatusCode >= 400) + else if (returnType == typeof(string)) { - return Task.CompletedTask; - //var endpointWriter = context.RequestServices.GetService(); - //return endpointWriter == null ? - // Task.CompletedTask : - // endpointWriter.WriteAsync(context); + return Expression.Call(StringResultWriteResponseAsyncMethod, HttpContextExpr, methodCall); + } + else if (returnType.IsValueType) + { + var box = Expression.TypeAs(methodCall, typeof(object)); + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); + } + else + { + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); } - - return Task.CompletedTask; } private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) @@ -2093,7 +2063,6 @@ private sealed class FactoryContext public List? RouteParameters { get; init; } public bool ThrowOnBadRequest { get; init; } public bool DisableInferredFromBody { get; init; } - public bool CanGenerateProblemDetails { get; set; } // Temporary State public ParameterInfo? JsonRequestBodyParameter { get; set; } @@ -2119,7 +2088,7 @@ private sealed class FactoryContext public Expression? MethodCall { get; set; } public Type[] ArgumentTypes { get; set; } = Array.Empty(); public Expression[] ArgumentExpressions { get; set; } = Array.Empty(); - public Expression[] BoxedArgs { get; set; } = Array.Empty(); + public Expression[] BoxedArgs { get; set; } = Array.Empty(); public List>? Filters { get; init; } public List Parameters { get; set; } = new(); From f6a5c37394bd7b14f21f25eb7cc277c8301e48dc Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Jun 2022 16:58:35 -0700 Subject: [PATCH 14/59] Initial cleanup --- .../src/ProblemDetails/IProblemDetailsWriter.cs | 3 --- .../Http.Abstractions/src/PublicAPI.Unshipped.txt | 14 ++++++-------- src/Http/Http/src/Microsoft.AspNetCore.Http.csproj | 1 - .../OpenApiRouteHandlerBuilderExtensions.cs | 1 - .../src/Microsoft.AspNetCore.Routing.csproj | 1 - .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 + 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index 3ee0a0613821..6e27a3ce4c9a 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -15,7 +15,6 @@ public interface IProblemDetailsWriter /// /// /// - /// /// /// bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting); @@ -24,8 +23,6 @@ public interface IProblemDetailsWriter /// /// /// - /// - /// /// /// /// diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 685a581c78ce..79504cd9f017 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -9,17 +9,17 @@ Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext.DefaultRouteHandlerInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! arguments) -> void Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator.Current.get -> object! Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T! -Microsoft.AspNetCore.Http.IProblemDetailsMapPolicy -Microsoft.AspNetCore.Http.IProblemDetailsMapPolicy.CanMap(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, int statusCode, bool isRouting) -> bool +Microsoft.AspNetCore.Http.IProblemDetailsProvider +Microsoft.AspNetCore.Http.IProblemDetailsProvider.GetWriter(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? currentMetadata = null, bool isRouting = false) -> Microsoft.AspNetCore.Http.IProblemDetailsWriter? +Microsoft.AspNetCore.Http.IProblemDetailsProvider.IsEnabled(int statusCode, bool isRouting = false) -> bool +Microsoft.AspNetCore.Http.IProblemDetailsWriter +Microsoft.AspNetCore.Http.IProblemDetailsWriter.CanWrite(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, bool isRouting) -> bool +Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null, System.Action? configureDetails = null) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long? -Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter -Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.CanWrite(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata = null, int? statusCode = null, bool isRouting = false) -> bool -Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.Policies.get -> Microsoft.AspNetCore.Http.IProblemDetailsMapPolicy![]! -Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.ProblemDetailsEndpointWriter(System.Collections.Generic.IEnumerable! policies) -> void Microsoft.AspNetCore.Http.RouteHandlerContext Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection! @@ -38,8 +38,6 @@ Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata.Description.get -> string! Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata.Summary.get -> string! -abstract Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.IsEnabled(int statusCode, bool isRouting = false) -> bool -abstract Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata = null, bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null, System.Action? configureDetails = null) -> System.Threading.Tasks.Task! abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.Arguments.get -> System.Collections.Generic.IList! abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.GetArgument(int index) -> T abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index 48a4fc03c2af..e5e1ce15a3d9 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -25,7 +25,6 @@ - diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs index e9c3145b3e93..2615e70e1ac9 100644 --- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Mime; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 1b0d89ac2852..0e1f47d2ab67 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -37,7 +37,6 @@ - diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 9d9b2ac58e91..2754f5f8b007 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -47,6 +47,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + From c13a1bdbdf58af67fd52459590fd5e7f2ccb6b4a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 16 Jun 2022 16:30:58 -0700 Subject: [PATCH 15/59] Clean up --- .../ProblemDetails/IProblemDetailsProvider.cs | 30 ----------- .../ProblemDetails/IProblemDetailsWriter.cs | 4 +- .../ProblemDetailsEndpointProvider.cs | 44 +++++++++++++++ .../ProblemDetails}/ProblemDetailsOptions.cs | 25 ++++++++- .../src/PublicAPI.Unshipped.txt | 2 +- .../Http/src/Builder/ApplicationBuilder.cs | 2 +- .../src/DefaultIProblemDetailsProvider.cs | 54 ------------------- .../src/DefaultProblemDetailsWriter.cs | 25 +-------- ...oblemDetailsServiceCollectionExtensions.cs | 15 ++---- .../src/PublicAPI.Unshipped.txt | 2 +- .../src/Matching/AcceptsMatcherPolicy.cs | 2 +- .../src/Matching/HttpMethodMatcherPolicy.cs | 2 +- .../DeveloperExceptionPageMiddleware.cs | 30 +++++------ .../ExceptionHandlerExtensions.cs | 5 +- .../ExceptionHandlerMiddleware.cs | 27 +++------- .../src/Formatters/TextOutputFormatter.cs | 3 +- .../DefaultApiProblemDetailsWriter.cs | 19 +++---- .../Infrastructure/ObjectResultExecutor.cs | 5 +- .../ProblemDetailsClientErrorFactory.cs | 10 ++-- 19 files changed, 125 insertions(+), 181 deletions(-) delete mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs create mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs rename src/Http/{ProblemDetails/src/DependencyInjection => Http.Abstractions/src/ProblemDetails}/ProblemDetailsOptions.cs (58%) delete mode 100644 src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.cs diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs deleted file mode 100644 index ee120e2b8380..000000000000 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsProvider.cs +++ /dev/null @@ -1,30 +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; - -/// -/// -/// -public interface IProblemDetailsProvider -{ - /// - /// - /// - /// - /// - /// - bool IsEnabled(int statusCode, bool isRouting = false); - - /// - /// - /// - /// - /// - /// - /// - IProblemDetailsWriter? GetWriter( - HttpContext context, - EndpointMetadataCollection? currentMetadata = null, - bool isRouting = false); -} diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index 6e27a3ce4c9a..8e2178b30104 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -29,7 +29,6 @@ public interface IProblemDetailsWriter /// /// /// - /// /// Task WriteAsync( HttpContext context, @@ -38,6 +37,5 @@ Task WriteAsync( string? type = null, string? detail = null, string? instance = null, - IDictionary? extensions = null, - Action? configureDetails = null); + IDictionary? extensions = null); } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs new file mode 100644 index 000000000000..a0e4542000bf --- /dev/null +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; + +namespace Microsoft.AspNetCore.Http; + +/// +/// +/// +public sealed class ProblemDetailsWriterProvider +{ + private readonly IProblemDetailsWriter[] _writers; + + public ProblemDetailsWriterProvider(IEnumerable writers) + { + _writers = writers.ToArray(); + } + + /// + /// + /// + /// + /// + /// + /// + public IProblemDetailsWriter? GetWriter( + HttpContext context, + EndpointMetadataCollection? currentMetadata = null, + bool isRouting = false) + { + currentMetadata ??= context.GetEndpoint()?.Metadata; + + for (var i = 0; i < _writers.Length; i++) + { + if (_writers[i].CanWrite(context, currentMetadata, isRouting: isRouting)) + { + return _writers[i]; + } + } + + return null; + } +} diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsOptions.cs similarity index 58% rename from src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs rename to src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsOptions.cs index 91e8e8c5e928..38bf650e2e5d 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsOptions.cs @@ -13,12 +13,35 @@ public class ProblemDetailsOptions /// /// /// - public MappingOptions AllowedMapping { get; set; } = MappingOptions.Unspecified; + public MappingOptions AllowedMapping { get; set; } = MappingOptions.All; /// /// /// public Action? ConfigureDetails { get; set; } + + /// + /// + /// + /// + /// + /// + public bool IsEnabled(int statusCode, bool isRouting = false) + { + if (AllowedMapping == MappingOptions.Unspecified) + { + return false; + } + + return isRouting ? + AllowedMapping.HasFlag(MappingOptions.RoutingFailures) : + statusCode switch + { + >= 400 and <= 499 => AllowedMapping.HasFlag(MappingOptions.ClientErrors), + >= 500 => AllowedMapping.HasFlag(MappingOptions.Exceptions), + _ => false, + }; + } } /// diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 409dce187f49..d60af09550f7 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -14,7 +14,7 @@ Microsoft.AspNetCore.Http.IProblemDetailsProvider.GetWriter(Microsoft.AspNetCore Microsoft.AspNetCore.Http.IProblemDetailsProvider.IsEnabled(int statusCode, bool isRouting = false) -> bool Microsoft.AspNetCore.Http.IProblemDetailsWriter Microsoft.AspNetCore.Http.IProblemDetailsWriter.CanWrite(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, bool isRouting) -> bool -Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null, System.Action? configureDetails = null) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Http.IBindableFromHttpContext Microsoft.AspNetCore.Http.IBindableFromHttpContext.BindAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Reflection.ParameterInfo! parameter) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index d0642f4d55cd..ff46a2f7a008 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -133,7 +133,7 @@ public RequestDelegate Build() const int statusCode = StatusCodes.Status404NotFound; context.Response.StatusCode = statusCode; - var endpointProvider = context.RequestServices.GetService(); + var endpointProvider = context.RequestServices.GetService(); if (endpointProvider != null && endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) { diff --git a/src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.cs b/src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.cs deleted file mode 100644 index 6021ca17f8f4..000000000000 --- a/src/Http/ProblemDetails/src/DefaultIProblemDetailsProvider.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. - -using System.Linq; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -internal sealed class DefaultProblemDetailsEndpointProvider : IProblemDetailsProvider -{ - private readonly ProblemDetailsOptions _options; - private readonly IProblemDetailsWriter[] _writers; - - public DefaultProblemDetailsEndpointProvider( - IEnumerable writers, - IOptions options) - { - _options = options.Value; - _writers = writers.ToArray(); - } - - public IProblemDetailsWriter? GetWriter(HttpContext context, EndpointMetadataCollection? currentMetadata = null, bool isRouting = false) - { - if (IsEnabled(context.Response.StatusCode, isRouting)) - { - currentMetadata ??= context.GetEndpoint()?.Metadata; - - for (var i = 0; i < _writers.Length; i++) - { - if (_writers[i].CanWrite(context, currentMetadata, isRouting: isRouting)) - { - return _writers[i]; - } - } - } - - return null; - } - - public bool IsEnabled(int statusCode, bool isRouting = false) - { - if (_options.AllowedMapping == MappingOptions.Unspecified) - { - return false; - } - - return isRouting ? _options.AllowedMapping.HasFlag(MappingOptions.RoutingFailures) : statusCode switch - { - >= 400 and <= 499 => _options.AllowedMapping.HasFlag(MappingOptions.ClientErrors), - >= 500 => _options.AllowedMapping.HasFlag(MappingOptions.Exceptions), - _ => false, - }; - } -} diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index cc0b5c889d76..919299274071 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -17,23 +16,7 @@ public DefaultProblemDetailsWriter(IOptions options) } public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) - { - if (isRouting || context.Response.StatusCode >= 500) - { - return true; - } - - var problemDetailsMetadata = metadata?.GetMetadata(); - return problemDetailsMetadata != null; - - //var headers = context.Request.GetTypedHeaders(); - //var acceptHeader = headers.Accept; - //if (acceptHeader != null && - // !acceptHeader.Any(h => _problemMediaType.IsSubsetOf(h))) - //{ - // return false; - //} - } + => (isRouting || context.Response.StatusCode >= 500) && _options.IsEnabled(context.Response.StatusCode, isRouting); public Task WriteAsync( HttpContext context, @@ -42,8 +25,7 @@ public Task WriteAsync( string? type = null, string? detail = null, string? instance = null, - IDictionary? extensions = null, - Action? configureDetails = null) + IDictionary? extensions = null) { var problemDetails = new ProblemDetails { @@ -63,11 +45,8 @@ public Task WriteAsync( } ProblemDetailsDefaults.Apply(problemDetails, context.Response.StatusCode); - _options.ConfigureDetails?.Invoke(context, problemDetails); - configureDetails?.Invoke(context, problemDetails); return context.Response.WriteAsJsonAsync(problemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); } } - diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs index 0657c17c0501..e02111462bd4 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs @@ -16,20 +16,15 @@ public static class ProblemDetailsServiceCollectionExtensions /// Adds services required for creation of for failed requests. /// /// The to add the services to. - /// /// The so that additional calls can be chained. - public static IServiceCollection AddProblemDetails( - this IServiceCollection services, - MappingOptions allowedMapping = MappingOptions.All) + public static IServiceCollection AddProblemDetails(this IServiceCollection services) { - ArgumentNullException.ThrowIfNull(nameof(services)); + ArgumentNullException.ThrowIfNull(services); // Adding default services; - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.Configure(options => options.AllowedMapping = allowedMapping); - return services; } @@ -43,8 +38,8 @@ public static IServiceCollection AddProblemDetails( this IServiceCollection services, Action configureOptions) { - ArgumentNullException.ThrowIfNull(nameof(services)); - ArgumentNullException.ThrowIfNull(nameof(configureOptions)); + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); // Adding default services services.AddProblemDetails(); diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt index 1f041d56a149..9e8b33ae12b3 100644 --- a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt +++ b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt @@ -12,5 +12,5 @@ Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.get -> System.A Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Http.MappingOptions allowedMapping = Microsoft.AspNetCore.Http.MappingOptions.All) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 92c7a6b8d4bf..71b33e86f308 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -265,7 +265,7 @@ private static Endpoint CreateRejectionEndpoint() const int statusCode = StatusCodes.Status415UnsupportedMediaType; context.Response.StatusCode = statusCode; - var endpointProvider = context.RequestServices.GetService(); + var endpointProvider = context.RequestServices.GetService(); if (endpointProvider != null && endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) { diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 961c4420bb51..3d7a8def1a92 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -412,7 +412,7 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods const int statusCode = StatusCodes.Status405MethodNotAllowed; context.Response.StatusCode = statusCode; - var endpointProvider = context.RequestServices.GetService(); + var endpointProvider = context.RequestServices.GetService(); if (endpointProvider != null && endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) { diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 38d12811239b..1b060eacd7cb 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -10,11 +10,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.StackTrace.Sources; @@ -35,7 +32,7 @@ public class DeveloperExceptionPageMiddleware private readonly ExceptionDetailsProvider _exceptionDetailsProvider; private readonly Func _exceptionHandler; private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); - private readonly IProblemDetailsProvider? _problemDetailsProvider; + private readonly ProblemDetailsWriterProvider? _problemDetailsProvider; /// /// Initializes a new instance of the class @@ -54,7 +51,7 @@ public DeveloperExceptionPageMiddleware( IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable filters, - IProblemDetailsProvider? problemDetailsProvider = null) + ProblemDetailsWriterProvider? problemDetailsProvider = null) { if (next == null) { @@ -173,19 +170,22 @@ private Task DisplayExceptionContent(ErrorContext errorContext) if (_problemDetailsProvider?.GetWriter(httpContext) is { } writer) { - void ConfigureDetails(HttpContext context, ProblemDetails problemDetails) + var exceptionExtensions = new Dictionary { - problemDetails.Extensions.Add("exception", new { - Headers = httpContext.Request.Headers, - Error = errorContext.Exception.ToString(), - Path = context.Request.Path, - Endpoint = context.GetEndpoint()?.ToString(), - RouteValues = context.Features.Get()?.RouteValues, - }); - } + "exception", + new + { + Headers = httpContext.Request.Headers, + Error = errorContext.Exception.ToString(), + Path = httpContext.Request.Path, + Endpoint = httpContext.GetEndpoint()?.ToString(), + RouteValues = httpContext.Features.Get()?.RouteValues, + } + } + }; - return writer.WriteAsync(httpContext, configureDetails: ConfigureDetails); + return writer.WriteAsync(httpContext, extensions: exceptionExtensions); } httpContext.Response.ContentType = "text/plain; charset=utf-8"; diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index d59d3cbc5bf5..b9e569eee351 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -131,8 +131,9 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui options.Value.ExceptionHandler = builder.Build(); } - var problemDetailsProvider = app.ApplicationServices.GetService(); - return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsProvider).Invoke; + var problemDetailsProvider = app.ApplicationServices.GetService(); + var problemDetailsOption = app.ApplicationServices.GetService>(); + return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsOption, problemDetailsProvider).Invoke; }); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index f677be0cc69e..9d1d46a4eb3a 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -8,8 +8,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -27,22 +25,7 @@ public class ExceptionHandlerMiddleware private readonly ILogger _logger; private readonly Func _clearCacheHeadersDelegate; private readonly DiagnosticListener _diagnosticListener; - private readonly IProblemDetailsProvider? _problemDetailsProvider; - - /// - /// Creates a new - /// - /// The representing the next middleware in the pipeline. - /// The used for logging. - /// The options for configuring the middleware. - /// The used for writing diagnostic messages. - public ExceptionHandlerMiddleware( - RequestDelegate next, - ILoggerFactory loggerFactory, - IOptions options, - DiagnosticListener diagnosticListener) - : this(next, loggerFactory, options, diagnosticListener, null) - { } + private readonly ProblemDetailsWriterProvider? _problemDetailsProvider; /// /// Creates a new @@ -51,13 +34,15 @@ public ExceptionHandlerMiddleware( /// The used for logging. /// The options for configuring the middleware. /// The used for writing diagnostic messages. + /// /// The used for writing messages. public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions options, DiagnosticListener diagnosticListener, - IProblemDetailsProvider? problemDetailsProvider = null) + IOptions? problemDetailsOptions = null, + ProblemDetailsWriterProvider? problemDetailsProvider = null) { _next = next; _options = options.Value; @@ -70,7 +55,9 @@ public ExceptionHandlerMiddleware( { if (_options.ExceptionHandlingPath == null) { - if (_problemDetailsProvider == null || _problemDetailsProvider.IsEnabled(statusCode: DefaultStatusCode) == false) + if (_problemDetailsProvider == null || + problemDetailsOptions?.Value == null || + problemDetailsOptions.Value.IsEnabled(statusCode: DefaultStatusCode) == false) { throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); } diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index 676e5ba63c4d..a7692f6c435e 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -4,7 +4,6 @@ using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -137,7 +136,7 @@ public override Task WriteAsync(OutputFormatterWriteContext context) const int statusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = statusCode; - var provider = context.HttpContext.RequestServices.GetService(); + var provider = context.HttpContext.RequestServices.GetService(); if (provider != null && provider.GetWriter(context.HttpContext) is { } problemDetailsWriter) { diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index efa7d6b7debd..c56c244f3580 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -7,13 +7,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; -internal class DefaultApiProblemDetailsWriter : IProblemDetailsWriter +internal sealed class DefaultApiProblemDetailsWriter : IProblemDetailsWriter { private readonly OutputFormatterSelector _formatterSelector; private readonly IHttpResponseStreamWriterFactory _writerFactory; private readonly ProblemDetailsFactory _problemDetailsFactory; private readonly ProblemDetailsOptions _options; - private static readonly MediaTypeCollection _problemContentTypes = new() { "application/problem+json", @@ -34,15 +33,15 @@ public DefaultApiProblemDetailsWriter( public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) { - if (isRouting || context.Response.StatusCode >= 500) + if (context.Response.StatusCode >= 500 || isRouting) { - return true; + return false; } - if (metadata != null) + if (_options.IsEnabled(context.Response.StatusCode)) { - var responseType = metadata.GetMetadata(); - var apiControllerAttribute = metadata.GetMetadata(); + var responseType = metadata?.GetMetadata(); + var apiControllerAttribute = metadata?.GetMetadata(); if (apiControllerAttribute != null && responseType?.Type == typeof(ProblemDetails)) { @@ -68,8 +67,7 @@ public Task WriteAsync( string? type = null, string? detail = null, string? instance = null, - IDictionary? extensions = null, - Action? configureDetails = null) + IDictionary? extensions = null) { var problemDetails = _problemDetailsFactory.CreateProblemDetails(context, statusCode ?? context.Response.StatusCode, title, type, detail, instance); @@ -81,9 +79,6 @@ public Task WriteAsync( } } - _options.ConfigureDetails?.Invoke(context, problemDetails); - configureDetails?.Invoke(context, problemDetails); - var formatterContext = new OutputFormatterWriteContext( context, _writerFactory.CreateWriter, diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 94b0f864594c..52b5219d25cd 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -110,6 +110,7 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? formatterContext, (IList)result.Formatters ?? Array.Empty(), result.ContentTypes); + if (selectedFormatter == null) { // No formatter supports this. @@ -118,12 +119,14 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? const int statusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = statusCode; - var provider = context.HttpContext.RequestServices.GetService(); + var provider = context.HttpContext.RequestServices.GetService(); if (provider != null && provider.GetWriter(context.HttpContext) is { } problemDetailsWriter) { return problemDetailsWriter.WriteAsync(context.HttpContext); } + + return Task.CompletedTask; } Log.ObjectResultExecuting(Logger, result, value); diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index 7a360ec86819..306dcd19b7b2 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -2,26 +2,30 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; - private readonly IProblemDetailsProvider? _problemDetailsProvider; + private readonly ProblemDetailsOptions? _options; + private readonly ProblemDetailsWriterProvider? _problemDetailsProvider; public ProblemDetailsClientErrorFactory( ProblemDetailsFactory problemDetailsFactory, - IProblemDetailsProvider? problemDetailsProvider = null) + IOptions? options = null, + ProblemDetailsWriterProvider? problemDetailsProvider = null) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); + _options = options?.Value; _problemDetailsProvider = problemDetailsProvider; } public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { if (_problemDetailsProvider != null && - !_problemDetailsProvider.IsEnabled(clientError.StatusCode ?? 500)) + !_options?.IsEnabled(clientError.StatusCode ?? 500) == true) { return null; } From 80aa84570aebfdf673aaa26b3ed4d18a8b5f6d48 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 16 Jun 2022 17:04:08 -0700 Subject: [PATCH 16/59] Public api clean up --- .../ProblemDetails/IProblemDetailsWriter.cs | 2 -- .../ProblemDetailsEndpointProvider.cs | 4 ++++ .../src/PublicAPI.Unshipped.txt | 23 ++++++++++++++++--- .../src/Properties/AssemblyInfo.cs | 2 ++ .../src/PublicAPI.Unshipped.txt | 17 ++++++++++++++ .../ProblemDetailsOptions.cs | 0 .../src/PublicAPI.Unshipped.txt | 1 + .../DeveloperExceptionPageMiddleware.cs | 13 ++++++----- .../ExceptionHandlerMiddleware.cs | 4 ++-- .../Microsoft.AspNetCore.Diagnostics.csproj | 1 + .../Diagnostics/src/PublicAPI.Unshipped.txt | 6 +++-- .../src/ProducesErrorResponseTypeAttribute.cs | 2 -- 12 files changed, 58 insertions(+), 17 deletions(-) rename src/Http/{Http.Abstractions/src/ProblemDetails => ProblemDetails/src/DependencyInjection}/ProblemDetailsOptions.cs (100%) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index 8e2178b30104..c167b0145b7b 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -3,8 +3,6 @@ namespace Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - /// /// /// diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs index a0e4542000bf..c543225c6e9c 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs @@ -12,6 +12,10 @@ public sealed class ProblemDetailsWriterProvider { private readonly IProblemDetailsWriter[] _writers; + /// + /// + /// + /// public ProblemDetailsWriterProvider(IEnumerable writers) { _writers = writers.ToArray(); diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index d60af09550f7..7678022d8f5c 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -9,9 +9,10 @@ Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext.DefaultRouteHandlerInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! arguments) -> void Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator.Current.get -> object! Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T! -Microsoft.AspNetCore.Http.IProblemDetailsProvider -Microsoft.AspNetCore.Http.IProblemDetailsProvider.GetWriter(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? currentMetadata = null, bool isRouting = false) -> Microsoft.AspNetCore.Http.IProblemDetailsWriter? -Microsoft.AspNetCore.Http.IProblemDetailsProvider.IsEnabled(int statusCode, bool isRouting = false) -> bool +Microsoft.AspNetCore.Http.HttpValidationProblemDetails +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void Microsoft.AspNetCore.Http.IProblemDetailsWriter Microsoft.AspNetCore.Http.IProblemDetailsWriter.CanWrite(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, bool isRouting) -> bool Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! @@ -22,6 +23,9 @@ Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long? +Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider +Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider.GetWriter(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? currentMetadata = null, bool isRouting = false) -> Microsoft.AspNetCore.Http.IProblemDetailsWriter? +Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider.ProblemDetailsWriterProvider(System.Collections.Generic.IEnumerable! writers) -> void Microsoft.AspNetCore.Http.RouteHandlerContext Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection! @@ -40,6 +44,19 @@ Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata.Description.get -> string! Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata.Summary.get -> string! +Microsoft.AspNetCore.Mvc.ProblemDetails +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.Arguments.get -> System.Collections.Generic.IList! abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.GetArgument(int index) -> T abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! diff --git a/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs b/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs index 2122b7773ae1..e3cf753d8886 100644 --- a/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs +++ b/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs @@ -3,7 +3,9 @@ using System.Runtime.CompilerServices; +#pragma warning disable RS0016 // Suppress PublicAPI analyzer [assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ProblemDetails))] [assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Http.HttpValidationProblemDetails))] +#pragma warning restore RS0016 [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index f2913822e181..11091a1d38ac 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,8 @@ #nullable enable +*REMOVED*Microsoft.AspNetCore.Http.HttpValidationProblemDetails +*REMOVED*Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! +*REMOVED*Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void +*REMOVED*Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadataContext(System.Reflection.MethodInfo! method, System.Collections.Generic.IList! endpointMetadata, System.IServiceProvider! applicationServices) -> void @@ -17,6 +21,19 @@ Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.InitialEndpointMetadata. Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.InitialEndpointMetadata.init -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? +*REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs similarity index 100% rename from src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsOptions.cs rename to src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt index 9e8b33ae12b3..ed3c8a265b07 100644 --- a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt +++ b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt @@ -10,6 +10,7 @@ Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.get -> Microsoft. Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.get -> System.Action? Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.set -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions.IsEnabled(int statusCode, bool isRouting = false) -> bool Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 1b060eacd7cb..6fd128b9da85 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; @@ -37,13 +38,13 @@ public class DeveloperExceptionPageMiddleware /// /// Initializes a new instance of the class /// - /// - /// - /// + /// The representing the next middleware in the pipeline. + /// The options for configuring the middleware. + /// The used for logging. /// - /// - /// - /// + /// The used for writing diagnostic messages. + /// The list of registered . + /// The used for writing messages. public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions options, diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index 9d1d46a4eb3a..f16ad2e29a18 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -34,8 +34,8 @@ public class ExceptionHandlerMiddleware /// The used for logging. /// The options for configuring the middleware. /// The used for writing diagnostic messages. - /// - /// The used for writing messages. + /// The options for configuring generation. + /// The used for writing messages. public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index ebf506b0a0ae..584056b42a44 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index 0115a19ea94b..c0da6a2b849e 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable -Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.ProblemDetailsEndpointWriter? problemDetailsWriter = null) -> void -Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.IProblemDetailsProvider? problemDetailsProvider = null) -> void +*REMOVED*Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters) -> void +*REMOVED*Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener) -> void +Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider? problemDetailsProvider = null) -> void +Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.Extensions.Options.IOptions? problemDetailsOptions = null, Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider? problemDetailsProvider = null) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? diff --git a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs index fd656e9ef473..7718bdfb04a1 100644 --- a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs @@ -3,8 +3,6 @@ namespace Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Http.Metadata; - /// /// Specifies the type returned by default by controllers annotated with . /// From e0d43338f4a8e2448bd93fc9fd6ac44d595eded4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 17 Jun 2022 14:31:46 -0700 Subject: [PATCH 17/59] Updating public API --- .../ProblemDetails/IProblemDetailsService.cs | 34 +++++++++++ .../ProblemDetailsEndpointProvider.cs | 48 --------------- .../Http/src/Builder/ApplicationBuilder.cs | 6 +- .../src/DefaultProblemDetailsWriter.cs | 2 +- ...oblemDetailsServiceCollectionExtensions.cs | 2 +- .../src/ProblemDetailsResponseMetadata.cs | 19 ------ .../src/ProblemDetailsService.cs | 61 +++++++++++++++++++ .../src/Matching/AcceptsMatcherPolicy.cs | 6 +- .../src/Matching/HttpMethodMatcherPolicy.cs | 6 +- .../DeveloperExceptionPageMiddleware.cs | 40 ++++++------ .../ExceptionHandlerExtensions.cs | 2 +- .../ExceptionHandlerMiddleware.cs | 16 ++--- .../Diagnostics/src/PublicAPI.Unshipped.txt | 4 +- .../src/Formatters/TextOutputFormatter.cs | 6 +- .../DefaultApiProblemDetailsWriter.cs | 32 ++++------ .../Infrastructure/ObjectResultExecutor.cs | 6 +- .../ProblemDetailsClientErrorFactory.cs | 4 +- 17 files changed, 153 insertions(+), 141 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs delete mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs delete mode 100644 src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs create mode 100644 src/Http/ProblemDetails/src/ProblemDetailsService.cs diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs new file mode 100644 index 000000000000..0caf1662238d --- /dev/null +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs @@ -0,0 +1,34 @@ +// 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; + +/// +/// +/// +public interface IProblemDetailsService +{ + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task WriteAsync( + HttpContext context, + EndpointMetadataCollection? currentMetadata = null, + bool isRouting = false, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null); +} diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs deleted file mode 100644 index c543225c6e9c..000000000000 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsEndpointProvider.cs +++ /dev/null @@ -1,48 +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.Linq; - -namespace Microsoft.AspNetCore.Http; - -/// -/// -/// -public sealed class ProblemDetailsWriterProvider -{ - private readonly IProblemDetailsWriter[] _writers; - - /// - /// - /// - /// - public ProblemDetailsWriterProvider(IEnumerable writers) - { - _writers = writers.ToArray(); - } - - /// - /// - /// - /// - /// - /// - /// - public IProblemDetailsWriter? GetWriter( - HttpContext context, - EndpointMetadataCollection? currentMetadata = null, - bool isRouting = false) - { - currentMetadata ??= context.GetEndpoint()?.Metadata; - - for (var i = 0; i < _writers.Length; i++) - { - if (_writers[i].CanWrite(context, currentMetadata, isRouting: isRouting)) - { - return _writers[i]; - } - } - - return null; - } -} diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index ff46a2f7a008..561c0f12498c 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -133,11 +133,9 @@ public RequestDelegate Build() const int statusCode = StatusCodes.Status404NotFound; context.Response.StatusCode = statusCode; - var endpointProvider = context.RequestServices.GetService(); - if (endpointProvider != null && - endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) + if (context.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsEndpoint.WriteAsync(context); + return problemDetailsService.WriteAsync(context, isRouting: true); } return Task.CompletedTask; diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index 919299274071..cfa08a1628ee 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -16,7 +16,7 @@ public DefaultProblemDetailsWriter(IOptions options) } public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) - => (isRouting || context.Response.StatusCode >= 500) && _options.IsEnabled(context.Response.StatusCode, isRouting); + => (isRouting || context.Response.StatusCode >= 500); public Task WriteAsync( HttpContext context, diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs index e02111462bd4..1f16d5d136dc 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs @@ -22,7 +22,7 @@ public static IServiceCollection AddProblemDetails(this IServiceCollection servi ArgumentNullException.ThrowIfNull(services); // Adding default services; - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); return services; diff --git a/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs b/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs deleted file mode 100644 index 43d24ce6f0c4..000000000000 --- a/src/Http/ProblemDetails/src/ProblemDetailsResponseMetadata.cs +++ /dev/null @@ -1,19 +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.Mvc; - -namespace Microsoft.AspNetCore.Http; - -internal sealed class ProblemDetailsResponseMetadata -{ - /// - /// Gets the default error type. - /// - public Type Type { get; } = typeof(ProblemDetails); - - /// - /// - /// - public IEnumerable ContentTypes { get; } = new string[] { "application/problem+json" }; -} diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs new file mode 100644 index 000000000000..4a2f12324d09 --- /dev/null +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class ProblemDetailsService : IProblemDetailsService +{ + private readonly IProblemDetailsWriter[] _writers; + private readonly ProblemDetailsOptions _options; + + public ProblemDetailsService( + IEnumerable writers, + IOptions options) + { + _writers = writers.ToArray(); + _options = options.Value; + } + + public Task WriteAsync( + HttpContext context, + EndpointMetadataCollection? currentMetadata = null, + bool isRouting = false, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null, + IDictionary? extensions = null) + { + if (!_options.IsEnabled(context.Response.StatusCode, isRouting)) + { + return Task.CompletedTask; + } + + currentMetadata ??= context.GetEndpoint()?.Metadata; + var writer = GetWriter(context, currentMetadata, isRouting); + + return writer != null ? + writer.WriteAsync(context, statusCode, title, type, detail, instance, extensions) : + Task.CompletedTask; + } + + private IProblemDetailsWriter? GetWriter( + HttpContext context, + EndpointMetadataCollection? currentMetadata, + bool isRouting) + { + for (var i = 0; i < _writers.Length; i++) + { + if (_writers[i].CanWrite(context, currentMetadata, isRouting: isRouting)) + { + return _writers[i]; + } + } + + return null; + } +} diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 71b33e86f308..65f30c3870c1 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -265,11 +265,9 @@ private static Endpoint CreateRejectionEndpoint() const int statusCode = StatusCodes.Status415UnsupportedMediaType; context.Response.StatusCode = statusCode; - var endpointProvider = context.RequestServices.GetService(); - if (endpointProvider != null && - endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) + if (context.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsEndpoint.WriteAsync(context); + return problemDetailsService.WriteAsync(context, isRouting: true); } return Task.CompletedTask; diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 3d7a8def1a92..b6ca9c07612f 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -412,11 +412,9 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods const int statusCode = StatusCodes.Status405MethodNotAllowed; context.Response.StatusCode = statusCode; - var endpointProvider = context.RequestServices.GetService(); - if (endpointProvider != null && - endpointProvider.GetWriter(context, isRouting: true) is { } problemDetailsEndpoint) + if (context.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsEndpoint.WriteAsync(context); + return problemDetailsService.WriteAsync(context, isRouting: true); } return Task.CompletedTask; diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 6fd128b9da85..1cd4565ff8b2 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -33,7 +33,7 @@ public class DeveloperExceptionPageMiddleware private readonly ExceptionDetailsProvider _exceptionDetailsProvider; private readonly Func _exceptionHandler; private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); - private readonly ProblemDetailsWriterProvider? _problemDetailsProvider; + private readonly IProblemDetailsService? _problemDetailsService; /// /// Initializes a new instance of the class @@ -44,7 +44,7 @@ public class DeveloperExceptionPageMiddleware /// /// The used for writing diagnostic messages. /// The list of registered . - /// The used for writing messages. + /// The used for writing messages. public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions options, @@ -52,7 +52,7 @@ public DeveloperExceptionPageMiddleware( IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable filters, - ProblemDetailsWriterProvider? problemDetailsProvider = null) + IProblemDetailsService? problemDetailsService = null) { if (next == null) { @@ -76,7 +76,7 @@ public DeveloperExceptionPageMiddleware( _diagnosticSource = diagnosticSource; _exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _logger, _options.SourceCodeLineCount); _exceptionHandler = DisplayException; - _problemDetailsProvider = problemDetailsProvider; + _problemDetailsService = problemDetailsService; foreach (var filter in filters.Reverse()) { @@ -165,11 +165,11 @@ private Task DisplayException(ErrorContext errorContext) return DisplayRuntimeException(httpContext, errorContext.Exception); } - private Task DisplayExceptionContent(ErrorContext errorContext) + private async Task DisplayExceptionContent(ErrorContext errorContext) { var httpContext = errorContext.HttpContext; - if (_problemDetailsProvider?.GetWriter(httpContext) is { } writer) + if (_problemDetailsService != null) { var exceptionExtensions = new Dictionary { @@ -186,22 +186,26 @@ private Task DisplayExceptionContent(ErrorContext errorContext) } }; - return writer.WriteAsync(httpContext, extensions: exceptionExtensions); + await _problemDetailsService.WriteAsync(httpContext, extensions: exceptionExtensions); } - httpContext.Response.ContentType = "text/plain; charset=utf-8"; - - var sb = new StringBuilder(); - sb.AppendLine(errorContext.Exception.ToString()); - sb.AppendLine(); - sb.AppendLine("HEADERS"); - sb.AppendLine("======="); - foreach (var pair in httpContext.Request.Headers) + // If the response has not started, assume the problem details was not written. + if (!httpContext.Response.HasStarted) { - sb.AppendLine(FormattableString.Invariant($"{pair.Key}: {pair.Value}")); - } + httpContext.Response.ContentType = "text/plain; charset=utf-8"; + + var sb = new StringBuilder(); + sb.AppendLine(errorContext.Exception.ToString()); + sb.AppendLine(); + sb.AppendLine("HEADERS"); + sb.AppendLine("======="); + foreach (var pair in httpContext.Request.Headers) + { + sb.AppendLine(FormattableString.Invariant($"{pair.Key}: {pair.Value}")); + } - return httpContext.Response.WriteAsync(sb.ToString()); + await httpContext.Response.WriteAsync(sb.ToString()); + } } private Task DisplayCompilationException( diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index b9e569eee351..a73c41473ca5 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -131,7 +131,7 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui options.Value.ExceptionHandler = builder.Build(); } - var problemDetailsProvider = app.ApplicationServices.GetService(); + var problemDetailsProvider = app.ApplicationServices.GetService(); var problemDetailsOption = app.ApplicationServices.GetService>(); return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsOption, problemDetailsProvider).Invoke; }); diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index f16ad2e29a18..2d4fda1cea22 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -25,7 +25,7 @@ public class ExceptionHandlerMiddleware private readonly ILogger _logger; private readonly Func _clearCacheHeadersDelegate; private readonly DiagnosticListener _diagnosticListener; - private readonly ProblemDetailsWriterProvider? _problemDetailsProvider; + private readonly IProblemDetailsService? _problemDetailsService; /// /// Creates a new @@ -35,27 +35,27 @@ public class ExceptionHandlerMiddleware /// The options for configuring the middleware. /// The used for writing diagnostic messages. /// The options for configuring generation. - /// The used for writing messages. + /// The used for writing messages. public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions options, DiagnosticListener diagnosticListener, IOptions? problemDetailsOptions = null, - ProblemDetailsWriterProvider? problemDetailsProvider = null) + IProblemDetailsService? problemDetailsService = null) { _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger(); _clearCacheHeadersDelegate = ClearCacheHeaders; _diagnosticListener = diagnosticListener; - _problemDetailsProvider = problemDetailsProvider; + _problemDetailsService = problemDetailsService; if (_options.ExceptionHandler == null) { if (_options.ExceptionHandlingPath == null) { - if (_problemDetailsProvider == null || + if (problemDetailsService == null || problemDetailsOptions?.Value == null || problemDetailsOptions.Value.IsEnabled(statusCode: DefaultStatusCode) == false) { @@ -152,11 +152,7 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed } else { - var writer = _problemDetailsProvider!.GetWriter(context, exceptionHandlerFeature.Endpoint?.Metadata); - if (writer != null) - { - await writer.WriteAsync(context); - } + await _problemDetailsService!.WriteAsync(context, exceptionHandlerFeature.Endpoint?.Metadata); } // If the response has already started, assume exception handler was successful. diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index c0da6a2b849e..f6ee3b8bb356 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,8 +1,8 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters) -> void *REMOVED*Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener) -> void -Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider? problemDetailsProvider = null) -> void -Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.Extensions.Options.IOptions? problemDetailsOptions = null, Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider? problemDetailsProvider = null) -> void +Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService = null) -> void +Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.Extensions.Options.IOptions? problemDetailsOptions = null, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService = null) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index a7692f6c435e..f71db7f50d65 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -136,11 +136,9 @@ public override Task WriteAsync(OutputFormatterWriteContext context) const int statusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = statusCode; - var provider = context.HttpContext.RequestServices.GetService(); - if (provider != null && - provider.GetWriter(context.HttpContext) is { } problemDetailsWriter) + if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsWriter.WriteAsync(context.HttpContext); + return problemDetailsService.WriteAsync(context.HttpContext, isRouting: true); } return Task.CompletedTask; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index c56c244f3580..6b4b93372163 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Infrastructure; @@ -12,7 +11,6 @@ internal sealed class DefaultApiProblemDetailsWriter : IProblemDetailsWriter private readonly OutputFormatterSelector _formatterSelector; private readonly IHttpResponseStreamWriterFactory _writerFactory; private readonly ProblemDetailsFactory _problemDetailsFactory; - private readonly ProblemDetailsOptions _options; private static readonly MediaTypeCollection _problemContentTypes = new() { "application/problem+json", @@ -22,23 +20,16 @@ internal sealed class DefaultApiProblemDetailsWriter : IProblemDetailsWriter public DefaultApiProblemDetailsWriter( OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, - ProblemDetailsFactory problemDetailsFactory, - IOptions options) + ProblemDetailsFactory problemDetailsFactory) { _formatterSelector = formatterSelector; _writerFactory = writerFactory; _problemDetailsFactory = problemDetailsFactory; - _options = options.Value; } public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) { - if (context.Response.StatusCode >= 500 || isRouting) - { - return false; - } - - if (_options.IsEnabled(context.Response.StatusCode)) + static bool HasMetadata(EndpointMetadataCollection? metadata) { var responseType = metadata?.GetMetadata(); var apiControllerAttribute = metadata?.GetMetadata(); @@ -47,17 +38,20 @@ public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, { return true; } + return false; + } - return false; + if (isRouting) + { + return false; + } - //var headers = context.Request.GetTypedHeaders(); - //var acceptHeader = headers.Accept; - //if (acceptHeader != null && - // !acceptHeader.Any(h => _problemMediaType.IsSubsetOf(h))) - //{ - // return false; - //} + return context.Response.StatusCode switch + { + >= 400 and <= 499 => HasMetadata(metadata), + _ => false, + }; } public Task WriteAsync( diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 52b5219d25cd..7cb7dabfa0af 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -119,11 +119,9 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? const int statusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = statusCode; - var provider = context.HttpContext.RequestServices.GetService(); - if (provider != null && - provider.GetWriter(context.HttpContext) is { } problemDetailsWriter) + if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsWriter.WriteAsync(context.HttpContext); + return problemDetailsService.WriteAsync(context.HttpContext, isRouting: true); } return Task.CompletedTask; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index 306dcd19b7b2..0159308007b2 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -10,12 +10,12 @@ internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; private readonly ProblemDetailsOptions? _options; - private readonly ProblemDetailsWriterProvider? _problemDetailsProvider; + private readonly IProblemDetailsService? _problemDetailsProvider; public ProblemDetailsClientErrorFactory( ProblemDetailsFactory problemDetailsFactory, IOptions? options = null, - ProblemDetailsWriterProvider? problemDetailsProvider = null) + IProblemDetailsService? problemDetailsProvider = null) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); _options = options?.Value; From 3fa6af0d7b7d665877973c202300db41d164163b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 17 Jun 2022 14:48:01 -0700 Subject: [PATCH 18/59] More clean ups --- .../ProblemDetails/IProblemDetailsService.cs | 8 ++++++ .../src/PublicAPI.Unshipped.txt | 6 ++--- .../src/DefaultProblemDetailsWriter.cs | 7 +++++- .../ProblemDetailsOptions.cs | 23 ----------------- .../src/ProblemDetailsJsonContext.cs | 11 -------- .../src/ProblemDetailsService.cs | 25 ++++++++++++++++++- .../ExceptionHandlerExtensions.cs | 3 +-- .../ExceptionHandlerMiddleware.cs | 5 +--- .../Microsoft.AspNetCore.Diagnostics.csproj | 1 - .../Diagnostics/src/PublicAPI.Unshipped.txt | 2 +- .../ProblemDetailsClientErrorFactory.cs | 10 ++++---- 11 files changed, 49 insertions(+), 52 deletions(-) delete mode 100644 src/Http/ProblemDetails/src/ProblemDetailsJsonContext.cs diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs index 0caf1662238d..e48802351175 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs @@ -31,4 +31,12 @@ Task WriteAsync( string? detail = null, string? instance = null, IDictionary? extensions = null); + + /// + /// + /// + /// + /// + /// + bool IsEnabled(int statusCode, bool isRouting = false); } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 7678022d8f5c..902cde87a049 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -13,6 +13,9 @@ Microsoft.AspNetCore.Http.HttpValidationProblemDetails Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void +Microsoft.AspNetCore.Http.IProblemDetailsService +Microsoft.AspNetCore.Http.IProblemDetailsService.IsEnabled(int statusCode, bool isRouting = false) -> bool +Microsoft.AspNetCore.Http.IProblemDetailsService.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? currentMetadata = null, bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Http.IProblemDetailsWriter Microsoft.AspNetCore.Http.IProblemDetailsWriter.CanWrite(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, bool isRouting) -> bool Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! @@ -23,9 +26,6 @@ Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long? -Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider -Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider.GetWriter(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? currentMetadata = null, bool isRouting = false) -> Microsoft.AspNetCore.Http.IProblemDetailsWriter? -Microsoft.AspNetCore.Http.ProblemDetailsWriterProvider.ProblemDetailsWriterProvider(System.Collections.Generic.IEnumerable! writers) -> void Microsoft.AspNetCore.Http.RouteHandlerContext Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection! diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index cfa08a1628ee..87ffb3af9bdf 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -1,12 +1,13 @@ // 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.Serialization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http; -internal sealed class DefaultProblemDetailsWriter : IProblemDetailsWriter +internal sealed partial class DefaultProblemDetailsWriter : IProblemDetailsWriter { private readonly ProblemDetailsOptions _options; @@ -49,4 +50,8 @@ public Task WriteAsync( return context.Response.WriteAsJsonAsync(problemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); } + + [JsonSerializable(typeof(ProblemDetails))] + internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext + { } } diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs index 38bf650e2e5d..73f80ad60f1f 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs @@ -19,29 +19,6 @@ public class ProblemDetailsOptions /// /// public Action? ConfigureDetails { get; set; } - - /// - /// - /// - /// - /// - /// - public bool IsEnabled(int statusCode, bool isRouting = false) - { - if (AllowedMapping == MappingOptions.Unspecified) - { - return false; - } - - return isRouting ? - AllowedMapping.HasFlag(MappingOptions.RoutingFailures) : - statusCode switch - { - >= 400 and <= 499 => AllowedMapping.HasFlag(MappingOptions.ClientErrors), - >= 500 => AllowedMapping.HasFlag(MappingOptions.Exceptions), - _ => false, - }; - } } /// diff --git a/src/Http/ProblemDetails/src/ProblemDetailsJsonContext.cs b/src/Http/ProblemDetails/src/ProblemDetailsJsonContext.cs deleted file mode 100644 index 64f5f07d6815..000000000000 --- a/src/Http/ProblemDetails/src/ProblemDetailsJsonContext.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. - -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Mvc; - -namespace Microsoft.AspNetCore.Http; - -[JsonSerializable(typeof(ProblemDetails))] -internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext -{ } diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs index 4a2f12324d09..e41524ce050d 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -19,6 +19,29 @@ public ProblemDetailsService( _options = options.Value; } + /// + /// + /// + /// + /// + /// + public bool IsEnabled(int statusCode, bool isRouting = false) + { + if (_options.AllowedMapping == MappingOptions.Unspecified) + { + return false; + } + + return isRouting ? + _options.AllowedMapping.HasFlag(MappingOptions.RoutingFailures) : + statusCode switch + { + >= 400 and <= 499 => _options.AllowedMapping.HasFlag(MappingOptions.ClientErrors), + >= 500 => _options.AllowedMapping.HasFlag(MappingOptions.Exceptions), + _ => false, + }; + } + public Task WriteAsync( HttpContext context, EndpointMetadataCollection? currentMetadata = null, @@ -30,7 +53,7 @@ public Task WriteAsync( string? instance = null, IDictionary? extensions = null) { - if (!_options.IsEnabled(context.Response.StatusCode, isRouting)) + if (IsEnabled(context.Response.StatusCode, isRouting)) { return Task.CompletedTask; } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index a73c41473ca5..7c518aeb6723 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -132,8 +132,7 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui } var problemDetailsProvider = app.ApplicationServices.GetService(); - var problemDetailsOption = app.ApplicationServices.GetService>(); - return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsOption, problemDetailsProvider).Invoke; + return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsProvider).Invoke; }); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index 2d4fda1cea22..f1a0fb3bc403 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -34,14 +34,12 @@ public class ExceptionHandlerMiddleware /// The used for logging. /// The options for configuring the middleware. /// The used for writing diagnostic messages. - /// The options for configuring generation. /// The used for writing messages. public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions options, DiagnosticListener diagnosticListener, - IOptions? problemDetailsOptions = null, IProblemDetailsService? problemDetailsService = null) { _next = next; @@ -56,8 +54,7 @@ public ExceptionHandlerMiddleware( if (_options.ExceptionHandlingPath == null) { if (problemDetailsService == null || - problemDetailsOptions?.Value == null || - problemDetailsOptions.Value.IsEnabled(statusCode: DefaultStatusCode) == false) + problemDetailsService.IsEnabled(statusCode: DefaultStatusCode) == false) { throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); } diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index 584056b42a44..ebf506b0a0ae 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -21,7 +21,6 @@ - diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index f6ee3b8bb356..ee35e348121d 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -2,7 +2,7 @@ *REMOVED*Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters) -> void *REMOVED*Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener) -> void Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService = null) -> void -Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.Extensions.Options.IOptions? problemDetailsOptions = null, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService = null) -> void +Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService = null) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index 0159308007b2..d2946a6c0e15 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -10,22 +10,22 @@ internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; private readonly ProblemDetailsOptions? _options; - private readonly IProblemDetailsService? _problemDetailsProvider; + private readonly IProblemDetailsService? _problemDetailsService; public ProblemDetailsClientErrorFactory( ProblemDetailsFactory problemDetailsFactory, IOptions? options = null, - IProblemDetailsService? problemDetailsProvider = null) + IProblemDetailsService? problemDetailsService = null) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); _options = options?.Value; - _problemDetailsProvider = problemDetailsProvider; + _problemDetailsService = problemDetailsService; } public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { - if (_problemDetailsProvider != null && - !_options?.IsEnabled(clientError.StatusCode ?? 500) == true) + if (_problemDetailsService != null && + _problemDetailsService.IsEnabled(clientError.StatusCode ?? 500) == false) { return null; } From 89fd8ec85ed5b6c88f04e0326d4d8dcc219f6ae9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 20 Jun 2022 10:34:47 -0700 Subject: [PATCH 19/59] Adding initial unit tests --- AspNetCore.sln | 21 ++++++++++++++++++- .../src/DefaultProblemDetailsWriter.cs | 2 +- ...soft.AspNetCore.Http.ProblemDetails.csproj | 5 +++++ .../src/ProblemDetailsService.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 1 - ...lidationProblemDetailsJsonConverterTest.cs | 0 ...spNetCore.Http.ProblemDetails.Tests.csproj | 12 +++++++++++ .../test/ProblemDetailsJsonConverterTest.cs | 0 src/Mvc/Mvc.slnf | 2 +- 9 files changed, 40 insertions(+), 5 deletions(-) rename src/Http/{Http.Extensions => ProblemDetails}/test/HttpValidationProblemDetailsJsonConverterTest.cs (100%) create mode 100644 src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj rename src/Http/{Http.Extensions => ProblemDetails}/test/ProblemDetailsJsonConverterTest.cs (100%) diff --git a/AspNetCore.sln b/AspNetCore.sln index e0d26dab4a8e..2515c2b44fc5 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1728,7 +1728,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Too EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet-user-jwts", "{AB4B9E75-719C-4589-B852-20FBFD727730}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalJwtBearerSample", "src\Security\Authentication\JwtBearer\samples\MinimalJwtBearerSample\MinimalJwtBearerSample.csproj", "{7F079E92-32D5-4257-B95B-CFFB0D49C160}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalJwtBearerSample", "src\Security\Authentication\JwtBearer\samples\MinimalJwtBearerSample\MinimalJwtBearerSample.csproj", "{7F079E92-32D5-4257-B95B-CFFB0D49C160}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts.Tests", "src\Tools\dotnet-user-jwts\test\dotnet-user-jwts.Tests.csproj", "{89896261-C5DD-4901-BCA7-7A5F718BC008}" EndProject @@ -1740,6 +1740,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestInfrastructure", "TestI src\ProjectTemplates\TestInfrastructure\runtimeconfig.norollforward.json = src\ProjectTemplates\TestInfrastructure\runtimeconfig.norollforward.json EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.ProblemDetails.Tests", "src\Http\ProblemDetails\test\Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj", "{13384A43-5CF9-4CBE-9F20-B4D6FB304C11}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10405,6 +10407,22 @@ Global {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x64.Build.0 = Release|Any CPU {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.ActiveCfg = Release|Any CPU {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.Build.0 = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|arm64.ActiveCfg = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|arm64.Build.0 = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x64.ActiveCfg = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x64.Build.0 = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x86.ActiveCfg = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x86.Build.0 = Debug|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|Any CPU.Build.0 = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|arm64.ActiveCfg = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|arm64.Build.0 = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x64.ActiveCfg = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x64.Build.0 = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x86.ActiveCfg = Release|Any CPU + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11264,6 +11282,7 @@ Global {7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67} {89896261-C5DD-4901-BCA7-7A5F718BC008} = {AB4B9E75-719C-4589-B852-20FBFD727730} {F0FBA346-D8BC-4FAE-A4B2-85B33FA23055} = {08D53E58-4AAE-40C4-8497-63EC8664F304} + {13384A43-5CF9-4CBE-9F20-B4D6FB304C11} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index 87ffb3af9bdf..7b95814f3a04 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -17,7 +17,7 @@ public DefaultProblemDetailsWriter(IOptions options) } public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) - => (isRouting || context.Response.StatusCode >= 500); + => isRouting || context.Response.StatusCode >= 500; public Task WriteAsync( HttpContext context, diff --git a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj index a1537ec05fb3..7772806f159c 100644 --- a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj +++ b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj @@ -18,6 +18,11 @@ + + + + + diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs index e41524ce050d..c37990504f4b 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -53,7 +53,7 @@ public Task WriteAsync( string? instance = null, IDictionary? extensions = null) { - if (IsEnabled(context.Response.StatusCode, isRouting)) + if (!IsEnabled(context.Response.StatusCode, isRouting)) { return Task.CompletedTask; } diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt index ed3c8a265b07..9e8b33ae12b3 100644 --- a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt +++ b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt @@ -10,7 +10,6 @@ Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.get -> Microsoft. Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.get -> System.Action? Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.set -> void -Microsoft.AspNetCore.Http.ProblemDetailsOptions.IsEnabled(int statusCode, bool isRouting = false) -> bool Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/ProblemDetails/test/HttpValidationProblemDetailsJsonConverterTest.cs similarity index 100% rename from src/Http/Http.Extensions/test/HttpValidationProblemDetailsJsonConverterTest.cs rename to src/Http/ProblemDetails/test/HttpValidationProblemDetailsJsonConverterTest.cs diff --git a/src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj b/src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj new file mode 100644 index 000000000000..94c17bd2261e --- /dev/null +++ b/src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj @@ -0,0 +1,12 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + + diff --git a/src/Http/Http.Extensions/test/ProblemDetailsJsonConverterTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs similarity index 100% rename from src/Http/Http.Extensions/test/ProblemDetailsJsonConverterTest.cs rename to src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index 48a0381b6d55..aa24d72efa80 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -32,8 +32,8 @@ "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", - "src\\Http\\ProblemDetails.Abstractions\\src\\Microsoft.AspnetCore.Http.ProblemDetails.Abstractions.csproj", "src\\Http\\ProblemDetails\\src\\Microsoft.AspNetCore.Http.ProblemDetails.csproj", + "src\\Http\\ProblemDetails\\test\\Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", From dca5c731a4fb421bc845a6be67e52e17fbf1f26c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Jun 2022 13:38:45 -0700 Subject: [PATCH 20/59] Updating unit tests --- ...soft.AspNetCore.Http.ProblemDetails.csproj | 1 + .../src/ProblemDetailsService.cs | 19 +- ...mDetailsServiceCollectionExtensionsTest.cs | 61 ++++ .../test/ProblemDetailsServiceTest.cs | 328 ++++++++++++++++++ ...ApiBehaviorApplicationModelProviderTest.cs | 4 +- .../EndpointMetadataConventionTest.cs | 22 +- .../MvcCoreServiceCollectionExtensionsTest.cs | 7 + 7 files changed, 429 insertions(+), 13 deletions(-) create mode 100644 src/Http/ProblemDetails/test/ProblemDetailsServiceCollectionExtensionsTest.cs create mode 100644 src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs diff --git a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj index 7772806f159c..97747d93de56 100644 --- a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj +++ b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs index c37990504f4b..71945928c7b0 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -37,7 +37,7 @@ public bool IsEnabled(int statusCode, bool isRouting = false) statusCode switch { >= 400 and <= 499 => _options.AllowedMapping.HasFlag(MappingOptions.ClientErrors), - >= 500 => _options.AllowedMapping.HasFlag(MappingOptions.Exceptions), + >= 500 and <= 599 => _options.AllowedMapping.HasFlag(MappingOptions.Exceptions), _ => false, }; } @@ -53,24 +53,25 @@ public Task WriteAsync( string? instance = null, IDictionary? extensions = null) { - if (!IsEnabled(context.Response.StatusCode, isRouting)) - { - return Task.CompletedTask; - } - - currentMetadata ??= context.GetEndpoint()?.Metadata; var writer = GetWriter(context, currentMetadata, isRouting); - return writer != null ? writer.WriteAsync(context, statusCode, title, type, detail, instance, extensions) : Task.CompletedTask; } - private IProblemDetailsWriter? GetWriter( + // Internal for testing + internal IProblemDetailsWriter? GetWriter( HttpContext context, EndpointMetadataCollection? currentMetadata, bool isRouting) { + if (!IsEnabled(context.Response.StatusCode, isRouting)) + { + return null; + } + + currentMetadata ??= context.GetEndpoint()?.Metadata; + for (var i = 0; i < _writers.Length; i++) { if (_writers[i].CanWrite(context, currentMetadata, isRouting: isRouting)) diff --git a/src/Http/ProblemDetails/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsServiceCollectionExtensionsTest.cs new file mode 100644 index 000000000000..1775deea24f2 --- /dev/null +++ b/src/Http/ProblemDetails/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -0,0 +1,61 @@ +// 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.DependencyInjection.Extensions; +using Moq; + +namespace Microsoft.AspNetCore.Http.Tests; + +public class ProblemDetailsServiceCollectionExtensionsTest +{ + [Fact] + public void AddProblemDetails_AddsNeededServices() + { + // Arrange + var collection = new ServiceCollection(); + + // Act + collection.AddProblemDetails(); + + // Assert + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsService) && sd.ImplementationType == typeof(ProblemDetailsService)); + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsWriter) && sd.ImplementationType == typeof(DefaultProblemDetailsWriter)); + } + + [Fact] + public void AddProblemDetails_AllowMultipleWritersRegistration() + { + // Arrange + var collection = new ServiceCollection(); + var expectedCount = 2; + var mockWriter = Mock.Of(); + collection.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IProblemDetailsWriter), mockWriter)); + + // Act + collection.AddProblemDetails(); + + // Assert + var serviceDescriptors = collection.Where(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IProblemDetailsWriter)); + Assert.True( + (expectedCount == serviceDescriptors.Count()), + $"Expected service type '{typeof(IProblemDetailsWriter)}' to be registered {expectedCount}" + + $" time(s) but was actually registered {serviceDescriptors.Count()} time(s)."); + } + + [Fact] + public void AddProblemDetails_KeepCustomRegisteredService() + { + // Arrange + var collection = new ServiceCollection(); + var customService = Mock.Of(); + collection.AddSingleton(typeof(IProblemDetailsService), customService); + + // Act + collection.AddProblemDetails(); + + // Assert + var service = Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsService)); + Assert.Same(customService, service.ImplementationInstance); + } +} diff --git a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs new file mode 100644 index 000000000000..a30cb8b0c2d7 --- /dev/null +++ b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs @@ -0,0 +1,328 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.AspNetCore.Http.Tests; + +public class ProblemDetailsServiceTest +{ + [Fact] + public void IsEnable_ReturnsFalse_ForRouting_WhenDisable() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.ClientErrors | MappingOptions.Exceptions }); + + //Act + var isEnabled = service.IsEnabled(400, isRouting: true); + + //Assert + Assert.False(isEnabled); + } + + [Fact] + public void IsEnable_ReturnsTrue_ForRouting_WhenEnabled() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.RoutingFailures }); + + //Act + var isEnabled = service.IsEnabled(400, isRouting: true); + + //Assert + Assert.True(isEnabled); + } + + [Theory] + [InlineData(100)] + [InlineData(300)] + [InlineData(400)] + [InlineData(500)] + public void IsEnable_ReturnsFalse_WhenUnspecified(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.Unspecified }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.False(isEnabled); + } + + [Theory] + [InlineData(100)] + [InlineData(200)] + [InlineData(300)] + [InlineData(399)] + public void IsEnable_ReturnsFalse_ForSuccessStatus(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.False(isEnabled); + } + + [Theory] + [InlineData(0)] + [InlineData(99)] + [InlineData(600)] + [InlineData(700)] + public void IsEnable_ReturnsFalse_ForUnknownStatus(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.False(isEnabled); + } + + [Theory] + [InlineData(400)] + [InlineData(415)] + [InlineData(422)] + [InlineData(499)] + public void IsEnable_ReturnsTrue_ForClientErrors(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.ClientErrors }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.True(isEnabled); + } + + [Theory] + [InlineData(500)] + [InlineData(599)] + public void IsEnable_ReturnsTrue_ForServerErrors(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.Exceptions }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.True(isEnabled); + } + + [Fact] + public void GetWriter_ReturnsNull_WhenNotEnabled() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.Unspecified }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.Null(writer); + } + + [Fact] + public void GetWriter_ReturnsNull_WhenNotRegisteredWriters() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.Null(writer); + } + + [Fact] + public void GetWriter_ReturnsNull_WhenNoWriterCanWrite() + { + //Arrange + var writers = new List() { + Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false), + Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false) + }; + var service = CreateService( + writers: writers, + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.Null(writer); + } + + [Fact] + public void GetWriter_Returns_ForContextMetadata() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json"}); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); + + //Act + var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.NotNull(selectedWriter); + Assert.IsType(selectedWriter); + } + + [Fact] + public void GetWriter_Returns_ForSpecifiedMetadata() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, EndpointMetadataCollection.Empty, null)); + + //Act + var selectedWriter = service.GetWriter(context, currentMetadata: metadata, isRouting: false); + + //Assert + Assert.NotNull(selectedWriter); + Assert.IsType(selectedWriter); + } + + [Fact] + public void GetWriter_Returns_FirstCanWriter() + { + //Arrange + var writer1 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); + var writer2 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); + var writers = new List() { writer1, writer2 }; + var service = CreateService( + writers: writers, + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.NotNull(selectedWriter); + Assert.Equal(writer1, selectedWriter); + } + + [Fact] + public async Task WriteAsync_Call_SelectedWriter() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + }; + + //Act + await service.WriteAsync(context, currentMetadata: metadata); + + //Assert + Assert.Equal("\"Content\"", Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public async Task WriteAsync_Skip_WhenNoWriter() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + }; + + //Act + await service.WriteAsync(context); + + //Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); + } + + private static ProblemDetailsService CreateService( + ProblemDetailsOptions options, + IEnumerable writers = null) + { + writers ??= Array.Empty(); + return new ProblemDetailsService(writers, Options.Create(options)); + } + + private class SampleMetadata + { + public string ContentType { get; set; } + } + + private class MetadataBasedWriter : IProblemDetailsWriter + { + public bool CanWrite(HttpContext context, EndpointMetadataCollection metadata, bool isRouting) + { + return metadata != null && metadata.GetMetadata != null; + } + + public Task WriteAsync(HttpContext context, int? statusCode = null, string title = null, string type = null, string detail = null, string instance = null, IDictionary extensions = null) + { + return context.Response.WriteAsJsonAsync("Content"); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs index fc4d24cba9d2..41d4e1ab1d15 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs @@ -79,7 +79,7 @@ public void OnProvidersExecuting_AppliesConventions() Assert.NotEmpty(actionModel.Filters.OfType()); Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource); Assert.NotEmpty(actionModel.Selectors); - Assert.Empty(actionModel.Selectors[0].EndpointMetadata); + Assert.NotEmpty(actionModel.Selectors[0].EndpointMetadata.OfType()); } [Fact] @@ -122,7 +122,7 @@ public void OnProvidersExecuting_AppliesConventionsForIResult() Assert.NotEmpty(actionModel.Filters.OfType()); Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource); Assert.NotEmpty(actionModel.Selectors); - Assert.Empty(actionModel.Selectors[0].EndpointMetadata); + Assert.NotEmpty(actionModel.Selectors[0].EndpointMetadata.OfType()); } [Fact] diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs index 7c590f2b6640..721697091bae 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs @@ -12,6 +12,23 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels; public class EndpointMetadataConventionTest { + [Fact] + public void Apply_DefaultErrorTypeMetadata() + { + // Arrange + var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); + var errorType = typeof(ProblemDetails); + var convention = GetConvention(errorType: errorType); + + //Act + convention.Apply(action); + + // Assert + foreach (var selector in action.Selectors) + { + Assert.Contains(selector.EndpointMetadata, m => m is ProducesErrorResponseTypeAttribute attribute && attribute.Type == errorType); + } + } [Theory] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfActionResult))] @@ -185,10 +202,11 @@ public void Apply_AllowsRemovalOfMetadata_ByParameterTypeImplementingIEndpointPa Assert.DoesNotContain(action.Selectors[0].EndpointMetadata, m => m is IAcceptsMetadata); } - private static EndpointMetadataConvention GetConvention(IServiceProvider services = null) + private static EndpointMetadataConvention GetConvention(IServiceProvider services = null, Type errorType = null) { + errorType ??= typeof(void); services ??= Mock.Of(); - return new EndpointMetadataConvention(services, typeof(void)); + return new EndpointMetadataConvention(services, errorType); } private static ApplicationModelProviderContext GetContext(Type type) diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index 35a2b6fa71c7..d105879e827c 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -324,6 +324,13 @@ private Dictionary MultiRegistrationServiceTypes typeof(DynamicControllerEndpointMatcherPolicy), } }, + { + typeof(IProblemDetailsWriter), + new Type[] + { + typeof(DefaultApiProblemDetailsWriter), + } + }, }; } } From e885b3a0199983e5fed6f52b1d094338b70f821f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Jun 2022 15:01:21 -0700 Subject: [PATCH 21/59] Adding more unit tests --- .../src/DefaultProblemDetailsWriter.cs | 2 +- .../test/DefaultProblemDetailsWriterTest.cs | 267 ++++++++++++++++++ .../test/ProblemDetailsJsonConverterTest.cs | 3 + .../EndpointMetadataConventionTest.cs | 19 ++ 4 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index 7b95814f3a04..8c7bc1c484e7 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -17,7 +17,7 @@ public DefaultProblemDetailsWriter(IOptions options) } public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) - => isRouting || context.Response.StatusCode >= 500; + => isRouting || (context.Response.StatusCode >= 500 && context.Response.StatusCode <= 599); public Task WriteAsync( HttpContext context, diff --git a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs new file mode 100644 index 000000000000..105df3d345be --- /dev/null +++ b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs @@ -0,0 +1,267 @@ +// 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.Tests; + +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +public class DefaultProblemDetailsWriterTest +{ + [Theory] + [InlineData(100)] + [InlineData(200)] + [InlineData(300)] + [InlineData(399)] + public void CanWrite_IsFalse_ForSuccessStatus(int statusCode) + { + // Arrange + var writer = GetWriter(); + var context = new DefaultHttpContext() + { + Response = { StatusCode = statusCode }, + }; + + // Act + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + + // Assert + Assert.False(canWrite); + } + + [Theory] + [InlineData(0)] + [InlineData(99)] + [InlineData(600)] + [InlineData(700)] + public void CanWrite_IsFalse_ForUnknownStatus(int statusCode) + { + // Arrange + var writer = GetWriter(); + var context = new DefaultHttpContext() + { + Response = { StatusCode = statusCode }, + }; + + // Act + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + + // Assert + Assert.False(canWrite); + } + + [Theory] + [InlineData(400)] + [InlineData(499)] + public void CanWrite_IsFalse_ForClientErrors(int statusCode) + { + // Arrange + var writer = GetWriter(); + var context = new DefaultHttpContext() + { + Response = { StatusCode = statusCode }, + }; + + // Act + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + + // Assert + Assert.False(canWrite); + } + + [Theory] + [InlineData(500)] + [InlineData(599)] + public void CanWrite_IsTrue_ForServerErrors(int statusCode) + { + // Arrange + var writer = GetWriter(); + var context = new DefaultHttpContext() + { + Response = { StatusCode = statusCode }, + }; + + // Act + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + + // Assert + Assert.True(canWrite); + } + + [Fact] + public void CanWrite_IsTrue_ForRoutingErrors() + { + // Arrange + var writer = GetWriter(); + var context = new DefaultHttpContext(); + + // Act + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: true); + + // Assert + Assert.True(canWrite); + } + + [Fact] + public async Task WriteAsync_Works() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream }, + }; + var expectedProblem = new ProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1-custom", + Title = "Custom Bad Request", + }; + + //Act + await writer.WriteAsync( + context, + statusCode: expectedProblem.Status, + title: expectedProblem.Title, + type: expectedProblem.Type, + detail: expectedProblem.Detail, + instance: expectedProblem.Instance); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + } + + [Fact] + public async Task WriteAsync_AddExtensions() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream }, + }; + + //Act + await writer.WriteAsync( + context, + extensions: new Dictionary + { + ["Extension1"] = "Extension1-Value", + ["Extension2"] = "Extension2-Value", + }); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Collection(problemDetails.Extensions, + (extension) => + { + Assert.Equal("Extension1", extension.Key); + Assert.Equal("Extension1-Value", extension.Value.ToString()); + }, + (extension) => + { + Assert.Equal("Extension2", extension.Key); + Assert.Equal("Extension2-Value", extension.Value.ToString()); + }); + } + + [Fact] + public async Task WriteAsync_Applies_Defaults() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status500InternalServerError }, + }; + + //Act + await writer.WriteAsync(context); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(StatusCodes.Status500InternalServerError, problemDetails.Status); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); + Assert.Equal("An error occurred while processing your request.", problemDetails.Title); + } + + [Fact] + public async Task WriteAsync_Applies_CustomConfiguration() + { + // Arrange + var options = new ProblemDetailsOptions() + { + ConfigureDetails = (context, problem) => + { + problem.Status = StatusCodes.Status406NotAcceptable; + problem.Title = "Custom Title"; + problem.Extensions["new-extension"] = new { TraceId = Guid.NewGuid() }; + } + }; + var writer = GetWriter(options); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status500InternalServerError }, + }; + + //Act + await writer.WriteAsync(context, statusCode: StatusCodes.Status400BadRequest); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(StatusCodes.Status406NotAcceptable, problemDetails.Status); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Equal("Custom Title", problemDetails.Title); + Assert.Contains("new-extension", problemDetails.Extensions); + } + + [Fact] + public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status500InternalServerError }, + }; + + //Act + await writer.WriteAsync(context, statusCode: StatusCodes.Status400BadRequest); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(StatusCodes.Status400BadRequest, problemDetails.Status); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Equal("Bad Request", problemDetails.Title); + } + + private DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions? options = null) + { + options ??= new ProblemDetailsOptions(); + return new DefaultProblemDetailsWriter(Options.Create(options)); + } +} diff --git a/src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs index 6250ea43d7d3..066f346fa638 100644 --- a/src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs +++ b/src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs @@ -46,6 +46,7 @@ public void Read_Works() // Act var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); + //Assert Assert.Equal(type, problemDetails.Type); Assert.Equal(title, problemDetails.Title); Assert.Equal(status, problemDetails.Status); @@ -75,6 +76,7 @@ public void Read_UsingJsonSerializerWorks() // Act var problemDetails = JsonSerializer.Deserialize(json, JsonSerializerOptions); + // Assert Assert.Equal(type, problemDetails.Type); Assert.Equal(title, problemDetails.Title); Assert.Equal(status, problemDetails.Status); @@ -105,6 +107,7 @@ public void Read_WithSomeMissingValues_Works() // Act var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); + // Assert Assert.Equal(type, problemDetails.Type); Assert.Equal(title, problemDetails.Title); Assert.Equal(status, problemDetails.Status); diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs index 721697091bae..6b082743ea25 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs @@ -29,6 +29,25 @@ public void Apply_DefaultErrorTypeMetadata() Assert.Contains(selector.EndpointMetadata, m => m is ProducesErrorResponseTypeAttribute attribute && attribute.Type == errorType); } } + + [Fact] + public void Apply_SkipDefaultErrorTypeMetadata_WhenVoid() + { + // Arrange + var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); + var errorType = typeof(void); + var convention = GetConvention(errorType: errorType); + + //Act + convention.Apply(action); + + // Assert + foreach (var selector in action.Selectors) + { + Assert.DoesNotContain(selector.EndpointMetadata, m => m is ProducesErrorResponseTypeAttribute); + } + } + [Theory] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfActionResult))] From 987712097202aeb48cb6981e97a129545a60e124 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Jun 2022 15:11:46 -0700 Subject: [PATCH 22/59] Cleanup --- .../src/Infrastructure/DefaultProblemDetailsFactory.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs index 2caf14cb21ad..5a91c4922cf3 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs @@ -23,11 +23,6 @@ public DefaultProblemDetailsFactory( _configure = problemDetailsOptions?.Value?.ConfigureDetails; } - public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) - { - throw new NotImplementedException(); - } - public override ProblemDetails CreateProblemDetails( HttpContext httpContext, int? statusCode = null, From 2e0c98e9b47a0a486b55ffd9b974b2fbb9466b65 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Jun 2022 15:14:25 -0700 Subject: [PATCH 23/59] Clean up --- src/OpenApi/src/OpenApiGenerator.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/OpenApi/src/OpenApiGenerator.cs b/src/OpenApi/src/OpenApiGenerator.cs index a3c020af325f..454f3718ff9f 100644 --- a/src/OpenApi/src/OpenApiGenerator.cs +++ b/src/OpenApi/src/OpenApiGenerator.cs @@ -107,9 +107,8 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM responseType = typeof(void); } - var errorMetadata = metadata.GetMetadata(); + var errorMetadata = metadata.GetMetadata(); var defaultErrorType = errorMetadata?.Type; - var defaultErrorContentType = errorMetadata?.ContentType; var responseProviderMetadata = metadata.GetOrderedMetadata(); var producesResponseMetadata = metadata.GetOrderedMetadata(); @@ -129,11 +128,6 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM { discoveredTypeAnnotation = responseType; } - else if (defaultErrorType != null && defaultErrorContentType != null && statusCode >= 400 && statusCode < 500) - { - discoveredContentTypeAnnotation.Add(defaultErrorContentType); - discoveredTypeAnnotation = defaultErrorType; - } } foreach (var contentType in responseMetadata.ContentTypes) From e8f0e1a63a8c0fa6e76c92087d2de45c28c54798 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Jun 2022 15:20:07 -0700 Subject: [PATCH 24/59] clean up --- AspNetCore.sln | 2 +- .../src/Microsoft.AspNetCore.Http.Abstractions.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AspNetCore.sln b/AspNetCore.sln index f4639d67cb6a..9e4385ee93c3 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1738,7 +1738,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Too EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet-user-jwts", "{AB4B9E75-719C-4589-B852-20FBFD727730}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalJwtBearerSample", "src\Security\Authentication\JwtBearer\samples\MinimalJwtBearerSample\MinimalJwtBearerSample.csproj", "{7F079E92-32D5-4257-B95B-CFFB0D49C160}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalJwtBearerSample", "src\Security\Authentication\JwtBearer\samples\MinimalJwtBearerSample\MinimalJwtBearerSample.csproj", "{7F079E92-32D5-4257-B95B-CFFB0D49C160}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts.Tests", "src\Tools\dotnet-user-jwts\test\dotnet-user-jwts.Tests.csproj", "{89896261-C5DD-4901-BCA7-7A5F718BC008}" EndProject diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 1a8f0ff06b71..2b64507312fe 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -1,4 +1,4 @@ - + From 6316af72bc0cf78faad2ab428410af933d5620a6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Jun 2022 15:25:14 -0700 Subject: [PATCH 25/59] clean up --- src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs index 7718bdfb04a1..68e78af149bc 100644 --- a/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesErrorResponseTypeAttribute.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Mvc; From 0ed099e385059ce843bdd7015fff90f5d27f376e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Jun 2022 16:12:29 -0700 Subject: [PATCH 26/59] Removing nullable --- src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs index 105df3d345be..ef634b9cd59e 100644 --- a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs +++ b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs @@ -259,7 +259,7 @@ public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified() Assert.Equal("Bad Request", problemDetails.Title); } - private DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions? options = null) + private DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null) { options ??= new ProblemDetailsOptions(); return new DefaultProblemDetailsWriter(Options.Create(options)); From a8837066eda5d44a0bd74bc6c72d0993b6193743 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 24 Jun 2022 17:18:27 -0700 Subject: [PATCH 27/59] Simplifying public api --- .../src/Metadata/IProblemMetadata.cs | 20 ++ ...rosoft.AspNetCore.Http.Abstractions.csproj | 3 +- .../ProblemDetails/IProblemDetailsService.cs | 28 +- .../ProblemDetails/IProblemDetailsWriter.cs | 16 +- .../src/ProblemDetails/ProblemTypes.cs | 36 ++ .../Microsoft.AspNetCore.Http.Results.csproj | 2 +- .../Http/src/Builder/ApplicationBuilder.cs | 10 +- .../src/DefaultProblemDetailsWriter.cs | 28 +- .../ProblemDetailsOptions.cs | 34 +- ...soft.AspNetCore.Http.ProblemDetails.csproj | 6 +- .../src/ProblemDetailsService.cs | 81 +++-- .../src/PublicAPI.Unshipped.txt | 10 +- .../test/DefaultProblemDetailsWriterTest.cs | 10 +- .../test/ProblemDetailsServiceTest.cs | 32 +- src/Http/ProblemDetails/test/hcmd2eox.wbd~ | 328 ++++++++++++++++++ src/Http/Routing/src/EndpointMiddleware.cs | 5 +- .../Routing/src/EndpointRoutingMiddleware.cs | 9 + .../src/Matching/AcceptsMatcherPolicy.cs | 15 +- .../src/Matching/HttpMethodMatcherPolicy.cs | 13 +- .../src/Microsoft.AspNetCore.Routing.csproj | 1 + .../DeveloperExceptionPageMiddleware.cs | 5 +- .../ExceptionHandlerMiddleware.cs | 7 +- .../StatusCodePage/StatusCodePagesOptions.cs | 19 +- .../EndpointMetadataConvention.cs | 27 +- .../src/Formatters/TextOutputFormatter.cs | 4 +- .../DefaultApiProblemDetailsWriter.cs | 31 +- .../Infrastructure/ObjectResultExecutor.cs | 4 +- .../ProblemDetailsClientErrorFactory.cs | 5 +- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 6 +- ...tpValidationProblemDetailsJsonConverter.cs | 0 .../ProblemDetailsDefaults.cs | 0 .../ProblemDetailsJsonConverter.cs | 0 src/Shared/ProblemDetails/ProblemMetadata.cs | 16 + .../ProblemDetails/RoutingProblemMetadata.cs | 18 + 34 files changed, 594 insertions(+), 235 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs create mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs create mode 100644 src/Http/ProblemDetails/test/hcmd2eox.wbd~ rename src/Shared/{ => ProblemDetails}/HttpValidationProblemDetailsJsonConverter.cs (100%) rename src/Shared/{ => ProblemDetails}/ProblemDetailsDefaults.cs (100%) rename src/Shared/{ => ProblemDetails}/ProblemDetailsJsonConverter.cs (100%) create mode 100644 src/Shared/ProblemDetails/ProblemMetadata.cs create mode 100644 src/Shared/ProblemDetails/RoutingProblemMetadata.cs diff --git a/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs new file mode 100644 index 000000000000..14d74ea962d3 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs @@ -0,0 +1,20 @@ +// 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.Metadata; + +/// +/// +/// +public interface IProblemMetadata +{ + /// + /// + /// + public int? StatusCode { get; } + + /// + /// + /// + public ProblemTypes ProblemType { get; } +} diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 2b64507312fe..3b2b2e8e2d17 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -26,8 +26,7 @@ Microsoft.AspNetCore.Http.HttpResponse - - + diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs index e48802351175..545a5506a462 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs @@ -3,40 +3,22 @@ namespace Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; + /// /// /// public interface IProblemDetailsService { - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + bool IsEnabled(ProblemTypes type); + Task WriteAsync( HttpContext context, - EndpointMetadataCollection? currentMetadata = null, - bool isRouting = false, + EndpointMetadataCollection? additionalMetadata = null, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, IDictionary? extensions = null); - - /// - /// - /// - /// - /// - /// - bool IsEnabled(int statusCode, bool isRouting = false); } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index c167b0145b7b..33e8533e9fa0 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -12,10 +12,8 @@ public interface IProblemDetailsWriter /// /// /// - /// - /// /// - bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting); + bool CanWrite(HttpContext context); /// /// @@ -30,10 +28,10 @@ public interface IProblemDetailsWriter /// Task WriteAsync( HttpContext context, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null); + int? statusCode, + string? title, + string? type, + string? detail, + string? instance, + IDictionary? extensions); } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs new file mode 100644 index 000000000000..a58030b19d76 --- /dev/null +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// +/// +[Flags] +public enum ProblemTypes : uint +{ + /// + /// + /// + Unspecified = 0, + + /// + /// + /// + Server = 1, + + /// + /// 404 / 405 / 415 + /// + Routing = 2, + + /// + /// + /// + Client = 4, + + /// + /// + /// + All = Routing | Server | Client, +} diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index de0934e22002..7808c52b5ec9 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index 561c0f12498c..8a0265c93bc6 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -129,15 +129,7 @@ public RequestDelegate Build() $"routing."; throw new InvalidOperationException(message); } - - const int statusCode = StatusCodes.Status404NotFound; - context.Response.StatusCode = statusCode; - - if (context.RequestServices.GetService() is { } problemDetailsService) - { - return problemDetailsService.WriteAsync(context, isRouting: true); - } - + context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index 8c7bc1c484e7..08cfdb2a9de5 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -16,8 +16,7 @@ public DefaultProblemDetailsWriter(IOptions options) _options = options.Value; } - public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) - => isRouting || (context.Response.StatusCode >= 500 && context.Response.StatusCode <= 599); + public bool CanWrite(HttpContext context) => true; public Task WriteAsync( HttpContext context, @@ -28,27 +27,10 @@ public Task WriteAsync( string? instance = null, IDictionary? extensions = null) { - var problemDetails = new ProblemDetails - { - Status = statusCode, - Title = title, - Type = type, - Detail = detail, - Instance = instance - }; - - if (extensions is not null) - { - foreach (var extension in extensions) - { - problemDetails.Extensions[extension.Key] = extension.Value; - } - } - - ProblemDetailsDefaults.Apply(problemDetails, context.Response.StatusCode); - _options.ConfigureDetails?.Invoke(context, problemDetails); - - return context.Response.WriteAsJsonAsync(problemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); + var problemResult = TypedResults.Problem(detail, instance, statusCode, title, type, extensions); + _options.ConfigureDetails?.Invoke(context, problemResult.ProblemDetails); + + return problemResult.ExecuteAsync(context); } [JsonSerializable(typeof(ProblemDetails))] diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs index 73f80ad60f1f..d9a8ad436209 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs @@ -13,42 +13,10 @@ public class ProblemDetailsOptions /// /// /// - public MappingOptions AllowedMapping { get; set; } = MappingOptions.All; + public ProblemTypes AllowedProblemTypes { get; set; } = ProblemTypes.All; /// /// /// public Action? ConfigureDetails { get; set; } } - -/// -/// -/// -[Flags] -public enum MappingOptions : uint -{ - /// - /// - /// - Unspecified = 0, - - /// - /// - /// - Exceptions = 1, - - /// - /// 404 / 405 / 415 - /// - RoutingFailures = 2, - - /// - /// - /// - ClientErrors = 4, - - /// - /// - /// - All = RoutingFailures | Exceptions | ClientErrors, -} diff --git a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj index 97747d93de56..4aca8321df6a 100644 --- a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj +++ b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj @@ -12,14 +12,12 @@ - + - - - + diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs index 71945928c7b0..e540d749e272 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http; @@ -19,33 +20,19 @@ public ProblemDetailsService( _options = options.Value; } - /// - /// - /// - /// - /// - /// - public bool IsEnabled(int statusCode, bool isRouting = false) + public bool IsEnabled(ProblemTypes type) { - if (_options.AllowedMapping == MappingOptions.Unspecified) + if (_options.AllowedProblemTypes == ProblemTypes.Unspecified) { return false; } - return isRouting ? - _options.AllowedMapping.HasFlag(MappingOptions.RoutingFailures) : - statusCode switch - { - >= 400 and <= 499 => _options.AllowedMapping.HasFlag(MappingOptions.ClientErrors), - >= 500 and <= 599 => _options.AllowedMapping.HasFlag(MappingOptions.Exceptions), - _ => false, - }; + return _options.AllowedProblemTypes.HasFlag(type); } public Task WriteAsync( HttpContext context, - EndpointMetadataCollection? currentMetadata = null, - bool isRouting = false, + EndpointMetadataCollection? additionalMetadata = null, int? statusCode = null, string? title = null, string? type = null, @@ -53,28 +40,56 @@ public Task WriteAsync( string? instance = null, IDictionary? extensions = null) { - var writer = GetWriter(context, currentMetadata, isRouting); - return writer != null ? - writer.WriteAsync(context, statusCode, title, type, detail, instance, extensions) : - Task.CompletedTask; - } + static ProblemTypes CalculateProblemType( + HttpContext context, + EndpointMetadataCollection? metadataCollection, + int statusCode) + { + var problemMetadata = metadataCollection?.GetMetadata() ?? + context.GetEndpoint()?.Metadata.GetMetadata(); - // Internal for testing - internal IProblemDetailsWriter? GetWriter( - HttpContext context, - EndpointMetadataCollection? currentMetadata, - bool isRouting) - { - if (!IsEnabled(context.Response.StatusCode, isRouting)) + if (problemMetadata != null) + { + var expectedProblemType = statusCode >= 500 ? ProblemTypes.Server : ProblemTypes.Client; + + if (problemMetadata.StatusCode == statusCode) + { + return problemMetadata.ProblemType; + } + else if (problemMetadata.StatusCode == null && + problemMetadata.ProblemType.HasFlag(expectedProblemType)) + { + return expectedProblemType; + } + } + + return ProblemTypes.Unspecified; + } + + var problemStatusCode = statusCode ?? context.Response.StatusCode; + var problemType = CalculateProblemType(context, additionalMetadata, statusCode: problemStatusCode); + + if (IsEnabled(problemType) && GetWriter(context) is { } writer) { - return null; + return writer.WriteAsync( + context, + statusCode, + title, + type, + detail, + instance, + extensions); } - currentMetadata ??= context.GetEndpoint()?.Metadata; + return Task.CompletedTask; + } + // Internal for testing + internal IProblemDetailsWriter? GetWriter(HttpContext context) + { for (var i = 0; i < _writers.Length; i++) { - if (_writers[i].CanWrite(context, currentMetadata, isRouting: isRouting)) + if (_writers[i].CanWrite(context)) { return _writers[i]; } diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt index 9e8b33ae12b3..bc1556eafd47 100644 --- a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt +++ b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt @@ -1,13 +1,7 @@ #nullable enable -Microsoft.AspNetCore.Http.MappingOptions -Microsoft.AspNetCore.Http.MappingOptions.All = Microsoft.AspNetCore.Http.MappingOptions.Exceptions | Microsoft.AspNetCore.Http.MappingOptions.RoutingFailures | Microsoft.AspNetCore.Http.MappingOptions.ClientErrors -> Microsoft.AspNetCore.Http.MappingOptions -Microsoft.AspNetCore.Http.MappingOptions.ClientErrors = 4 -> Microsoft.AspNetCore.Http.MappingOptions -Microsoft.AspNetCore.Http.MappingOptions.Exceptions = 1 -> Microsoft.AspNetCore.Http.MappingOptions -Microsoft.AspNetCore.Http.MappingOptions.RoutingFailures = 2 -> Microsoft.AspNetCore.Http.MappingOptions -Microsoft.AspNetCore.Http.MappingOptions.Unspecified = 0 -> Microsoft.AspNetCore.Http.MappingOptions Microsoft.AspNetCore.Http.ProblemDetailsOptions -Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.get -> Microsoft.AspNetCore.Http.MappingOptions -Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedMapping.set -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedProblemTypes.get -> Microsoft.AspNetCore.Http.ProblemTypes +Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedProblemTypes.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.get -> System.Action? Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void diff --git a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs index ef634b9cd59e..1359042d6ddb 100644 --- a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs +++ b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs @@ -25,7 +25,7 @@ public void CanWrite_IsFalse_ForSuccessStatus(int statusCode) }; // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); // Assert Assert.False(canWrite); @@ -46,7 +46,7 @@ public void CanWrite_IsFalse_ForUnknownStatus(int statusCode) }; // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); // Assert Assert.False(canWrite); @@ -65,7 +65,7 @@ public void CanWrite_IsFalse_ForClientErrors(int statusCode) }; // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); // Assert Assert.False(canWrite); @@ -84,7 +84,7 @@ public void CanWrite_IsTrue_ForServerErrors(int statusCode) }; // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: false); + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); // Assert Assert.True(canWrite); @@ -98,7 +98,7 @@ public void CanWrite_IsTrue_ForRoutingErrors() var context = new DefaultHttpContext(); // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty, isRouting: true); + var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); // Assert Assert.True(canWrite); diff --git a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs index a30cb8b0c2d7..3720170676ae 100644 --- a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs +++ b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs @@ -19,7 +19,7 @@ public void IsEnable_ReturnsFalse_ForRouting_WhenDisable() { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.ClientErrors | MappingOptions.Exceptions }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.ClientErrors | ProblemTypes.Exceptions }); //Act var isEnabled = service.IsEnabled(400, isRouting: true); @@ -33,7 +33,7 @@ public void IsEnable_ReturnsTrue_ForRouting_WhenEnabled() { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.RoutingFailures }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.RoutingFailures }); //Act var isEnabled = service.IsEnabled(400, isRouting: true); @@ -51,7 +51,7 @@ public void IsEnable_ReturnsFalse_WhenUnspecified(int statuCode) { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.Unspecified }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.Unspecified }); //Act var isEnabled = service.IsEnabled(statuCode, isRouting: false); @@ -69,7 +69,7 @@ public void IsEnable_ReturnsFalse_ForSuccessStatus(int statuCode) { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); //Act var isEnabled = service.IsEnabled(statuCode, isRouting: false); @@ -87,7 +87,7 @@ public void IsEnable_ReturnsFalse_ForUnknownStatus(int statuCode) { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); //Act var isEnabled = service.IsEnabled(statuCode, isRouting: false); @@ -105,7 +105,7 @@ public void IsEnable_ReturnsTrue_ForClientErrors(int statuCode) { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.ClientErrors }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.ClientErrors }); //Act var isEnabled = service.IsEnabled(statuCode, isRouting: false); @@ -121,7 +121,7 @@ public void IsEnable_ReturnsTrue_ForServerErrors(int statuCode) { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.Exceptions }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.Exceptions }); //Act var isEnabled = service.IsEnabled(statuCode, isRouting: false); @@ -135,7 +135,7 @@ public void GetWriter_ReturnsNull_WhenNotEnabled() { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.Unspecified }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.Unspecified }); var context = new DefaultHttpContext() { Response = { StatusCode = StatusCodes.Status400BadRequest } @@ -153,7 +153,7 @@ public void GetWriter_ReturnsNull_WhenNotRegisteredWriters() { //Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); var context = new DefaultHttpContext() { Response = { StatusCode = StatusCodes.Status400BadRequest } @@ -176,7 +176,7 @@ public void GetWriter_ReturnsNull_WhenNoWriterCanWrite() }; var service = CreateService( writers: writers, - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); var context = new DefaultHttpContext() { Response = { StatusCode = StatusCodes.Status400BadRequest } @@ -195,7 +195,7 @@ public void GetWriter_Returns_ForContextMetadata() //Arrange var service = CreateService( writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); var context = new DefaultHttpContext() { @@ -218,7 +218,7 @@ public void GetWriter_Returns_ForSpecifiedMetadata() //Arrange var service = CreateService( writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); var context = new DefaultHttpContext() { @@ -244,7 +244,7 @@ public void GetWriter_Returns_FirstCanWriter() var writers = new List() { writer1, writer2 }; var service = CreateService( writers: writers, - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); var context = new DefaultHttpContext() { Response = { StatusCode = StatusCodes.Status400BadRequest } @@ -264,7 +264,7 @@ public async Task WriteAsync_Call_SelectedWriter() //Arrange var service = CreateService( writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); var stream = new MemoryStream(); @@ -286,7 +286,7 @@ public async Task WriteAsync_Skip_WhenNoWriter() //Arrange var service = CreateService( writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = MappingOptions.All }); + options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); var stream = new MemoryStream(); var context = new DefaultHttpContext() { @@ -315,7 +315,7 @@ private class SampleMetadata private class MetadataBasedWriter : IProblemDetailsWriter { - public bool CanWrite(HttpContext context, EndpointMetadataCollection metadata, bool isRouting) + public bool CanWrite(HttpContext context, ProblemTypes problemType) { return metadata != null && metadata.GetMetadata != null; } diff --git a/src/Http/ProblemDetails/test/hcmd2eox.wbd~ b/src/Http/ProblemDetails/test/hcmd2eox.wbd~ new file mode 100644 index 000000000000..bf94586fbf00 --- /dev/null +++ b/src/Http/ProblemDetails/test/hcmd2eox.wbd~ @@ -0,0 +1,328 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.AspNetCore.Http.Tests; + +public class ProblemDetailsServiceTest +{ + [Fact] + public void IsEnable_ReturnsFalse_ForRouting_WhenDisable() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.ClientErrors | ProblemTypes.Exceptions }); + + //Act + var isEnabled = service.IsEnabled(400, isRouting: true); + + //Assert + Assert.False(isEnabled); + } + + [Fact] + public void IsEnable_ReturnsTrue_ForRouting_WhenEnabled() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.RoutingFailures }); + + //Act + var isEnabled = service.IsEnabled(400, isRouting: true); + + //Assert + Assert.True(isEnabled); + } + + [Theory] + [InlineData(100)] + [InlineData(300)] + [InlineData(400)] + [InlineData(500)] + public void IsEnable_ReturnsFalse_WhenUnspecified(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.Unspecified }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.False(isEnabled); + } + + [Theory] + [InlineData(100)] + [InlineData(200)] + [InlineData(300)] + [InlineData(399)] + public void IsEnable_ReturnsFalse_ForSuccessStatus(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.False(isEnabled); + } + + [Theory] + [InlineData(0)] + [InlineData(99)] + [InlineData(600)] + [InlineData(700)] + public void IsEnable_ReturnsFalse_ForUnknownStatus(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.False(isEnabled); + } + + [Theory] + [InlineData(400)] + [InlineData(415)] + [InlineData(422)] + [InlineData(499)] + public void IsEnable_ReturnsTrue_ForClientErrors(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.ClientErrors }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.True(isEnabled); + } + + [Theory] + [InlineData(500)] + [InlineData(599)] + public void IsEnable_ReturnsTrue_ForServerErrors(int statuCode) + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.Exceptions }); + + //Act + var isEnabled = service.IsEnabled(statuCode, isRouting: false); + + //Assert + Assert.True(isEnabled); + } + + [Fact] + public void GetWriter_ReturnsNull_WhenNotEnabled() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.Unspecified }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.Null(writer); + } + + [Fact] + public void GetWriter_ReturnsNull_WhenNotRegisteredWriters() + { + //Arrange + var service = CreateService( + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.Null(writer); + } + + [Fact] + public void GetWriter_ReturnsNull_WhenNoWriterCanWrite() + { + //Arrange + var writers = new List() { + Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false), + Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false) + }; + var service = CreateService( + writers: writers, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.Null(writer); + } + + [Fact] + public void GetWriter_Returns_ForContextMetadata() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json"}); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); + + //Act + var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.NotNull(selectedWriter); + Assert.IsType(selectedWriter); + } + + [Fact] + public void GetWriter_Returns_ForSpecifiedMetadata() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, EndpointMetadataCollection.Empty, null)); + + //Act + var selectedWriter = service.GetWriter(context, currentMetadata: metadata, isRouting: false); + + //Assert + Assert.NotNull(selectedWriter); + Assert.IsType(selectedWriter); + } + + [Fact] + public void GetWriter_Returns_FirstCanWriter() + { + //Arrange + var writer1 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); + var writer2 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); + var writers = new List() { writer1, writer2 }; + var service = CreateService( + writers: writers, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + + //Act + var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); + + //Assert + Assert.NotNull(selectedWriter); + Assert.Equal(writer1, selectedWriter); + } + + [Fact] + public async Task WriteAsync_Call_SelectedWriter() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + }; + + //Act + await service.WriteAsync(context, currentMetadata: metadata); + + //Assert + Assert.Equal("\"Content\"", Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public async Task WriteAsync_Skip_WhenNoWriter() + { + //Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + }; + + //Act + await service.WriteAsync(context); + + //Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); + } + + private static ProblemDetailsService CreateService( + ProblemDetailsOptions options, + IEnumerable writers = null) + { + writers ??= Array.Empty(); + return new ProblemDetailsService(writers, Options.Create(options)); + } + + private class SampleMetadata + { + public string ContentType { get; set; } + } + + private class MetadataBasedWriter : IProblemDetailsWriter + { + public bool CanWrite(HttpContext context, ProblemTypes problemType) + { + return metadata != null && metadata.GetMetadata != null; + } + + public Task WriteAsync(HttpContext context, int? statusCode = null, string title = null, string type = null, string detail = null, string instance = null, IDictionary extensions = null) + { + return context.Response.WriteAsJsonAsync("Content"); + } + } +} diff --git a/src/Http/Routing/src/EndpointMiddleware.cs b/src/Http/Routing/src/EndpointMiddleware.cs index d04af317c0e4..303a45188fa7 100644 --- a/src/Http/Routing/src/EndpointMiddleware.cs +++ b/src/Http/Routing/src/EndpointMiddleware.cs @@ -70,7 +70,10 @@ public Task Invoke(HttpContext httpContext) return _next(httpContext); - static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger) + static async Task AwaitRequestTask( + Endpoint endpoint, + Task requestTask, + ILogger logger) { try { diff --git a/src/Http/Routing/src/EndpointRoutingMiddleware.cs b/src/Http/Routing/src/EndpointRoutingMiddleware.cs index fe336e869c95..fa94e6d0258b 100644 --- a/src/Http/Routing/src/EndpointRoutingMiddleware.cs +++ b/src/Http/Routing/src/EndpointRoutingMiddleware.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Logging; @@ -12,6 +13,7 @@ namespace Microsoft.AspNetCore.Routing; internal sealed partial class EndpointRoutingMiddleware { + internal const string Http404EndpointDisplayName = "404 HTTP Not Found"; private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched"; private readonly MatcherFactory _matcherFactory; @@ -95,6 +97,13 @@ private Task SetRoutingAndContinue(HttpContext httpContext) if (endpoint == null) { Log.MatchFailure(_logger); + httpContext.SetEndpoint(new Endpoint(context => + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return Task.CompletedTask; + }, + new EndpointMetadataCollection(new RoutingProblemMetadata(StatusCodes.Status404NotFound)), + Http404EndpointDisplayName)); } else { diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 65f30c3870c1..7bff38c5e3e8 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Routing.Matching; @@ -259,20 +258,12 @@ public IReadOnlyList GetEdges(IReadOnlyList endpoints) private static Endpoint CreateRejectionEndpoint() { - return new Endpoint( - context => + return new Endpoint(context => { - const int statusCode = StatusCodes.Status415UnsupportedMediaType; - context.Response.StatusCode = statusCode; - - if (context.RequestServices.GetService() is { } problemDetailsService) - { - return problemDetailsService.WriteAsync(context, isRouting: true); - } - + context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; return Task.CompletedTask; }, - EndpointMetadataCollection.Empty, + new EndpointMetadataCollection(new RoutingProblemMetadata(StatusCodes.Status415UnsupportedMediaType)), Http415EndpointDisplayName); } diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index b6ca9c07612f..209d4caa6054 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -408,18 +409,10 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods // Prevent ArgumentException from duplicate key if header already added, such as when the // request is re-executed by an error handler (see https://github.com/dotnet/aspnetcore/issues/6415) context.Response.Headers.Allow = allow; - - const int statusCode = StatusCodes.Status405MethodNotAllowed; - context.Response.StatusCode = statusCode; - - if (context.RequestServices.GetService() is { } problemDetailsService) - { - return problemDetailsService.WriteAsync(context, isRouting: true); - } - + context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; return Task.CompletedTask; }, - EndpointMetadataCollection.Empty, + new EndpointMetadataCollection(new RoutingProblemMetadata(StatusCodes.Status405MethodNotAllowed)), Http405EndpointDisplayName); } diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 0e1f47d2ab67..8739aa10ce1a 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 1cd4565ff8b2..3cbc26d39e1e 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -186,7 +186,10 @@ private async Task DisplayExceptionContent(ErrorContext errorContext) } }; - await _problemDetailsService.WriteAsync(httpContext, extensions: exceptionExtensions); + await _problemDetailsService.WriteAsync( + httpContext, + extensions: exceptionExtensions, + statusCode: httpContext.Response.StatusCode); } // If the response has not started, assume the problem details was not written. diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index f1a0fb3bc403..530c41491f28 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -54,7 +54,7 @@ public ExceptionHandlerMiddleware( if (_options.ExceptionHandlingPath == null) { if (problemDetailsService == null || - problemDetailsService.IsEnabled(statusCode: DefaultStatusCode) == false) + problemDetailsService.IsEnabled(ProblemTypes.Server) == false) { throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); } @@ -149,7 +149,10 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed } else { - await _problemDetailsService!.WriteAsync(context, exceptionHandlerFeature.Endpoint?.Metadata); + await _problemDetailsService!.WriteAsync( + context, + exceptionHandlerFeature.Endpoint?.Metadata, + statusCode: DefaultStatusCode); } // If the response has already started, assume exception handler was successful. diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs index 56c00d2ea67a..0640e81f2a69 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Builder; @@ -19,15 +20,25 @@ public class StatusCodePagesOptions /// public StatusCodePagesOptions() { - HandleAsync = context => + HandleAsync = async context => { // TODO: Render with a pre-compiled html razor view. var statusCode = context.HttpContext.Response.StatusCode; - var body = BuildResponseBody(statusCode); + if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) + { + await problemDetailsService.WriteAsync( + context.HttpContext, + statusCode: statusCode); + } - context.HttpContext.Response.ContentType = "text/plain"; - return context.HttpContext.Response.WriteAsync(body); + if (!context.HttpContext.Response.HasStarted) + { + var body = BuildResponseBody(statusCode); + + context.HttpContext.Response.ContentType = "text/plain"; + await context.HttpContext.Response.WriteAsync(body); + } }; } diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs index 3668139126f3..0645a7b8edfa 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -22,13 +24,8 @@ public EndpointMetadataConvention(IServiceProvider serviceProvider, Type default public void Apply(ActionModel action) { - if (_defaultErrorType != typeof(void)) - { - for (var i = 0; i < action.Selectors.Count; i++) - { - action.Selectors[i].EndpointMetadata.Add(new ProducesErrorResponseTypeAttribute(_defaultErrorType)); - } - } + // Set the problem metadata when defaultError is ProblemDetails + ApplyProblemMetadata(action); // Get metadata from parameter types ApplyParametersMetadata(action); @@ -37,6 +34,22 @@ public void Apply(ActionModel action) ApplyReturnTypeMetadata(action); } + private void ApplyProblemMetadata(ActionModel action) + { + if (_defaultErrorType == typeof(ProblemDetails)) + { + var problemDetailsService = _serviceProvider.GetService(); + + if (problemDetailsService != null) + { + for (var i = 0; i < action.Selectors.Count; i++) + { + action.Selectors[i].EndpointMetadata.Add(new ProblemMetadata()); + } + } + } + } + private void ApplyReturnTypeMetadata(ActionModel action) { var returnType = action.ActionMethod.ReturnType; diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index f71db7f50d65..85ca15690180 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -138,7 +138,9 @@ public override Task WriteAsync(OutputFormatterWriteContext context) if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsService.WriteAsync(context.HttpContext, isRouting: true); + return problemDetailsService.WriteAsync( + context.HttpContext, + statusCode: statusCode); } return Task.CompletedTask; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index 6b4b93372163..583c0a229582 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Formatters; namespace Microsoft.AspNetCore.Mvc.Infrastructure; @@ -27,31 +28,10 @@ public DefaultApiProblemDetailsWriter( _problemDetailsFactory = problemDetailsFactory; } - public bool CanWrite(HttpContext context, EndpointMetadataCollection? metadata, bool isRouting) + public bool CanWrite(HttpContext context) { - static bool HasMetadata(EndpointMetadataCollection? metadata) - { - var responseType = metadata?.GetMetadata(); - var apiControllerAttribute = metadata?.GetMetadata(); - - if (apiControllerAttribute != null && responseType?.Type == typeof(ProblemDetails)) - { - return true; - } - return false; - - } - - if (isRouting) - { - return false; - } - - return context.Response.StatusCode switch - { - >= 400 and <= 499 => HasMetadata(metadata), - _ => false, - }; + var apiControllerAttribute = context.GetEndpoint()?.Metadata.GetMetadata(); + return apiControllerAttribute != null; } public Task WriteAsync( @@ -86,7 +66,8 @@ public Task WriteAsync( if (selectedFormatter == null) { - return Task.CompletedTask; + return Results.Problem(problemDetails) + .ExecuteAsync(context); } return selectedFormatter.WriteAsync(formatterContext); diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 7cb7dabfa0af..70600aea056b 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -121,7 +121,9 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsService.WriteAsync(context.HttpContext, isRouting: true); + return problemDetailsService.WriteAsync( + context.HttpContext, + statusCode: statusCode); } return Task.CompletedTask; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index d2946a6c0e15..b7c0dc9622b1 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -24,8 +24,11 @@ public ProblemDetailsClientErrorFactory( public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { + var statusCode = clientError.StatusCode ?? 500; + var problemType = statusCode >= 500 ? ProblemTypes.Server : ProblemTypes.Client; + if (_problemDetailsService != null && - _problemDetailsService.IsEnabled(clientError.StatusCode ?? 500) == false) + _problemDetailsService.IsEnabled(problemType) == false) { return null; } diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 2ca504a56f8e..5f8a80cef356 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core MVC core components. Contains common action result types, attribute routing, application model conventions, API explorer, application parts, filters, formatters, model binding, and more. @@ -28,9 +28,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - - - + diff --git a/src/Shared/HttpValidationProblemDetailsJsonConverter.cs b/src/Shared/ProblemDetails/HttpValidationProblemDetailsJsonConverter.cs similarity index 100% rename from src/Shared/HttpValidationProblemDetailsJsonConverter.cs rename to src/Shared/ProblemDetails/HttpValidationProblemDetailsJsonConverter.cs diff --git a/src/Shared/ProblemDetailsDefaults.cs b/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs similarity index 100% rename from src/Shared/ProblemDetailsDefaults.cs rename to src/Shared/ProblemDetails/ProblemDetailsDefaults.cs diff --git a/src/Shared/ProblemDetailsJsonConverter.cs b/src/Shared/ProblemDetails/ProblemDetailsJsonConverter.cs similarity index 100% rename from src/Shared/ProblemDetailsJsonConverter.cs rename to src/Shared/ProblemDetails/ProblemDetailsJsonConverter.cs diff --git a/src/Shared/ProblemDetails/ProblemMetadata.cs b/src/Shared/ProblemDetails/ProblemMetadata.cs new file mode 100644 index 000000000000..84272770e6bd --- /dev/null +++ b/src/Shared/ProblemDetails/ProblemMetadata.cs @@ -0,0 +1,16 @@ +// 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.Metadata; + +internal class ProblemMetadata : IProblemMetadata +{ + public ProblemMetadata(ProblemTypes problemType = ProblemTypes.All) + { + ProblemType = problemType; + } + + public int? StatusCode => null; + + public ProblemTypes ProblemType { get; } +} diff --git a/src/Shared/ProblemDetails/RoutingProblemMetadata.cs b/src/Shared/ProblemDetails/RoutingProblemMetadata.cs new file mode 100644 index 000000000000..9e0fd12acef0 --- /dev/null +++ b/src/Shared/ProblemDetails/RoutingProblemMetadata.cs @@ -0,0 +1,18 @@ +// 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.Metadata; + +using Microsoft.AspNetCore.Http; + +internal sealed class RoutingProblemMetadata : IProblemMetadata +{ + public RoutingProblemMetadata(int statusCode = StatusCodes.Status404NotFound) + { + StatusCode = statusCode; + } + + public int? StatusCode { get; } + + public ProblemTypes ProblemType => ProblemTypes.Routing; +} From 1ecb0e023d38aae7d9153a7f451cf1aae9d4984d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 27 Jun 2022 15:18:56 -0700 Subject: [PATCH 28/59] API Review feedback --- .../src/Metadata/IProblemMetadata.cs | 4 +- .../ProblemDetails/IProblemDetailsService.cs | 4 - .../ProblemDetails/IProblemDetailsWriter.cs | 2 +- .../src/ProblemDetails/ProblemTypes.cs | 4 +- .../src/DefaultProblemDetailsWriter.cs | 28 +- .../ProblemDetailsOptions.cs | 2 +- .../src/ProblemDetailsService.cs | 114 +++-- .../test/DefaultProblemDetailsWriterTest.cs | 20 +- .../test/ProblemDetailsServiceTest.cs | 404 ++++++++++-------- .../ExceptionHandlerMiddleware.cs | 21 +- .../DefaultApiProblemDetailsWriter.cs | 6 +- .../ProblemDetailsClientErrorFactory.cs | 14 +- ...ApiBehaviorApplicationModelProviderTest.cs | 3 +- .../EndpointMetadataConventionTest.cs | 32 +- .../ProblemDetalsClientErrorFactoryTest.cs | 10 +- src/Shared/ProblemDetails/ProblemMetadata.cs | 6 +- .../ProblemDetails/RoutingProblemMetadata.cs | 4 +- 17 files changed, 397 insertions(+), 281 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs index 14d74ea962d3..22fa2615dc4a 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Http.Metadata; /// /// /// -public interface IProblemMetadata +public interface IProblemDetailsMetadata { /// /// @@ -16,5 +16,5 @@ public interface IProblemMetadata /// /// /// - public ProblemTypes ProblemType { get; } + public ProblemDetailsTypes ProblemType { get; } } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs index 545a5506a462..5624acc505aa 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs @@ -3,15 +3,11 @@ namespace Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; - /// /// /// public interface IProblemDetailsService { - bool IsEnabled(ProblemTypes type); - Task WriteAsync( HttpContext context, EndpointMetadataCollection? additionalMetadata = null, diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index 33e8533e9fa0..32f2be6bfa75 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -13,7 +13,7 @@ public interface IProblemDetailsWriter /// /// /// - bool CanWrite(HttpContext context); + bool CanWrite(HttpContext context, EndpointMetadataCollection? additionalMetadata); /// /// diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs index a58030b19d76..4290bb39facc 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs @@ -7,12 +7,12 @@ namespace Microsoft.AspNetCore.Http; /// /// [Flags] -public enum ProblemTypes : uint +public enum ProblemDetailsTypes : uint { /// /// /// - Unspecified = 0, + None = 0, /// /// diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index 08cfdb2a9de5..c2fc041ec778 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -16,7 +16,8 @@ public DefaultProblemDetailsWriter(IOptions options) _options = options.Value; } - public bool CanWrite(HttpContext context) => true; + public bool CanWrite(HttpContext context, EndpointMetadataCollection? additionalMetadata) + => context.Response.StatusCode >= 400 && context.Response.StatusCode <= 599; public Task WriteAsync( HttpContext context, @@ -27,10 +28,27 @@ public Task WriteAsync( string? instance = null, IDictionary? extensions = null) { - var problemResult = TypedResults.Problem(detail, instance, statusCode, title, type, extensions); - _options.ConfigureDetails?.Invoke(context, problemResult.ProblemDetails); - - return problemResult.ExecuteAsync(context); + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance + }; + + if (extensions is not null) + { + foreach (var extension in extensions) + { + problemDetails.Extensions[extension.Key] = extension.Value; + } + } + + ProblemDetailsDefaults.Apply(problemDetails, context.Response.StatusCode); + _options.ConfigureDetails?.Invoke(context, problemDetails); + + return context.Response.WriteAsJsonAsync(problemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); } [JsonSerializable(typeof(ProblemDetails))] diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs index d9a8ad436209..164c0cbaeaf4 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs @@ -13,7 +13,7 @@ public class ProblemDetailsOptions /// /// /// - public ProblemTypes AllowedProblemTypes { get; set; } = ProblemTypes.All; + public ProblemDetailsTypes AllowedProblemTypes { get; set; } = ProblemDetailsTypes.All; /// /// diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs index e540d749e272..6ae3da95d558 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -20,16 +20,6 @@ public ProblemDetailsService( _options = options.Value; } - public bool IsEnabled(ProblemTypes type) - { - if (_options.AllowedProblemTypes == ProblemTypes.Unspecified) - { - return false; - } - - return _options.AllowedProblemTypes.HasFlag(type); - } - public Task WriteAsync( HttpContext context, EndpointMetadataCollection? additionalMetadata = null, @@ -40,56 +30,34 @@ public Task WriteAsync( string? instance = null, IDictionary? extensions = null) { - static ProblemTypes CalculateProblemType( - HttpContext context, - EndpointMetadataCollection? metadataCollection, - int statusCode) + if (_options.AllowedProblemTypes != ProblemDetailsTypes.None && _writers is { Length: > 0 }) { - var problemMetadata = metadataCollection?.GetMetadata() ?? - context.GetEndpoint()?.Metadata.GetMetadata(); + var problemStatusCode = statusCode ?? context.Response.StatusCode; + var calculatedProblemType = CalculateProblemType(problemStatusCode, context.GetEndpoint()?.Metadata, additionalMetadata); - if (problemMetadata != null) + if ((_options.AllowedProblemTypes & calculatedProblemType) != ProblemDetailsTypes.None && + GetWriter(context, additionalMetadata) is { } writer) { - var expectedProblemType = statusCode >= 500 ? ProblemTypes.Server : ProblemTypes.Client; - - if (problemMetadata.StatusCode == statusCode) - { - return problemMetadata.ProblemType; - } - else if (problemMetadata.StatusCode == null && - problemMetadata.ProblemType.HasFlag(expectedProblemType)) - { - return expectedProblemType; - } + return writer.WriteAsync( + context, + statusCode, + title, + type, + detail, + instance, + extensions); } - - return ProblemTypes.Unspecified; - } - - var problemStatusCode = statusCode ?? context.Response.StatusCode; - var problemType = CalculateProblemType(context, additionalMetadata, statusCode: problemStatusCode); - - if (IsEnabled(problemType) && GetWriter(context) is { } writer) - { - return writer.WriteAsync( - context, - statusCode, - title, - type, - detail, - instance, - extensions); } return Task.CompletedTask; } // Internal for testing - internal IProblemDetailsWriter? GetWriter(HttpContext context) + internal IProblemDetailsWriter? GetWriter(HttpContext context, EndpointMetadataCollection? additionalMetadata) { for (var i = 0; i < _writers.Length; i++) { - if (_writers[i].CanWrite(context)) + if (_writers[i].CanWrite(context, additionalMetadata)) { return _writers[i]; } @@ -97,4 +65,56 @@ static ProblemTypes CalculateProblemType( return null; } + + // internal for testing + internal static ProblemDetailsTypes CalculateProblemType( + int statusCode, + EndpointMetadataCollection? metadataCollection, + EndpointMetadataCollection? additionalMetadata) + { + if (statusCode < 400) + { + return ProblemDetailsTypes.None; + } + + ProblemDetailsTypes? statusCodeProblemType = null; + ProblemDetailsTypes? generalProblemType = null; + + void SetProblemType(IProblemDetailsMetadata metadata) + { + if (!metadata.StatusCode.HasValue) + { + generalProblemType = metadata.ProblemType; + } + else if (statusCode == metadata.StatusCode) + { + statusCodeProblemType = metadata.ProblemType; + } + } + + if (metadataCollection?.GetOrderedMetadata() is { Count: > 0 } problemDetailsCollection) + { + for (var i = 0; i < problemDetailsCollection.Count; i++) + { + SetProblemType(problemDetailsCollection[i]); + } + } + + if (additionalMetadata?.GetOrderedMetadata() is { Count: > 0 } additionalProblemDetailsCollection) + { + for (var i = 0; i < additionalProblemDetailsCollection.Count; i++) + { + SetProblemType(additionalProblemDetailsCollection[i]); + } + } + + var problemTypeFromMetadata = statusCodeProblemType ?? generalProblemType ?? ProblemDetailsTypes.None; + var expectedProblemType = statusCode >= 500 ? ProblemDetailsTypes.Server : ProblemDetailsTypes.Client; + + var problemType = problemTypeFromMetadata & expectedProblemType; + return problemType != ProblemDetailsTypes.None ? + problemType : + // We need to special case Routing, since it could generate any status code + problemTypeFromMetadata & ProblemDetailsTypes.Routing; + } } diff --git a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs index 1359042d6ddb..9d063f6e77da 100644 --- a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs +++ b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs @@ -55,7 +55,7 @@ public void CanWrite_IsFalse_ForUnknownStatus(int statusCode) [Theory] [InlineData(400)] [InlineData(499)] - public void CanWrite_IsFalse_ForClientErrors(int statusCode) + public void CanWrite_IsTrue_ForClientErrors(int statusCode) { // Arrange var writer = GetWriter(); @@ -68,7 +68,7 @@ public void CanWrite_IsFalse_ForClientErrors(int statusCode) var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); // Assert - Assert.False(canWrite); + Assert.True(canWrite); } [Theory] @@ -90,20 +90,6 @@ public void CanWrite_IsTrue_ForServerErrors(int statusCode) Assert.True(canWrite); } - [Fact] - public void CanWrite_IsTrue_ForRoutingErrors() - { - // Arrange - var writer = GetWriter(); - var context = new DefaultHttpContext(); - - // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.True(canWrite); - } - [Fact] public async Task WriteAsync_Works() { @@ -259,7 +245,7 @@ public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified() Assert.Equal("Bad Request", problemDetails.Title); } - private DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null) + private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null) { options ??= new ProblemDetailsOptions(); return new DefaultProblemDetailsWriter(Options.Create(options)); diff --git a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs index 3720170676ae..f5efb0f8722c 100644 --- a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs +++ b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Moq; @@ -15,288 +16,322 @@ namespace Microsoft.AspNetCore.Http.Tests; public class ProblemDetailsServiceTest { [Fact] - public void IsEnable_ReturnsFalse_ForRouting_WhenDisable() + public void GetWriter_ReturnsNull_WhenNotEnabled() { - //Arrange + // Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.ClientErrors | ProblemTypes.Exceptions }); + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.None }); + var context = new DefaultHttpContext(); - //Act - var isEnabled = service.IsEnabled(400, isRouting: true); + // Act + var writer = service.GetWriter(context, EndpointMetadataCollection.Empty); - //Assert - Assert.False(isEnabled); + // Assert + Assert.Null(writer); } [Fact] - public void IsEnable_ReturnsTrue_ForRouting_WhenEnabled() + public void GetWriter_ReturnsNull_WhenNotRegisteredWriters() { - //Arrange + // Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.RoutingFailures }); + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); + var context = new DefaultHttpContext(); - //Act - var isEnabled = service.IsEnabled(400, isRouting: true); + // Act + var writer = service.GetWriter(context, EndpointMetadataCollection.Empty); - //Assert - Assert.True(isEnabled); + // Assert + Assert.Null(writer); } - [Theory] - [InlineData(100)] - [InlineData(300)] - [InlineData(400)] - [InlineData(500)] - public void IsEnable_ReturnsFalse_WhenUnspecified(int statuCode) + [Fact] + public void GetWriter_ReturnsNull_WhenNoWriterCanWrite() { - //Arrange + // Arrange + var writers = new List() { + Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == false), + Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == false) + }; var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.Unspecified }); + writers: writers, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); + var context = new DefaultHttpContext(); - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); + // Act + var writer = service.GetWriter(context, EndpointMetadataCollection.Empty); - //Assert - Assert.False(isEnabled); + // Assert + Assert.Null(writer); } - [Theory] - [InlineData(100)] - [InlineData(200)] - [InlineData(300)] - [InlineData(399)] - public void IsEnable_ReturnsFalse_ForSuccessStatus(int statuCode) + [Fact] + public void GetWriter_Returns_ForContextMetadata() { - //Arrange + // Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); + + var context = new DefaultHttpContext(); + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json"}); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); + // Act + var selectedWriter = service.GetWriter(context, EndpointMetadataCollection.Empty); - //Assert - Assert.False(isEnabled); + // Assert + Assert.NotNull(selectedWriter); + Assert.IsType(selectedWriter); } - [Theory] - [InlineData(0)] - [InlineData(99)] - [InlineData(600)] - [InlineData(700)] - public void IsEnable_ReturnsFalse_ForUnknownStatus(int statuCode) + [Fact] + public void GetWriter_Returns_ForSpecifiedMetadata() { - //Arrange + // Arrange var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); + + var context = new DefaultHttpContext() + { + Response = { StatusCode = StatusCodes.Status400BadRequest } + }; + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, EndpointMetadataCollection.Empty, null)); - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); + // Act + var selectedWriter = service.GetWriter(context, additionalMetadata: metadata); - //Assert - Assert.False(isEnabled); + // Assert + Assert.NotNull(selectedWriter); + Assert.IsType(selectedWriter); } - [Theory] - [InlineData(400)] - [InlineData(415)] - [InlineData(422)] - [InlineData(499)] - public void IsEnable_ReturnsTrue_ForClientErrors(int statuCode) + [Fact] + public void GetWriter_Returns_FirstCanWriter() { - //Arrange + // Arrange + var writer1 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == true); + var writer2 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == true); + var writers = new List() { writer1, writer2 }; var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.ClientErrors }); + writers: writers, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); + var context = new DefaultHttpContext(); - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); + // Act + var selectedWriter = service.GetWriter(context, EndpointMetadataCollection.Empty); - //Assert - Assert.True(isEnabled); + // Assert + Assert.NotNull(selectedWriter); + Assert.Equal(writer1, selectedWriter); } [Theory] + [InlineData(100)] + [InlineData(200)] + [InlineData(400)] [InlineData(500)] - [InlineData(599)] - public void IsEnable_ReturnsTrue_ForServerErrors(int statuCode) + public void CalculateProblemType_IsNone_WhenNoMetadata(int statusCode) { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.Exceptions }); - - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); - - //Assert - Assert.True(isEnabled); + // Arrange & Act + var problemType = ProblemDetailsService.CalculateProblemType( + statusCode, + metadataCollection: EndpointMetadataCollection.Empty, + additionalMetadata: EndpointMetadataCollection.Empty); + + // Assert + Assert.Equal(ProblemDetailsTypes.None, problemType); } - [Fact] - public void GetWriter_ReturnsNull_WhenNotEnabled() + [Theory] + [InlineData(100, ProblemDetailsTypes.All)] + [InlineData(200, ProblemDetailsTypes.All)] + [InlineData(400, ProblemDetailsTypes.Server)] + [InlineData(400, ProblemDetailsTypes.None)] + [InlineData(500, ProblemDetailsTypes.Client)] + [InlineData(500, ProblemDetailsTypes.None)] + public void CalculateProblemType_IsNone_WhenNotAllowed(int statusCode, ProblemDetailsTypes metadataProblemType) { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.Unspecified }); - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; + // Arrange & Act + var problemType = ProblemDetailsService.CalculateProblemType( + statusCode, + metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(metadataProblemType)), + additionalMetadata: EndpointMetadataCollection.Empty); + + // Assert + Assert.Equal(ProblemDetailsTypes.None, problemType); + } - //Act - var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); + [Theory] + [InlineData(400)] + [InlineData(500)] + public void CalculateProblemType_CanBeRouting_ForAllStatusCode(int statusCode) + { + // Arrange & Act + var problemType = ProblemDetailsService.CalculateProblemType( + statusCode, + metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(ProblemDetailsTypes.Routing)), + additionalMetadata: EndpointMetadataCollection.Empty); + + // Assert + Assert.Equal(ProblemDetailsTypes.Routing, problemType); + } - //Assert - Assert.Null(writer); + [Theory] + [InlineData(400, ProblemDetailsTypes.Client)] + [InlineData(400, ProblemDetailsTypes.Routing)] + [InlineData(500, ProblemDetailsTypes.Server)] + [InlineData(500, ProblemDetailsTypes.Routing)] + public void CalculateProblemType_IsCorrect_WhenMetadata_WithStatusCode(int statusCode, ProblemDetailsTypes metadataProblemType) + { + // Arrange & Act + var problemType = ProblemDetailsService.CalculateProblemType( + statusCode, + metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, metadataProblemType)), + additionalMetadata: EndpointMetadataCollection.Empty); + + // Assert + Assert.Equal(metadataProblemType, problemType); } [Fact] - public void GetWriter_ReturnsNull_WhenNotRegisteredWriters() + public void CalculateProblemType_PrefersAdditionalMetadata() { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - - //Act - var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); - - //Assert - Assert.Null(writer); + // Arrange & Act + var statusCode = StatusCodes.Status400BadRequest; + var problemType = ProblemDetailsService.CalculateProblemType( + statusCode, + metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, ProblemDetailsTypes.Client)), + additionalMetadata: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, ProblemDetailsTypes.Routing))); + + // Assert + Assert.Equal(ProblemDetailsTypes.Routing, problemType); } [Fact] - public void GetWriter_ReturnsNull_WhenNoWriterCanWrite() + public void CalculateProblemType_PrefersMetadataWithStatusCode() { - //Arrange - var writers = new List() { - Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false), - Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false) - }; - var service = CreateService( - writers: writers, - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - - //Act - var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); - - //Assert - Assert.Null(writer); + // Arrange & Act + var statusCode = StatusCodes.Status400BadRequest; + var problemType = ProblemDetailsService.CalculateProblemType( + statusCode, + metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, ProblemDetailsTypes.Client)), + additionalMetadata: new EndpointMetadataCollection(new TestProblemMetadata(ProblemDetailsTypes.Routing))); + + // Assert + Assert.Equal(ProblemDetailsTypes.Client, problemType); } [Fact] - public void GetWriter_Returns_ForContextMetadata() + public async Task WriteAsync_Call_SelectedWriter() { - //Arrange + // Arrange var service = CreateService( writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.Client | ProblemDetailsTypes.Server }); + var metadata = new EndpointMetadataCollection( + new SampleMetadata() { ContentType = "application/problem+json" }, + new TestProblemMetadata()); + var stream = new MemoryStream(); var context = new DefaultHttpContext() { - Response = { StatusCode = StatusCodes.Status400BadRequest } + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, }; - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json"}); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - //Act - var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); + // Act + await service.WriteAsync(context, additionalMetadata: metadata); - //Assert - Assert.NotNull(selectedWriter); - Assert.IsType(selectedWriter); + // Assert + Assert.Equal("\"Content\"", Encoding.UTF8.GetString(stream.ToArray())); } [Fact] - public void GetWriter_Returns_ForSpecifiedMetadata() + public async Task WriteAsync_Skip_WhenNoWriterRegistered() { - //Arrange + // Arrange var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); - + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); + var stream = new MemoryStream(); var context = new DefaultHttpContext() { - Response = { StatusCode = StatusCodes.Status400BadRequest } + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, }; - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, EndpointMetadataCollection.Empty, null)); - //Act - var selectedWriter = service.GetWriter(context, currentMetadata: metadata, isRouting: false); + // Act + await service.WriteAsync(context); - //Assert - Assert.NotNull(selectedWriter); - Assert.IsType(selectedWriter); + // Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); } [Fact] - public void GetWriter_Returns_FirstCanWriter() + public async Task WriteAsync_Skip_WhenNoWriterSelected() { - //Arrange - var writer1 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); - var writer2 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); - var writers = new List() { writer1, writer2 }; + // Arrange var service = CreateService( - writers: writers, - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); + writers: new List { new MetadataBasedWriter() }, + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); + var stream = new MemoryStream(); var context = new DefaultHttpContext() { - Response = { StatusCode = StatusCodes.Status400BadRequest } + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, }; - //Act - var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); + // Act + await service.WriteAsync(context); - //Assert - Assert.NotNull(selectedWriter); - Assert.Equal(writer1, selectedWriter); + // Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); } [Fact] - public async Task WriteAsync_Call_SelectedWriter() + public async Task WriteAsync_Skip_WhenNotEnabled() { - //Arrange + // Arrange var service = CreateService( writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); - - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.None }); var stream = new MemoryStream(); var context = new DefaultHttpContext() { Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, }; + var metadata = new EndpointMetadataCollection( + new SampleMetadata() { ContentType = "application/problem+json" }, + new TestProblemMetadata(context.Response.StatusCode, ProblemDetailsTypes.All)); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - //Act - await service.WriteAsync(context, currentMetadata: metadata); + // Act + await service.WriteAsync(context); - //Assert - Assert.Equal("\"Content\"", Encoding.UTF8.GetString(stream.ToArray())); + // Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); } [Fact] - public async Task WriteAsync_Skip_WhenNoWriter() + public async Task WriteAsync_Skip_WhenNotAllowed() { - //Arrange + // Arrange var service = CreateService( writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedMapping = ProblemTypes.All }); + options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); var stream = new MemoryStream(); var context = new DefaultHttpContext() { Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, }; + var metadata = new EndpointMetadataCollection( + new SampleMetadata() { ContentType = "application/problem+json" }, + new TestProblemMetadata(context.Response.StatusCode, ProblemDetailsTypes.None)); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - //Act + // Act await service.WriteAsync(context); - //Assert + // Assert Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); } @@ -315,9 +350,11 @@ private class SampleMetadata private class MetadataBasedWriter : IProblemDetailsWriter { - public bool CanWrite(HttpContext context, ProblemTypes problemType) + public bool CanWrite(HttpContext context, EndpointMetadataCollection additionalMetadata) { - return metadata != null && metadata.GetMetadata != null; + var metadata = additionalMetadata?.GetMetadata() ?? + context.GetEndpoint()?.Metadata.GetMetadata(); + return metadata != null; } public Task WriteAsync(HttpContext context, int? statusCode = null, string title = null, string type = null, string detail = null, string instance = null, IDictionary extensions = null) @@ -325,4 +362,27 @@ public Task WriteAsync(HttpContext context, int? statusCode = null, string title return context.Response.WriteAsJsonAsync("Content"); } } + + private class TestProblemMetadata : IProblemDetailsMetadata + { + public TestProblemMetadata() + { + ProblemType = ProblemDetailsTypes.All; + } + + public TestProblemMetadata(ProblemDetailsTypes problemTypes) + { + ProblemType = problemTypes; + } + + public TestProblemMetadata(int status, ProblemDetailsTypes problemTypes) + { + StatusCode = status; + ProblemType = problemTypes; + } + + public int? StatusCode { get;} + + public ProblemDetailsTypes ProblemType { get; } + } } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index 530c41491f28..08274d4463a5 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -27,6 +27,22 @@ public class ExceptionHandlerMiddleware private readonly DiagnosticListener _diagnosticListener; private readonly IProblemDetailsService? _problemDetailsService; + /// + /// Creates a new + /// + /// The representing the next middleware in the pipeline. + /// The used for logging. + /// The options for configuring the middleware. + /// The used for writing diagnostic messages. + public ExceptionHandlerMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions options, + DiagnosticListener diagnosticListener) + : this(next, loggerFactory, options, diagnosticListener, problemDetailsService: null) + { + } + /// /// Creates a new /// @@ -40,7 +56,7 @@ public ExceptionHandlerMiddleware( ILoggerFactory loggerFactory, IOptions options, DiagnosticListener diagnosticListener, - IProblemDetailsService? problemDetailsService = null) + IProblemDetailsService? problemDetailsService) { _next = next; _options = options.Value; @@ -53,8 +69,7 @@ public ExceptionHandlerMiddleware( { if (_options.ExceptionHandlingPath == null) { - if (problemDetailsService == null || - problemDetailsService.IsEnabled(ProblemTypes.Server) == false) + if (problemDetailsService == null) { throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index 583c0a229582..1f77266ffb80 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Formatters; namespace Microsoft.AspNetCore.Mvc.Infrastructure; @@ -28,9 +27,10 @@ public DefaultApiProblemDetailsWriter( _problemDetailsFactory = problemDetailsFactory; } - public bool CanWrite(HttpContext context) + public bool CanWrite(HttpContext context, EndpointMetadataCollection? additionalMetadata) { - var apiControllerAttribute = context.GetEndpoint()?.Metadata.GetMetadata(); + var apiControllerAttribute = additionalMetadata?.GetMetadata() ?? + context.GetEndpoint()?.Metadata.GetMetadata(); return apiControllerAttribute != null; } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index b7c0dc9622b1..4b11c4a3ac26 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -9,26 +9,22 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; - private readonly ProblemDetailsOptions? _options; - private readonly IProblemDetailsService? _problemDetailsService; + private readonly ProblemDetailsOptions _options; public ProblemDetailsClientErrorFactory( ProblemDetailsFactory problemDetailsFactory, - IOptions? options = null, - IProblemDetailsService? problemDetailsService = null) + IOptions options) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); - _options = options?.Value; - _problemDetailsService = problemDetailsService; + _options = options.Value; } public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { var statusCode = clientError.StatusCode ?? 500; - var problemType = statusCode >= 500 ? ProblemTypes.Server : ProblemTypes.Client; + var problemType = statusCode >= 500 ? ProblemDetailsTypes.Server : ProblemDetailsTypes.Client; - if (_problemDetailsService != null && - _problemDetailsService.IsEnabled(problemType) == false) + if ((_options.AllowedProblemTypes & problemType) == ProblemDetailsTypes.None) { return null; } diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs index 41d4e1ab1d15..feacaf0e1eeb 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs @@ -3,6 +3,7 @@ using System.Reflection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -79,7 +80,6 @@ public void OnProvidersExecuting_AppliesConventions() Assert.NotEmpty(actionModel.Filters.OfType()); Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource); Assert.NotEmpty(actionModel.Selectors); - Assert.NotEmpty(actionModel.Selectors[0].EndpointMetadata.OfType()); } [Fact] @@ -122,7 +122,6 @@ public void OnProvidersExecuting_AppliesConventionsForIResult() Assert.NotEmpty(actionModel.Filters.OfType()); Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource); Assert.NotEmpty(actionModel.Selectors); - Assert.NotEmpty(actionModel.Selectors[0].EndpointMetadata.OfType()); } [Fact] diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs index 6b082743ea25..36323ff32368 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; @@ -13,7 +14,25 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels; public class EndpointMetadataConventionTest { [Fact] - public void Apply_DefaultErrorTypeMetadata() + public void Apply_DefaultErrorTypeMetadata_WhenIProblemDetailsServiceRegistered() + { + // Arrange + var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); + var errorType = typeof(ProblemDetails); + var convention = GetConvention(services: CreateServicesWithProblemDetatils(), errorType: errorType); + + //Act + convention.Apply(action); + + // Assert + foreach (var selector in action.Selectors) + { + Assert.Contains(selector.EndpointMetadata, m => m is IProblemDetailsMetadata attribute && attribute.ProblemType == ProblemDetailsTypes.All); + } + } + + [Fact] + public void Apply_SkipDefaultErrorTypeMetadata_WhenIProblemDetailsServiceNotRegistered() { // Arrange var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); @@ -26,7 +45,7 @@ public void Apply_DefaultErrorTypeMetadata() // Assert foreach (var selector in action.Selectors) { - Assert.Contains(selector.EndpointMetadata, m => m is ProducesErrorResponseTypeAttribute attribute && attribute.Type == errorType); + Assert.DoesNotContain(selector.EndpointMetadata, m => m is IProblemDetailsMetadata); } } @@ -44,7 +63,7 @@ public void Apply_SkipDefaultErrorTypeMetadata_WhenVoid() // Assert foreach (var selector in action.Selectors) { - Assert.DoesNotContain(selector.EndpointMetadata, m => m is ProducesErrorResponseTypeAttribute); + Assert.DoesNotContain(selector.EndpointMetadata, m => m is IProblemDetailsMetadata); } } @@ -247,6 +266,13 @@ private static ActionModel GetActionModel( return Assert.Single(controller.Actions, m => m.ActionName == actionName); } + private static IServiceProvider CreateServicesWithProblemDetatils() + { + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of()); + return services.BuildServiceProvider(); + } + private class TestController { public ActionResult ActionWithParameterMetadata(AddsCustomParameterMetadata param1) => null; diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs index 8cfa50c3fbe2..978e9d560d5c 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -21,7 +21,7 @@ public void GetClientError_ReturnsProblemDetails_IfNoMappingWasFound() [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -48,7 +48,7 @@ public void GetClientError_ReturnsProblemDetails() [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -78,7 +78,7 @@ public void GetClientError_UsesActivityId_ToSetTraceId() [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -104,7 +104,7 @@ public void GetClientError_UsesHttpContext_ToSetTraceIdIfActivityIdIsNotSet() [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); // Act var result = factory.GetClientError(GetActionContext(), clientError); diff --git a/src/Shared/ProblemDetails/ProblemMetadata.cs b/src/Shared/ProblemDetails/ProblemMetadata.cs index 84272770e6bd..bd2821ffa9f9 100644 --- a/src/Shared/ProblemDetails/ProblemMetadata.cs +++ b/src/Shared/ProblemDetails/ProblemMetadata.cs @@ -3,14 +3,14 @@ namespace Microsoft.AspNetCore.Http.Metadata; -internal class ProblemMetadata : IProblemMetadata +internal class ProblemMetadata : IProblemDetailsMetadata { - public ProblemMetadata(ProblemTypes problemType = ProblemTypes.All) + public ProblemMetadata(ProblemDetailsTypes problemType = ProblemDetailsTypes.All) { ProblemType = problemType; } public int? StatusCode => null; - public ProblemTypes ProblemType { get; } + public ProblemDetailsTypes ProblemType { get; } } diff --git a/src/Shared/ProblemDetails/RoutingProblemMetadata.cs b/src/Shared/ProblemDetails/RoutingProblemMetadata.cs index 9e0fd12acef0..060c1bb58f4c 100644 --- a/src/Shared/ProblemDetails/RoutingProblemMetadata.cs +++ b/src/Shared/ProblemDetails/RoutingProblemMetadata.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Http; -internal sealed class RoutingProblemMetadata : IProblemMetadata +internal sealed class RoutingProblemMetadata : IProblemDetailsMetadata { public RoutingProblemMetadata(int statusCode = StatusCodes.Status404NotFound) { @@ -14,5 +14,5 @@ public RoutingProblemMetadata(int statusCode = StatusCodes.Status404NotFound) public int? StatusCode { get; } - public ProblemTypes ProblemType => ProblemTypes.Routing; + public ProblemDetailsTypes ProblemType => ProblemDetailsTypes.Routing; } From 6813d583d2480cf61bbbe67239f421a3b43eec06 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jun 2022 13:22:40 -0700 Subject: [PATCH 29/59] API review feedback --- ...Metadata.cs => IProblemDetailsMetadata.cs} | 8 +- .../ProblemDetails/IProblemDetailsService.cs | 17 +- .../ProblemDetails/IProblemDetailsWriter.cs | 31 +--- .../ProblemDetails/ProblemDetailsContext.cs | 37 ++++ .../src/ProblemDetails/ProblemTypes.cs | 12 +- .../src/PublicAPI.Unshipped.txt | 26 ++- .../Http/src/Builder/ApplicationBuilder.cs | 1 - .../src/DefaultProblemDetailsWriter.cs | 41 +---- .../ProblemDetailsOptions.cs | 10 +- ...oblemDetailsServiceCollectionExtensions.cs | 2 +- .../src/ProblemDetailsService.cs | 49 ++--- .../src/PublicAPI.Unshipped.txt | 2 +- .../test/DefaultProblemDetailsWriterTest.cs | 167 +++++------------- .../test/ProblemDetailsServiceTest.cs | 163 ++++------------- .../Routing/src/EndpointRoutingMiddleware.cs | 9 - .../src/Matching/HttpMethodMatcherPolicy.cs | 1 - .../DeveloperExceptionPageExtensions.cs | 25 ++- .../DeveloperExceptionPageMiddleware.cs | 53 ++++-- .../ExceptionHandlerExtensions.cs | 13 +- .../ExceptionHandlerMiddleware.cs | 9 +- .../Microsoft.AspNetCore.Diagnostics.csproj | 1 + .../Diagnostics/src/PublicAPI.Unshipped.txt | 6 +- .../StatusCodePage/StatusCodePagesOptions.cs | 14 +- .../src/Formatters/TextOutputFormatter.cs | 7 +- .../DefaultApiProblemDetailsWriter.cs | 47 ++--- .../Infrastructure/ObjectResultExecutor.cs | 7 +- .../EndpointMetadataConventionTest.cs | 2 +- src/Shared/ProblemDetails/ProblemMetadata.cs | 5 +- 28 files changed, 310 insertions(+), 455 deletions(-) rename src/Http/Http.Abstractions/src/Metadata/{IProblemMetadata.cs => IProblemDetailsMetadata.cs} (59%) create mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs diff --git a/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs similarity index 59% rename from src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs rename to src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs index 22fa2615dc4a..4d47cb4bf9de 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IProblemMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs @@ -4,17 +4,19 @@ namespace Microsoft.AspNetCore.Http.Metadata; /// -/// +/// Defines a contract used to specify metadata,in , +/// for configure generation. /// public interface IProblemDetailsMetadata { /// - /// + /// Gets the HTTP status code of the response. /// public int? StatusCode { get; } /// - /// + /// Gets the Problem Details Types + /// associated to the . /// public ProblemDetailsTypes ProblemType { get; } } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs index 5624acc505aa..7a5be8ec1304 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs @@ -4,17 +4,14 @@ namespace Microsoft.AspNetCore.Http; /// -/// +/// Defines a type that provide functionality to +/// create a response. /// public interface IProblemDetailsService { - Task WriteAsync( - HttpContext context, - EndpointMetadataCollection? additionalMetadata = null, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null); + /// + /// Write a response to the current context + /// + /// The associated with the current request/response. + ValueTask WriteAsync(ProblemDetailsContext context); } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index 32f2be6bfa75..2b1cd2181b76 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -4,34 +4,15 @@ namespace Microsoft.AspNetCore.Http; /// -/// +/// Defines a type that write a +/// payload to the current . /// public interface IProblemDetailsWriter { /// - /// + /// Write a response to the current context /// - /// - /// - bool CanWrite(HttpContext context, EndpointMetadataCollection? additionalMetadata); - - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - Task WriteAsync( - HttpContext context, - int? statusCode, - string? title, - string? type, - string? detail, - string? instance, - IDictionary? extensions); + /// The associated with the current request/response. + /// Flag that indicates if the response was started. + ValueTask WriteAsync(ProblemDetailsContext context); } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs new file mode 100644 index 000000000000..1624980190ab --- /dev/null +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -0,0 +1,37 @@ +// 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.Mvc; + +namespace Microsoft.AspNetCore.Http; + +/// +/// Represent the current problem detatils context for the request. +/// +public class ProblemDetailsContext +{ + /// + /// Creates a new instance of the . + /// + /// The associated with the current request being processed by the filter. + public ProblemDetailsContext(HttpContext httpContext) + { + HttpContext = httpContext; + } + + /// + /// The associated with the current request being processed by the filter. + /// + public HttpContext HttpContext { get; } + + /// + /// A collection of additional arbitrary metadata associated with the current request endpoint. + /// + public EndpointMetadataCollection? AdditionalMetadata { get; init; } + + /// + /// A instance of that will be + /// used during the response payload generation. + /// + public ProblemDetails? ProblemDetails { get; init; } +} diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs index 4290bb39facc..dbe2e8925478 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs @@ -4,33 +4,33 @@ namespace Microsoft.AspNetCore.Http; /// -/// +/// Represents the possible problem details types. /// [Flags] public enum ProblemDetailsTypes : uint { /// - /// + /// Specifies no types. /// None = 0, /// - /// + /// HTTP Status code 5xx /// Server = 1, /// - /// 404 / 405 / 415 + /// Failures occurred during the routing system processing. /// Routing = 2, /// - /// + /// HTTP Status code 4xx /// Client = 4, /// - /// + /// Specifies all types. /// All = Routing | Server | Client, } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 2d46376c48f1..b0b52deb1374 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -14,19 +14,33 @@ Microsoft.AspNetCore.Http.HttpValidationProblemDetails Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void -Microsoft.AspNetCore.Http.IProblemDetailsService -Microsoft.AspNetCore.Http.IProblemDetailsService.IsEnabled(int statusCode, bool isRouting = false) -> bool -Microsoft.AspNetCore.Http.IProblemDetailsService.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? currentMetadata = null, bool isRouting = false, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.IProblemDetailsWriter -Microsoft.AspNetCore.Http.IProblemDetailsWriter.CanWrite(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, bool isRouting) -> bool -Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.HttpContext! context, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null, System.Collections.Generic.IDictionary? extensions = null) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Http.IBindableFromHttpContext Microsoft.AspNetCore.Http.IBindableFromHttpContext.BindAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Reflection.ParameterInfo! parameter) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Http.IProblemDetailsService +Microsoft.AspNetCore.Http.IProblemDetailsService.WriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Http.IProblemDetailsWriter +Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? +Microsoft.AspNetCore.Http.Metadata.IProblemDetailsMetadata +Microsoft.AspNetCore.Http.Metadata.IProblemDetailsMetadata.ProblemType.get -> Microsoft.AspNetCore.Http.ProblemDetailsTypes +Microsoft.AspNetCore.Http.Metadata.IProblemDetailsMetadata.StatusCode.get -> int? Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long? +Microsoft.AspNetCore.Http.ProblemDetailsContext +Microsoft.AspNetCore.Http.ProblemDetailsContext.AdditionalMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection? +Microsoft.AspNetCore.Http.ProblemDetailsContext.AdditionalMetadata.init -> void +Microsoft.AspNetCore.Http.ProblemDetailsContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! +Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.get -> Microsoft.AspNetCore.Mvc.ProblemDetails? +Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.init -> void +Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetailsContext(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> void +Microsoft.AspNetCore.Http.ProblemDetailsTypes +Microsoft.AspNetCore.Http.ProblemDetailsTypes.All = Microsoft.AspNetCore.Http.ProblemDetailsTypes.Server | Microsoft.AspNetCore.Http.ProblemDetailsTypes.Routing | Microsoft.AspNetCore.Http.ProblemDetailsTypes.Client -> Microsoft.AspNetCore.Http.ProblemDetailsTypes +Microsoft.AspNetCore.Http.ProblemDetailsTypes.Client = 4 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes +Microsoft.AspNetCore.Http.ProblemDetailsTypes.None = 0 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes +Microsoft.AspNetCore.Http.ProblemDetailsTypes.Routing = 2 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes +Microsoft.AspNetCore.Http.ProblemDetailsTypes.Server = 1 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes Microsoft.AspNetCore.Http.RouteHandlerContext Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> System.Collections.Generic.IList! diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index 8a0265c93bc6..700670084818 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Builder; diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index c2fc041ec778..39de738ee4e5 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs @@ -1,7 +1,6 @@ // 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.Serialization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -16,42 +15,12 @@ public DefaultProblemDetailsWriter(IOptions options) _options = options.Value; } - public bool CanWrite(HttpContext context, EndpointMetadataCollection? additionalMetadata) - => context.Response.StatusCode >= 400 && context.Response.StatusCode <= 599; - - public Task WriteAsync( - HttpContext context, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null) + public async ValueTask WriteAsync(ProblemDetailsContext context) { - var problemDetails = new ProblemDetails - { - Status = statusCode, - Title = title, - Type = type, - Detail = detail, - Instance = instance - }; - - if (extensions is not null) - { - foreach (var extension in extensions) - { - problemDetails.Extensions[extension.Key] = extension.Value; - } - } + var problemResult = TypedResults.Problem(context.ProblemDetails ?? new ProblemDetails()); + _options.ConfigureDetails?.Invoke(context.HttpContext, problemResult.ProblemDetails); - ProblemDetailsDefaults.Apply(problemDetails, context.Response.StatusCode); - _options.ConfigureDetails?.Invoke(context, problemDetails); - - return context.Response.WriteAsJsonAsync(problemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); + await problemResult.ExecuteAsync(context.HttpContext); + return context.HttpContext.Response.HasStarted; } - - [JsonSerializable(typeof(ProblemDetails))] - internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext - { } } diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs index 164c0cbaeaf4..0e52cd130668 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs @@ -6,17 +6,21 @@ namespace Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; /// -/// +/// Options for controlling the behavior of +/// and similar methods. /// public class ProblemDetailsOptions { /// - /// + /// Controls the ProblemDetails types allowed when auto-generating the response payload. /// + /// + /// Defaults to . + /// public ProblemDetailsTypes AllowedProblemTypes { get; set; } = ProblemDetailsTypes.All; /// - /// + /// The operation that configures the current instance. /// public Action? ConfigureDetails { get; set; } } diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs index 1f16d5d136dc..5675b31208dd 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddProblemDetails(this IServiceCollection servi /// Adds services required for creation of for failed requests. /// /// The to add the services to. - /// The routing options to configure the middleware with. + /// The to configure the services with. /// The so that additional calls can be chained. public static IServiceCollection AddProblemDetails( this IServiceCollection services, diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs index 6ae3da95d558..0e53faac59da 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -20,50 +20,25 @@ public ProblemDetailsService( _options = options.Value; } - public Task WriteAsync( - HttpContext context, - EndpointMetadataCollection? additionalMetadata = null, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null) + public async ValueTask WriteAsync(ProblemDetailsContext context) { if (_options.AllowedProblemTypes != ProblemDetailsTypes.None && _writers is { Length: > 0 }) { - var problemStatusCode = statusCode ?? context.Response.StatusCode; - var calculatedProblemType = CalculateProblemType(problemStatusCode, context.GetEndpoint()?.Metadata, additionalMetadata); + var problemStatusCode = context.ProblemDetails?.Status ?? context.HttpContext.Response.StatusCode; + var calculatedProblemType = CalculateProblemType(problemStatusCode, context.HttpContext.GetEndpoint()?.Metadata, context.AdditionalMetadata); - if ((_options.AllowedProblemTypes & calculatedProblemType) != ProblemDetailsTypes.None && - GetWriter(context, additionalMetadata) is { } writer) + if ((_options.AllowedProblemTypes & calculatedProblemType) != ProblemDetailsTypes.None) { - return writer.WriteAsync( - context, - statusCode, - title, - type, - detail, - instance, - extensions); + var counter = 0; + var responseHasStarted = context.HttpContext.Response.HasStarted; + + while (counter < _writers.Length && !responseHasStarted) + { + responseHasStarted = await _writers[counter].WriteAsync(context); + counter++; + } } } - - return Task.CompletedTask; - } - - // Internal for testing - internal IProblemDetailsWriter? GetWriter(HttpContext context, EndpointMetadataCollection? additionalMetadata) - { - for (var i = 0; i < _writers.Length; i++) - { - if (_writers[i].CanWrite(context, additionalMetadata)) - { - return _writers[i]; - } - } - - return null; } // internal for testing diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt index bc1556eafd47..a518e81233d5 100644 --- a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt +++ b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt @@ -1,6 +1,6 @@ #nullable enable Microsoft.AspNetCore.Http.ProblemDetailsOptions -Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedProblemTypes.get -> Microsoft.AspNetCore.Http.ProblemTypes +Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedProblemTypes.get -> Microsoft.AspNetCore.Http.ProblemDetailsTypes Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedProblemTypes.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.get -> System.Action? Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.set -> void diff --git a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs index 9d063f6e77da..e67de3066d4a 100644 --- a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs +++ b/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs @@ -1,105 +1,24 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Tests; - -using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +namespace Microsoft.AspNetCore.Http.Tests; + public class DefaultProblemDetailsWriterTest { - [Theory] - [InlineData(100)] - [InlineData(200)] - [InlineData(300)] - [InlineData(399)] - public void CanWrite_IsFalse_ForSuccessStatus(int statusCode) - { - // Arrange - var writer = GetWriter(); - var context = new DefaultHttpContext() - { - Response = { StatusCode = statusCode }, - }; - - // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.False(canWrite); - } - - [Theory] - [InlineData(0)] - [InlineData(99)] - [InlineData(600)] - [InlineData(700)] - public void CanWrite_IsFalse_ForUnknownStatus(int statusCode) - { - // Arrange - var writer = GetWriter(); - var context = new DefaultHttpContext() - { - Response = { StatusCode = statusCode }, - }; - - // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.False(canWrite); - } - - [Theory] - [InlineData(400)] - [InlineData(499)] - public void CanWrite_IsTrue_ForClientErrors(int statusCode) - { - // Arrange - var writer = GetWriter(); - var context = new DefaultHttpContext() - { - Response = { StatusCode = statusCode }, - }; - - // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.True(canWrite); - } - - [Theory] - [InlineData(500)] - [InlineData(599)] - public void CanWrite_IsTrue_ForServerErrors(int statusCode) - { - // Arrange - var writer = GetWriter(); - var context = new DefaultHttpContext() - { - Response = { StatusCode = statusCode }, - }; - - // Act - var canWrite = writer.CanWrite(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.True(canWrite); - } - [Fact] public async Task WriteAsync_Works() { // Arrange var writer = GetWriter(); var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream }, - }; + var context = CreateContext(stream); var expectedProblem = new ProblemDetails() { Detail = "Custom Bad Request", @@ -108,15 +27,13 @@ public async Task WriteAsync_Works() Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1-custom", Title = "Custom Bad Request", }; + var problemDetailsContext = new ProblemDetailsContext(context) + { + ProblemDetails = expectedProblem + }; //Act - await writer.WriteAsync( - context, - statusCode: expectedProblem.Status, - title: expectedProblem.Title, - type: expectedProblem.Type, - detail: expectedProblem.Detail, - instance: expectedProblem.Instance); + await writer.WriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -135,19 +52,18 @@ public async Task WriteAsync_AddExtensions() // Arrange var writer = GetWriter(); var stream = new MemoryStream(); - var context = new DefaultHttpContext() + var context = CreateContext(stream); + var expectedProblem = new ProblemDetails(); + expectedProblem.Extensions["Extension1"] = "Extension1-Value"; + expectedProblem.Extensions["Extension2"] = "Extension2-Value"; + + var problemDetailsContext = new ProblemDetailsContext(context) { - Response = { Body = stream }, + ProblemDetails = expectedProblem }; //Act - await writer.WriteAsync( - context, - extensions: new Dictionary - { - ["Extension1"] = "Extension1-Value", - ["Extension2"] = "Extension2-Value", - }); + await writer.WriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -172,13 +88,10 @@ public async Task WriteAsync_Applies_Defaults() // Arrange var writer = GetWriter(); var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status500InternalServerError }, - }; + var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.WriteAsync(context); + await writer.WriteAsync(new ProblemDetailsContext(context)); //Assert stream.Position = 0; @@ -204,13 +117,13 @@ public async Task WriteAsync_Applies_CustomConfiguration() }; var writer = GetWriter(options); var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status500InternalServerError }, - }; + var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.WriteAsync(context, statusCode: StatusCodes.Status400BadRequest); + await writer.WriteAsync(new ProblemDetailsContext(context) + { + ProblemDetails = new() { Status = StatusCodes.Status400BadRequest } + }); //Assert stream.Position = 0; @@ -228,13 +141,13 @@ public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified() // Arrange var writer = GetWriter(); var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status500InternalServerError }, - }; + var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.WriteAsync(context, statusCode: StatusCodes.Status400BadRequest); + await writer.WriteAsync(new ProblemDetailsContext(context) + { + ProblemDetails = new() { Status = StatusCodes.Status400BadRequest } + }); //Assert stream.Position = 0; @@ -245,6 +158,24 @@ public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified() Assert.Equal("Bad Request", problemDetails.Title); } + private HttpContext CreateContext(Stream body, int statusCode = StatusCodes.Status400BadRequest) + { + return new DefaultHttpContext() + { + Response = { Body = body, StatusCode = statusCode }, + RequestServices = CreateServices(), + }; + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddTransient(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddSingleton(NullLoggerFactory.Instance); + + return services.BuildServiceProvider(); + } + private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null) { options ??= new ProblemDetailsOptions(); diff --git a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs index f5efb0f8722c..d62e45926da8 100644 --- a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs +++ b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs @@ -1,133 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using Moq; namespace Microsoft.AspNetCore.Http.Tests; public class ProblemDetailsServiceTest { - [Fact] - public void GetWriter_ReturnsNull_WhenNotEnabled() - { - // Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.None }); - var context = new DefaultHttpContext(); - - // Act - var writer = service.GetWriter(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.Null(writer); - } - - [Fact] - public void GetWriter_ReturnsNull_WhenNotRegisteredWriters() - { - // Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - var context = new DefaultHttpContext(); - - // Act - var writer = service.GetWriter(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.Null(writer); - } - - [Fact] - public void GetWriter_ReturnsNull_WhenNoWriterCanWrite() - { - // Arrange - var writers = new List() { - Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == false), - Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == false) - }; - var service = CreateService( - writers: writers, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - var context = new DefaultHttpContext(); - - // Act - var writer = service.GetWriter(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.Null(writer); - } - - [Fact] - public void GetWriter_Returns_ForContextMetadata() - { - // Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - - var context = new DefaultHttpContext(); - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json"}); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - - // Act - var selectedWriter = service.GetWriter(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.NotNull(selectedWriter); - Assert.IsType(selectedWriter); - } - - [Fact] - public void GetWriter_Returns_ForSpecifiedMetadata() - { - // Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, EndpointMetadataCollection.Empty, null)); - - // Act - var selectedWriter = service.GetWriter(context, additionalMetadata: metadata); - - // Assert - Assert.NotNull(selectedWriter); - Assert.IsType(selectedWriter); - } - - [Fact] - public void GetWriter_Returns_FirstCanWriter() - { - // Arrange - var writer1 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == true); - var writer2 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny()) == true); - var writers = new List() { writer1, writer2 }; - var service = CreateService( - writers: writers, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - var context = new DefaultHttpContext(); - - // Act - var selectedWriter = service.GetWriter(context, EndpointMetadataCollection.Empty); - - // Assert - Assert.NotNull(selectedWriter); - Assert.Equal(writer1, selectedWriter); - } - [Theory] [InlineData(100)] [InlineData(200)] @@ -225,11 +106,16 @@ public void CalculateProblemType_PrefersMetadataWithStatusCode() } [Fact] - public async Task WriteAsync_Call_SelectedWriter() + public async Task WriteAsync_Skip_NextWriters_WhenResponseAlreadyStarted() { // Arrange var service = CreateService( - writers: new List { new MetadataBasedWriter() }, + writers: new List + { + new MetadataBasedWriter("FirstWriter", canWrite: false), + new MetadataBasedWriter("SecondWriter"), + new MetadataBasedWriter("FirstWriter"), + }, options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.Client | ProblemDetailsTypes.Server }); var metadata = new EndpointMetadataCollection( @@ -242,10 +128,10 @@ public async Task WriteAsync_Call_SelectedWriter() }; // Act - await service.WriteAsync(context, additionalMetadata: metadata); + await service.WriteAsync(new ProblemDetailsContext(context) { AdditionalMetadata = metadata }); // Assert - Assert.Equal("\"Content\"", Encoding.UTF8.GetString(stream.ToArray())); + Assert.Equal("\"SecondWriter\"", Encoding.UTF8.GetString(stream.ToArray())); } [Fact] @@ -261,7 +147,7 @@ public async Task WriteAsync_Skip_WhenNoWriterRegistered() }; // Act - await service.WriteAsync(context); + await service.WriteAsync(new ProblemDetailsContext(context)); // Assert Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); @@ -281,7 +167,7 @@ public async Task WriteAsync_Skip_WhenNoWriterSelected() }; // Act - await service.WriteAsync(context); + await service.WriteAsync(new ProblemDetailsContext(context)); // Assert Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); @@ -305,7 +191,7 @@ public async Task WriteAsync_Skip_WhenNotEnabled() context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); // Act - await service.WriteAsync(context); + await service.WriteAsync(new ProblemDetailsContext(context)); // Assert Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); @@ -329,7 +215,7 @@ public async Task WriteAsync_Skip_WhenNotAllowed() context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); // Act - await service.WriteAsync(context); + await service.WriteAsync(new ProblemDetailsContext(context)); // Assert Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); @@ -350,16 +236,27 @@ private class SampleMetadata private class MetadataBasedWriter : IProblemDetailsWriter { - public bool CanWrite(HttpContext context, EndpointMetadataCollection additionalMetadata) + private readonly string _content; + private readonly bool _canWrite; + + public MetadataBasedWriter(string content = "Content", bool canWrite = true) { - var metadata = additionalMetadata?.GetMetadata() ?? - context.GetEndpoint()?.Metadata.GetMetadata(); - return metadata != null; + _content = content; + _canWrite = canWrite; } - public Task WriteAsync(HttpContext context, int? statusCode = null, string title = null, string type = null, string detail = null, string instance = null, IDictionary extensions = null) + public async ValueTask WriteAsync(ProblemDetailsContext context) { - return context.Response.WriteAsJsonAsync("Content"); + var metadata = context.AdditionalMetadata?.GetMetadata() ?? + context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + if (metadata != null && _canWrite) + { + await context.HttpContext.Response.WriteAsJsonAsync(_content); + return true; + } + + return false; } } diff --git a/src/Http/Routing/src/EndpointRoutingMiddleware.cs b/src/Http/Routing/src/EndpointRoutingMiddleware.cs index fa94e6d0258b..fe336e869c95 100644 --- a/src/Http/Routing/src/EndpointRoutingMiddleware.cs +++ b/src/Http/Routing/src/EndpointRoutingMiddleware.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Logging; @@ -13,7 +12,6 @@ namespace Microsoft.AspNetCore.Routing; internal sealed partial class EndpointRoutingMiddleware { - internal const string Http404EndpointDisplayName = "404 HTTP Not Found"; private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched"; private readonly MatcherFactory _matcherFactory; @@ -97,13 +95,6 @@ private Task SetRoutingAndContinue(HttpContext httpContext) if (endpoint == null) { Log.MatchFailure(_logger); - httpContext.SetEndpoint(new Endpoint(context => - { - context.Response.StatusCode = StatusCodes.Status404NotFound; - return Task.CompletedTask; - }, - new EndpointMetadataCollection(new RoutingProblemMetadata(StatusCodes.Status404NotFound)), - Http404EndpointDisplayName)); } else { diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 209d4caa6054..cfe9456ea33f 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs index b8d748b7c729..bd77359b3112 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder; @@ -26,7 +28,7 @@ public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBui throw new ArgumentNullException(nameof(app)); } - return app.UseMiddleware(); + return SetMiddleware(app); } /// @@ -52,6 +54,25 @@ public static IApplicationBuilder UseDeveloperExceptionPage( throw new ArgumentNullException(nameof(options)); } - return app.UseMiddleware(Options.Create(options)); + return SetMiddleware(app, options); + } + + private static IApplicationBuilder SetMiddleware( + IApplicationBuilder app, + DeveloperExceptionPageOptions? options = null) + { + var problemDetailsService = app.ApplicationServices.GetService(); + + if (options is null) + { + return problemDetailsService is null ? + app.UseMiddleware() : + app.UseMiddleware(problemDetailsService); + } + + return problemDetailsService is null ? + app.UseMiddleware(Options.Create(options)) : + app.UseMiddleware(Options.Create(options), problemDetailsService); + } } diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 3cbc26d39e1e..7be3941fb1f8 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -35,6 +35,26 @@ public class DeveloperExceptionPageMiddleware private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); private readonly IProblemDetailsService? _problemDetailsService; + /// + /// Initializes a new instance of the class + /// + /// The representing the next middleware in the pipeline. + /// The options for configuring the middleware. + /// The used for logging. + /// + /// The used for writing diagnostic messages. + /// The list of registered . + public DeveloperExceptionPageMiddleware( + RequestDelegate next, + IOptions options, + ILoggerFactory loggerFactory, + IWebHostEnvironment hostingEnvironment, + DiagnosticSource diagnosticSource, + IEnumerable filters) + : this(next, options, loggerFactory, hostingEnvironment, diagnosticSource, filters, problemDetailsService: null) + { + } + /// /// Initializes a new instance of the class /// @@ -52,7 +72,7 @@ public DeveloperExceptionPageMiddleware( IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable filters, - IProblemDetailsService? problemDetailsService = null) + IProblemDetailsService? problemDetailsService) { if (next == null) { @@ -171,25 +191,24 @@ private async Task DisplayExceptionContent(ErrorContext errorContext) if (_problemDetailsService != null) { - var exceptionExtensions = new Dictionary + var problemDetails = new ProblemDetails { - { - "exception", - new - { - Headers = httpContext.Request.Headers, - Error = errorContext.Exception.ToString(), - Path = httpContext.Request.Path, - Endpoint = httpContext.GetEndpoint()?.ToString(), - RouteValues = httpContext.Features.Get()?.RouteValues, - } - } + Status = httpContext.Response.StatusCode + }; + + problemDetails.Extensions["exception"] = new + { + Headers = httpContext.Request.Headers, + Error = errorContext.Exception.ToString(), + Path = httpContext.Request.Path, + Endpoint = httpContext.GetEndpoint()?.ToString(), + RouteValues = httpContext.Features.Get()?.RouteValues, }; - await _problemDetailsService.WriteAsync( - httpContext, - extensions: exceptionExtensions, - statusCode: httpContext.Response.StatusCode); + await _problemDetailsService.WriteAsync(new ProblemDetailsContext(httpContext) + { + ProblemDetails = problemDetails + }); } // If the response has not started, assume the problem details was not written. diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index 7c518aeb6723..91d05a774e2f 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -104,6 +104,8 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBuilder app, IOptions? options) { const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + var problemDetailsService = app.ApplicationServices.GetService(); + // Only use this path if there's a global router (in the 'WebApplication' case). if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) { @@ -131,16 +133,19 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui options.Value.ExceptionHandler = builder.Build(); } - var problemDetailsProvider = app.ApplicationServices.GetService(); - return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsProvider).Invoke; + return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsService).Invoke; }); } if (options is null) { - return app.UseMiddleware(); + return problemDetailsService is null ? + app.UseMiddleware() : + app.UseMiddleware(problemDetailsService); } - return app.UseMiddleware(options); + return problemDetailsService is null ? + app.UseMiddleware(options) : + app.UseMiddleware(options, problemDetailsService); } } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index 08274d4463a5..cf390982786c 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -164,10 +164,11 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed } else { - await _problemDetailsService!.WriteAsync( - context, - exceptionHandlerFeature.Endpoint?.Metadata, - statusCode: DefaultStatusCode); + await _problemDetailsService!.WriteAsync(new ProblemDetailsContext(context) + { + AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata, + ProblemDetails = new ProblemDetails() { Status = DefaultStatusCode } + }); } // If the response has already started, assume exception handler was successful. diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index ebf506b0a0ae..150107b9589b 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index ee35e348121d..c41c78796a7b 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,8 +1,6 @@ #nullable enable -*REMOVED*Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters) -> void -*REMOVED*Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener) -> void -Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService = null) -> void -Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService = null) -> void +Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService) -> void +Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs index 0640e81f2a69..5eb8c16b38e9 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs @@ -4,6 +4,8 @@ using System.Globalization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -14,6 +16,8 @@ namespace Microsoft.AspNetCore.Builder; /// public class StatusCodePagesOptions { + private static readonly ProblemMetadata NotFoundProblemMetadata = new(StatusCodes.Status404NotFound, ProblemDetailsTypes.Routing | ProblemDetailsTypes.Client); + /// /// Creates a default which produces a plaintext response /// containing the status code and the reason phrase. @@ -22,16 +26,18 @@ public StatusCodePagesOptions() { HandleAsync = async context => { - // TODO: Render with a pre-compiled html razor view. var statusCode = context.HttpContext.Response.StatusCode; if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - await problemDetailsService.WriteAsync( - context.HttpContext, - statusCode: statusCode); + await problemDetailsService.WriteAsync(new ProblemDetailsContext(context.HttpContext) + { + AdditionalMetadata = new EndpointMetadataCollection(NotFoundProblemMetadata), + ProblemDetails = new ProblemDetails() { Status = statusCode } + }); } + // TODO: Render with a pre-compiled html razor view. if (!context.HttpContext.Response.HasStarted) { var body = BuildResponseBody(statusCode); diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index 85ca15690180..81ebcbfd062c 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -138,9 +138,10 @@ public override Task WriteAsync(OutputFormatterWriteContext context) if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsService.WriteAsync( - context.HttpContext, - statusCode: statusCode); + return problemDetailsService.WriteAsync(new ProblemDetailsContext(context.HttpContext) + { + ProblemDetails = new ProblemDetails { Status = statusCode } + }).AsTask(); } return Task.CompletedTask; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index 1f77266ffb80..f926606850a1 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -27,34 +27,36 @@ public DefaultApiProblemDetailsWriter( _problemDetailsFactory = problemDetailsFactory; } - public bool CanWrite(HttpContext context, EndpointMetadataCollection? additionalMetadata) + public async ValueTask WriteAsync(ProblemDetailsContext context) { - var apiControllerAttribute = additionalMetadata?.GetMetadata() ?? - context.GetEndpoint()?.Metadata.GetMetadata(); - return apiControllerAttribute != null; - } + var apiControllerAttribute = context.AdditionalMetadata?.GetMetadata() ?? + context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - public Task WriteAsync( - HttpContext context, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null, - IDictionary? extensions = null) - { - var problemDetails = _problemDetailsFactory.CreateProblemDetails(context, statusCode ?? context.Response.StatusCode, title, type, detail, instance); + if (apiControllerAttribute is null) + { + return false; + } + + // Recreating the problem details to get all customizations + // from the factory + var problemDetails = _problemDetailsFactory.CreateProblemDetails( + context.HttpContext, + context.ProblemDetails?.Status ?? context.HttpContext.Response.StatusCode, + context.ProblemDetails?.Title, + context.ProblemDetails?.Type, + context.ProblemDetails?.Detail, + context.ProblemDetails?.Instance); - if (extensions is not null) + if (context.ProblemDetails?.Extensions is not null) { - foreach (var extension in extensions) + foreach (var extension in context.ProblemDetails.Extensions) { problemDetails.Extensions[extension.Key] = extension.Value; } } var formatterContext = new OutputFormatterWriteContext( - context, + context.HttpContext, _writerFactory.CreateWriter, typeof(ProblemDetails), problemDetails); @@ -66,10 +68,13 @@ public Task WriteAsync( if (selectedFormatter == null) { - return Results.Problem(problemDetails) - .ExecuteAsync(context); + await Results.Problem(problemDetails) + .ExecuteAsync(context.HttpContext); + + return context.HttpContext.Response.HasStarted; } - return selectedFormatter.WriteAsync(formatterContext); + await selectedFormatter.WriteAsync(formatterContext); + return context.HttpContext.Response.HasStarted; } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 70600aea056b..9abdedb154af 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -121,9 +121,10 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsService.WriteAsync( - context.HttpContext, - statusCode: statusCode); + return problemDetailsService.WriteAsync(new ProblemDetailsContext(context.HttpContext) + { + ProblemDetails = new ProblemDetails { Status = statusCode } + }).AsTask(); } return Task.CompletedTask; diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs index 36323ff32368..71590f349cae 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs @@ -16,7 +16,7 @@ public class EndpointMetadataConventionTest [Fact] public void Apply_DefaultErrorTypeMetadata_WhenIProblemDetailsServiceRegistered() { - // Arrange + // Arrange var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); var errorType = typeof(ProblemDetails); var convention = GetConvention(services: CreateServicesWithProblemDetatils(), errorType: errorType); diff --git a/src/Shared/ProblemDetails/ProblemMetadata.cs b/src/Shared/ProblemDetails/ProblemMetadata.cs index bd2821ffa9f9..301893fa6a97 100644 --- a/src/Shared/ProblemDetails/ProblemMetadata.cs +++ b/src/Shared/ProblemDetails/ProblemMetadata.cs @@ -5,12 +5,13 @@ namespace Microsoft.AspNetCore.Http.Metadata; internal class ProblemMetadata : IProblemDetailsMetadata { - public ProblemMetadata(ProblemDetailsTypes problemType = ProblemDetailsTypes.All) + public ProblemMetadata(int? statusCode = null, ProblemDetailsTypes problemType = ProblemDetailsTypes.All) { ProblemType = problemType; + StatusCode = statusCode; } - public int? StatusCode => null; + public int? StatusCode { get; } public ProblemDetailsTypes ProblemType { get; } } From 6b15f58b3ba770f4cd6e8604753ff7806faab52c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jun 2022 13:31:19 -0700 Subject: [PATCH 30/59] Clean up --- src/Http/ProblemDetails/test/hcmd2eox.wbd~ | 328 --------------------- src/Http/Routing/src/EndpointMiddleware.cs | 5 +- 2 files changed, 1 insertion(+), 332 deletions(-) delete mode 100644 src/Http/ProblemDetails/test/hcmd2eox.wbd~ diff --git a/src/Http/ProblemDetails/test/hcmd2eox.wbd~ b/src/Http/ProblemDetails/test/hcmd2eox.wbd~ deleted file mode 100644 index bf94586fbf00..000000000000 --- a/src/Http/ProblemDetails/test/hcmd2eox.wbd~ +++ /dev/null @@ -1,328 +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; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Moq; - -namespace Microsoft.AspNetCore.Http.Tests; - -public class ProblemDetailsServiceTest -{ - [Fact] - public void IsEnable_ReturnsFalse_ForRouting_WhenDisable() - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.ClientErrors | ProblemTypes.Exceptions }); - - //Act - var isEnabled = service.IsEnabled(400, isRouting: true); - - //Assert - Assert.False(isEnabled); - } - - [Fact] - public void IsEnable_ReturnsTrue_ForRouting_WhenEnabled() - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.RoutingFailures }); - - //Act - var isEnabled = service.IsEnabled(400, isRouting: true); - - //Assert - Assert.True(isEnabled); - } - - [Theory] - [InlineData(100)] - [InlineData(300)] - [InlineData(400)] - [InlineData(500)] - public void IsEnable_ReturnsFalse_WhenUnspecified(int statuCode) - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.Unspecified }); - - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); - - //Assert - Assert.False(isEnabled); - } - - [Theory] - [InlineData(100)] - [InlineData(200)] - [InlineData(300)] - [InlineData(399)] - public void IsEnable_ReturnsFalse_ForSuccessStatus(int statuCode) - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); - - //Assert - Assert.False(isEnabled); - } - - [Theory] - [InlineData(0)] - [InlineData(99)] - [InlineData(600)] - [InlineData(700)] - public void IsEnable_ReturnsFalse_ForUnknownStatus(int statuCode) - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); - - //Assert - Assert.False(isEnabled); - } - - [Theory] - [InlineData(400)] - [InlineData(415)] - [InlineData(422)] - [InlineData(499)] - public void IsEnable_ReturnsTrue_ForClientErrors(int statuCode) - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.ClientErrors }); - - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); - - //Assert - Assert.True(isEnabled); - } - - [Theory] - [InlineData(500)] - [InlineData(599)] - public void IsEnable_ReturnsTrue_ForServerErrors(int statuCode) - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.Exceptions }); - - //Act - var isEnabled = service.IsEnabled(statuCode, isRouting: false); - - //Assert - Assert.True(isEnabled); - } - - [Fact] - public void GetWriter_ReturnsNull_WhenNotEnabled() - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.Unspecified }); - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - - //Act - var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); - - //Assert - Assert.Null(writer); - } - - [Fact] - public void GetWriter_ReturnsNull_WhenNotRegisteredWriters() - { - //Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - - //Act - var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); - - //Assert - Assert.Null(writer); - } - - [Fact] - public void GetWriter_ReturnsNull_WhenNoWriterCanWrite() - { - //Arrange - var writers = new List() { - Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false), - Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == false) - }; - var service = CreateService( - writers: writers, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - - //Act - var writer = service.GetWriter(context, currentMetadata: null, isRouting: false); - - //Assert - Assert.Null(writer); - } - - [Fact] - public void GetWriter_Returns_ForContextMetadata() - { - //Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json"}); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - - //Act - var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); - - //Assert - Assert.NotNull(selectedWriter); - Assert.IsType(selectedWriter); - } - - [Fact] - public void GetWriter_Returns_ForSpecifiedMetadata() - { - //Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, EndpointMetadataCollection.Empty, null)); - - //Act - var selectedWriter = service.GetWriter(context, currentMetadata: metadata, isRouting: false); - - //Assert - Assert.NotNull(selectedWriter); - Assert.IsType(selectedWriter); - } - - [Fact] - public void GetWriter_Returns_FirstCanWriter() - { - //Arrange - var writer1 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); - var writer2 = Mock.Of(w => w.CanWrite(It.IsAny(), It.IsAny(), It.IsAny()) == true); - var writers = new List() { writer1, writer2 }; - var service = CreateService( - writers: writers, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - var context = new DefaultHttpContext() - { - Response = { StatusCode = StatusCodes.Status400BadRequest } - }; - - //Act - var selectedWriter = service.GetWriter(context, currentMetadata: null, isRouting: false); - - //Assert - Assert.NotNull(selectedWriter); - Assert.Equal(writer1, selectedWriter); - } - - [Fact] - public async Task WriteAsync_Call_SelectedWriter() - { - //Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); - var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - }; - - //Act - await service.WriteAsync(context, currentMetadata: metadata); - - //Assert - Assert.Equal("\"Content\"", Encoding.UTF8.GetString(stream.ToArray())); - } - - [Fact] - public async Task WriteAsync_Skip_WhenNoWriter() - { - //Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemTypes.All }); - var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - }; - - //Act - await service.WriteAsync(context); - - //Assert - Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); - } - - private static ProblemDetailsService CreateService( - ProblemDetailsOptions options, - IEnumerable writers = null) - { - writers ??= Array.Empty(); - return new ProblemDetailsService(writers, Options.Create(options)); - } - - private class SampleMetadata - { - public string ContentType { get; set; } - } - - private class MetadataBasedWriter : IProblemDetailsWriter - { - public bool CanWrite(HttpContext context, ProblemTypes problemType) - { - return metadata != null && metadata.GetMetadata != null; - } - - public Task WriteAsync(HttpContext context, int? statusCode = null, string title = null, string type = null, string detail = null, string instance = null, IDictionary extensions = null) - { - return context.Response.WriteAsJsonAsync("Content"); - } - } -} diff --git a/src/Http/Routing/src/EndpointMiddleware.cs b/src/Http/Routing/src/EndpointMiddleware.cs index 303a45188fa7..d04af317c0e4 100644 --- a/src/Http/Routing/src/EndpointMiddleware.cs +++ b/src/Http/Routing/src/EndpointMiddleware.cs @@ -70,10 +70,7 @@ public Task Invoke(HttpContext httpContext) return _next(httpContext); - static async Task AwaitRequestTask( - Endpoint endpoint, - Task requestTask, - ILogger logger) + static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger) { try { From a50ef62075c0ec3d7dd8ce00378a98799dd353ff Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jun 2022 13:51:00 -0700 Subject: [PATCH 31/59] Clean up --- .../ProblemDetails/{ProblemTypes.cs => ProblemDetailsTypes.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Http/Http.Abstractions/src/ProblemDetails/{ProblemTypes.cs => ProblemDetailsTypes.cs} (100%) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsTypes.cs similarity index 100% rename from src/Http/Http.Abstractions/src/ProblemDetails/ProblemTypes.cs rename to src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsTypes.cs From 6565091185893508fd411d3fdb7064ad8146c365 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jun 2022 13:51:50 -0700 Subject: [PATCH 32/59] clean up --- src/Http/Http/src/Builder/ApplicationBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index 700670084818..9e6298af839c 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -128,6 +128,7 @@ public RequestDelegate Build() $"routing."; throw new InvalidOperationException(message); } + context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; From 550dd9d9a8f9a23e0e0e6d17edfbc58c59a893af Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jun 2022 10:44:35 -0700 Subject: [PATCH 33/59] Reusing Endpoint & EndpointMetadataCollection --- src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs | 3 ++- src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 7bff38c5e3e8..9e8bfe7e1635 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Routing.Matching; internal sealed class AcceptsMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { + private static Endpoint? Http415Endpoint; internal const string Http415EndpointDisplayName = "415 HTTP Unsupported Media Type"; internal const string AnyContentType = "*/*"; @@ -258,7 +259,7 @@ public IReadOnlyList GetEdges(IReadOnlyList endpoints) private static Endpoint CreateRejectionEndpoint() { - return new Endpoint(context => + return Http415Endpoint ??= new Endpoint(context => { context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; return Task.CompletedTask; diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index cfe9456ea33f..0c26b2b8debf 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Routing.Matching; /// public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { + private static readonly EndpointMetadataCollection Http405EndpointMetadata = new(new RoutingProblemMetadata(StatusCodes.Status405MethodNotAllowed)); + // Used in tests internal static readonly string PreflightHttpMethod = HttpMethods.Options; @@ -411,7 +413,7 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; return Task.CompletedTask; }, - new EndpointMetadataCollection(new RoutingProblemMetadata(StatusCodes.Status405MethodNotAllowed)), + Http405EndpointMetadata, Http405EndpointDisplayName); } From f9fd5f62179cffcdca14a6bf78c77b13b8252520 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jun 2022 12:09:33 -0700 Subject: [PATCH 34/59] Fix build issues --- .../src/ProblemDetails/ProblemDetailsContext.cs | 2 +- .../Http.Abstractions/src/PublicAPI.Unshipped.txt | 2 +- .../src/DefaultProblemDetailsWriter.cs | 15 +++++++++++---- ...icrosoft.AspNetCore.Http.ProblemDetails.csproj | 2 +- .../ProblemDetails/src/ProblemDetailsService.cs | 4 ++++ .../DefaultApiProblemDetailsWriter.cs | 13 +++++-------- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs index 1624980190ab..9fd9b90afb51 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -33,5 +33,5 @@ public ProblemDetailsContext(HttpContext httpContext) /// A instance of that will be /// used during the response payload generation. /// - public ProblemDetails? ProblemDetails { get; init; } + public ProblemDetails ProblemDetails { get; init; } = new ProblemDetails(); } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index b0b52deb1374..a10c3ae197f5 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -32,7 +32,7 @@ Microsoft.AspNetCore.Http.ProblemDetailsContext Microsoft.AspNetCore.Http.ProblemDetailsContext.AdditionalMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection? Microsoft.AspNetCore.Http.ProblemDetailsContext.AdditionalMetadata.init -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! -Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.get -> Microsoft.AspNetCore.Mvc.ProblemDetails? +Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.get -> Microsoft.AspNetCore.Mvc.ProblemDetails! Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.init -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetailsContext(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> void Microsoft.AspNetCore.Http.ProblemDetailsTypes diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs index 39de738ee4e5..5671885520b2 100644 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.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.Json.Serialization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -17,10 +18,16 @@ public DefaultProblemDetailsWriter(IOptions options) public async ValueTask WriteAsync(ProblemDetailsContext context) { - var problemResult = TypedResults.Problem(context.ProblemDetails ?? new ProblemDetails()); - _options.ConfigureDetails?.Invoke(context.HttpContext, problemResult.ProblemDetails); + var httpContext = context.HttpContext; - await problemResult.ExecuteAsync(context.HttpContext); - return context.HttpContext.Response.HasStarted; + ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); + _options.ConfigureDetails?.Invoke(httpContext, context.ProblemDetails); + + await httpContext.Response.WriteAsJsonAsync(context.ProblemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); + return httpContext.Response.HasStarted; } + + [JsonSerializable(typeof(ProblemDetails))] + internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext + { } } diff --git a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj index 4aca8321df6a..d43064f94702 100644 --- a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj +++ b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs index 0e53faac59da..1a7d6e9ca748 100644 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ b/src/Http/ProblemDetails/src/ProblemDetailsService.cs @@ -22,6 +22,10 @@ public ProblemDetailsService( public async ValueTask WriteAsync(ProblemDetailsContext context) { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(context.ProblemDetails); + ArgumentNullException.ThrowIfNull(context.HttpContext); + if (_options.AllowedProblemTypes != ProblemDetailsTypes.None && _writers is { Length: > 0 }) { var problemStatusCode = context.ProblemDetails?.Status ?? context.HttpContext.Response.StatusCode; diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index f926606850a1..cee660bb7f50 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -41,11 +41,11 @@ public async ValueTask WriteAsync(ProblemDetailsContext context) // from the factory var problemDetails = _problemDetailsFactory.CreateProblemDetails( context.HttpContext, - context.ProblemDetails?.Status ?? context.HttpContext.Response.StatusCode, - context.ProblemDetails?.Title, - context.ProblemDetails?.Type, - context.ProblemDetails?.Detail, - context.ProblemDetails?.Instance); + context.ProblemDetails.Status ?? context.HttpContext.Response.StatusCode, + context.ProblemDetails.Title, + context.ProblemDetails.Type, + context.ProblemDetails.Detail, + context.ProblemDetails.Instance); if (context.ProblemDetails?.Extensions is not null) { @@ -68,9 +68,6 @@ public async ValueTask WriteAsync(ProblemDetailsContext context) if (selectedFormatter == null) { - await Results.Problem(problemDetails) - .ExecuteAsync(context.HttpContext); - return context.HttpContext.Response.HasStarted; } From 804ccac9ab1dabbf1c0e3f6555a4458bc98d33da Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jun 2022 12:17:10 -0700 Subject: [PATCH 35/59] Updates based on docs/Trimming.md --- src/Tools/Tools.slnf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index 4b5a33b51018..52472af8ff4c 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -5,6 +5,7 @@ "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj", "src\\Components\\Authorization\\src\\Microsoft.AspNetCore.Components.Authorization.csproj", "src\\Components\\Components\\src\\Microsoft.AspNetCore.Components.csproj", + "src\\Components\\CustomElements\\src\\Microsoft.AspNetCore.Components.CustomElements.csproj", "src\\Components\\Forms\\src\\Microsoft.AspNetCore.Components.Forms.csproj", "src\\Components\\WebAssembly\\Authentication.Msal\\src\\Microsoft.Authentication.WebAssembly.Msal.csproj", "src\\Components\\WebAssembly\\JSInterop\\src\\Microsoft.JSInterop.WebAssembly.csproj", @@ -33,6 +34,7 @@ "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", + "src\\Http\\ProblemDetails\\src\\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", @@ -52,6 +54,7 @@ "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", "src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", + "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\RateLimiting\\src\\Microsoft.AspNetCore.RateLimiting.csproj", "src\\Middleware\\RequestDecompression\\src\\Microsoft.AspNetCore.RequestDecompression.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", From 54e369d9b471c61c480fd0849af478ee964ad34d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jun 2022 17:04:35 -0700 Subject: [PATCH 36/59] Adding Functional tests --- .../DeveloperExceptionPageMiddleware.cs | 10 +++ src/Middleware/Diagnostics/src/Resources.resx | 2 +- .../DeveloperExceptionPageSampleTest.cs | 23 ++++++- ...roblemDetailsExceptionHandlerSampleTest.cs | 35 +++++++++++ .../FunctionalTests/StatusCodeSampleTest.cs | 21 ++++++- .../test/UnitTests/ExceptionHandlerTest.cs | 3 +- .../DeveloperExceptionPageSample.csproj | 7 ++- .../DeveloperExceptionPageSample/Startup.cs | 10 ++- .../ExceptionHandlerSample.csproj | 7 ++- .../ExceptionHandlerSample/Startup.cs | 16 +---- .../StartupWithProblemDetails.cs | 62 +++++++++++++++++++ .../StatusCodePagesSample/Startup.cs | 18 ++++++ .../StatusCodePagesSample.csproj | 8 ++- 13 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs create mode 100644 src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 7be3941fb1f8..eb12655f7f9a 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.StackTrace.Sources; @@ -193,11 +194,20 @@ private async Task DisplayExceptionContent(ErrorContext errorContext) { var problemDetails = new ProblemDetails { + Title = TypeNameHelper.GetTypeDisplayName(errorContext.Exception.GetType()), + Detail = errorContext.Exception.Message, Status = httpContext.Response.StatusCode }; + var exceptionDetails = _exceptionDetailsProvider.GetDetails(errorContext.Exception); problemDetails.Extensions["exception"] = new { + Details = exceptionDetails.Select(d => new + { + Message = d.ErrorMessage ?? d.Error?.Message, + Type = TypeNameHelper.GetTypeDisplayName(d.Error), + StackFrames = d.StackFrames, + }), Headers = httpContext.Request.Headers, Error = errorContext.Exception.ToString(), Path = httpContext.Request.Path, diff --git a/src/Middleware/Diagnostics/src/Resources.resx b/src/Middleware/Diagnostics/src/Resources.resx index 608af43ce852..6ac14a9a8b68 100644 --- a/src/Middleware/Diagnostics/src/Resources.resx +++ b/src/Middleware/Diagnostics/src/Resources.resx @@ -248,7 +248,7 @@ Environment: - An error occurred when configuring the exception handler middleware. Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'. Alternatively, set one of the aforementioned properties in 'Startup.ConfigureServices' as follows: 'services.AddExceptionHandler(options => { ... });' or configure to generate a 'ProblemDetails' response in 'service.AddProblemDetails(options => options.SuppressMapExceptions = false)'. + An error occurred when configuring the exception handler middleware. Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'. Alternatively, set one of the aforementioned properties in 'Startup.ConfigureServices' as follows: 'services.AddExceptionHandler(options => { ... });' or configure to generate a 'ProblemDetails' response in 'service.AddProblemDetails()'. No route values. diff --git a/src/Middleware/Diagnostics/test/FunctionalTests/DeveloperExceptionPageSampleTest.cs b/src/Middleware/Diagnostics/test/FunctionalTests/DeveloperExceptionPageSampleTest.cs index 7691e4a9c005..25770a6d51a1 100644 --- a/src/Middleware/Diagnostics/test/FunctionalTests/DeveloperExceptionPageSampleTest.cs +++ b/src/Middleware/Diagnostics/test/FunctionalTests/DeveloperExceptionPageSampleTest.cs @@ -1,8 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Mvc; +using System.Net.Http.Json; namespace Microsoft.AspNetCore.Diagnostics.FunctionalTests; @@ -29,4 +32,22 @@ public async Task DeveloperExceptionPage_ShowsError() Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); Assert.Contains("Exception: Demonstration exception.", body); } + + [Fact] + public async Task DeveloperExceptionPage_ShowsProblemDetails_WhenHtmlNotAccepted() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // Act + var response = await Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadFromJsonAsync(); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.NotNull(body); + Assert.Equal(500, body.Status); + Assert.Contains("Demonstration exception", body.Detail); + } } diff --git a/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs b/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs new file mode 100644 index 000000000000..400d420d1817 --- /dev/null +++ b/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using System.Net.Http.Json; + +namespace Microsoft.AspNetCore.Diagnostics.FunctionalTests; + +public class ProblemDetailsExceptionHandlerSampleTest : IClassFixture> +{ + public ProblemDetailsExceptionHandlerSampleTest(TestFixture fixture) + { + Client = fixture.Client; + } + + public HttpClient Client { get; } + + [Fact] + public async Task ExceptionHandlerPage_ProducesProblemDetails() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/throw"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadFromJsonAsync(); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.NotNull(body); + Assert.Equal(500, body.Status); + } +} diff --git a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs index 193999955a41..6cdc8be18299 100644 --- a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs +++ b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs @@ -1,8 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using System.Net.Http.Json; using Microsoft.AspNetCore.WebUtilities; namespace Microsoft.AspNetCore.Diagnostics.FunctionalTests; @@ -70,4 +72,21 @@ public async Task StatusCodePageOptions_IncludesSemicolon__AndReasonPhrase_WhenR Assert.Contains(";", responseBody); Assert.Contains(statusCodeReasonPhrase, responseBody); } + + [Fact] + public async Task StatusCodePage_ProducesProblemDetails() + { + // Arrange + var httpStatusCode = 400; + var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost/status?includeProblemMetadata=true&statuscode={httpStatusCode}"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadFromJsonAsync(); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.NotNull(body); + Assert.Equal(400, body.Status); + } } diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs index ee3582ab0b9e..9b7053d4c050 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs @@ -523,7 +523,8 @@ public void UsingExceptionHandler_ThrowsAnException_WhenExceptionHandlingPathNot // Assert Assert.Equal("An error occurred when configuring the exception handler middleware. " + "Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'. " + - "Alternatively, set one of the aforementioned properties in 'Startup.ConfigureServices' as follows: 'services.AddExceptionHandler(options => { ... });'.", + "Alternatively, set one of the aforementioned properties in 'Startup.ConfigureServices' as follows: 'services.AddExceptionHandler(options => { ... });' " + + "or configure to generate a 'ProblemDetails' response in 'service.AddProblemDetails()'.", exception.Message); } diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj index 40106c9233b9..ca44620292a6 100644 --- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj +++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -9,9 +9,14 @@ + + + + + diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs index f3592f61c7b8..4f15ba4bbeb2 100644 --- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs @@ -1,12 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Patterns; namespace DeveloperExceptionPageSample; public class Startup { + public void ConfigureServices(IServiceCollection services) + { + services.AddProblemDetails(); + } + public void Configure(IApplicationBuilder app) { app.Use((context, next) => @@ -21,7 +27,9 @@ public void Configure(IApplicationBuilder app) c => null, RoutePatternFactory.Parse("/"), 0, - new EndpointMetadataCollection(new HttpMethodMetadata(new[] { "GET", "POST" })), + new EndpointMetadataCollection( + new HttpMethodMetadata(new[] { "GET", "POST" }), + new ProblemMetadata()), "Endpoint display name"); context.SetEndpoint(endpoint); diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj index 907fa264f876..002682a4ab76 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -6,10 +6,15 @@ + + + + + diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs index 4d9402a535c3..40bb8ab3f5e7 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs @@ -3,6 +3,7 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.Metadata; namespace ExceptionHandlerSample; @@ -53,20 +54,5 @@ public void Configure(IApplicationBuilder app) await context.Response.WriteAsync("\r\n"); }); } - - public static Task Main(string[] args) - { - var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseKestrel() - .UseIISIntegration() - .UseStartup(); - }) - .Build(); - - return host.RunAsync(); - } } diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs new file mode 100644 index 000000000000..67d44709be8d --- /dev/null +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs @@ -0,0 +1,62 @@ +// 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.Encodings.Web; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.Metadata; + +namespace ExceptionHandlerSample; + +public class StartupWithProblemDetails +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddProblemDetails(); + } + + public void Configure(IApplicationBuilder app) + { + // Add a problemMetadata to all requests + app.Use((context, next) => + { + context.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(new ProblemMetadata()), string.Empty)); + return next(context); + }); + + // Configure the error handler to produces a ProblemDetails. + app.UseExceptionHandler(); + + // The broken section of our application. + app.Map("/throw", throwApp => + { + throwApp.Run(context => { throw new Exception("Application Exception"); }); + }); + + app.UseStaticFiles(); + + // The home page. + app.Run(async context => + { + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync("Welcome to the sample

\r\n"); + await context.Response.WriteAsync("Click here to throw an exception: throw\r\n"); + await context.Response.WriteAsync("\r\n"); + }); + } + + public static Task Main(string[] args) + { + var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .UseIISIntegration() + .UseStartup(); + }) + .Build(); + + return host.RunAsync(); + } +} + diff --git a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs index c1c8fd1e3f67..063f4c48611b 100644 --- a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs @@ -6,13 +6,31 @@ using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.Metadata; namespace StatusCodePagesSample; public class Startup { + public void ConfigureServices(IServiceCollection services) + { + services.AddProblemDetails(); + } + public void Configure(IApplicationBuilder app) { + // Add a problemMetadata to all requests + app.Use((context, next) => + { + var includeProblemMetadata = context.Request.Query["includeProblemMetadata"]; + if (includeProblemMetadata == "true") + { + context.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(new ProblemMetadata()), string.Empty)); + } + + return next(context); + }); + app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); // There is a default response but any of the following can be used to change the behavior. diff --git a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj index 755410745d90..f87ca5956c7b 100644 --- a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj +++ b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -6,8 +6,14 @@ + + + + + + From 1e695aa286141e8f8f276a9267666dd9cd026090 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jun 2022 22:41:39 -0700 Subject: [PATCH 37/59] Fix unittest --- src/Framework/test/TestData.cs | 2 ++ .../test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Framework/test/TestData.cs b/src/Framework/test/TestData.cs index 28aaa88dcc7b..ad125d433d57 100644 --- a/src/Framework/test/TestData.cs +++ b/src/Framework/test/TestData.cs @@ -52,6 +52,7 @@ static TestData() "Microsoft.AspNetCore.Http.Connections.Common", "Microsoft.AspNetCore.Http.Extensions", "Microsoft.AspNetCore.Http.Features", + "Microsoft.AspNetCore.Http.ProblemDetails", "Microsoft.AspNetCore.Http.Results", "Microsoft.AspNetCore.HttpLogging", "Microsoft.AspNetCore.HttpOverrides", @@ -189,6 +190,7 @@ static TestData() { "Microsoft.AspNetCore.Http.Connections.Common", "7.0.0.0" }, { "Microsoft.AspNetCore.Http.Extensions", "7.0.0.0" }, { "Microsoft.AspNetCore.Http.Features", "7.0.0.0" }, + { "Microsoft.AspNetCore.Http.ProblemDetails", "7.0.0.0" }, { "Microsoft.AspNetCore.Http.Results", "7.0.0.0" }, { "Microsoft.AspNetCore.HttpLogging", "7.0.0.0" }, { "Microsoft.AspNetCore.HttpOverrides", "7.0.0.0" }, diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs index b5cf4b1b7e2e..8b8c703904ff 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using static Microsoft.AspNetCore.Routing.Matching.HttpMethodMatcherPolicy; @@ -169,7 +170,9 @@ public async Task IEndpointSelectorPolicy_ApplyAsync_ProcessesInvalidCandidate(i await policy.ApplyAsync(httpContext, candidates); - Assert.Equal(httpContext.GetEndpoint().Metadata, EndpointMetadataCollection.Empty); + var metadata = Assert.Single(httpContext.GetEndpoint().Metadata); + var problemMetadata = Assert.IsAssignableFrom(metadata); + Assert.Equal(405, problemMetadata.StatusCode); Assert.True(string.Equals(httpContext.GetEndpoint().DisplayName, Http405EndpointDisplayName, StringComparison.OrdinalIgnoreCase)); } From 2a743dbf9fbabf905978f77b86f73ec43a2f4fe1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jun 2022 22:45:52 -0700 Subject: [PATCH 38/59] Seal context --- .../src/ProblemDetails/ProblemDetailsContext.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs index 9fd9b90afb51..7b07bd18e53a 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Http; /// /// Represent the current problem detatils context for the request. /// -public class ProblemDetailsContext +public sealed class ProblemDetailsContext { /// /// Creates a new instance of the . @@ -16,6 +16,8 @@ public class ProblemDetailsContext /// The associated with the current request being processed by the filter. public ProblemDetailsContext(HttpContext httpContext) { + ArgumentNullException.ThrowIfNull(httpContext); + HttpContext = httpContext; } From 8bbe54c7f9abab102fa0c612fcf99519a4314d69 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 30 Jun 2022 09:45:30 -0700 Subject: [PATCH 39/59] Seal ProblemMetadata --- src/Shared/ProblemDetails/ProblemMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/ProblemDetails/ProblemMetadata.cs b/src/Shared/ProblemDetails/ProblemMetadata.cs index 301893fa6a97..227970ca1c67 100644 --- a/src/Shared/ProblemDetails/ProblemMetadata.cs +++ b/src/Shared/ProblemDetails/ProblemMetadata.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Http.Metadata; -internal class ProblemMetadata : IProblemDetailsMetadata +internal sealed class ProblemMetadata : IProblemDetailsMetadata { public ProblemMetadata(int? statusCode = null, ProblemDetailsTypes problemType = ProblemDetailsTypes.All) { From a04b7ae37f5ea573dff3624c81f2ad5928723c8c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 6 Jul 2022 13:43:21 -0700 Subject: [PATCH 40/59] PR Feeback --- .../Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs index 4d47cb4bf9de..96d427024d0e 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs @@ -12,11 +12,11 @@ public interface IProblemDetailsMetadata /// /// Gets the HTTP status code of the response. /// - public int? StatusCode { get; } + int? StatusCode { get; } /// /// Gets the Problem Details Types /// associated to the . /// - public ProblemDetailsTypes ProblemType { get; } + ProblemDetailsTypes ProblemType { get; } } From cf188c26f6c792196c992a0c6f637c27044b9194 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 Jul 2022 14:42:58 -0700 Subject: [PATCH 41/59] API Review --- AspNetCore.sln | 41 --- .../src/Metadata/IProblemDetailsMetadata.cs | 22 -- .../ProblemDetails/IProblemDetailsWriter.cs | 2 +- .../ProblemDetails/ProblemDetailsContext.cs | 13 +- .../src/ProblemDetails/ProblemDetailsTypes.cs | 36 -- .../src/PublicAPI.Unshipped.txt | 14 +- ...lidationProblemDetailsJsonConverterTest.cs | 2 +- .../test/ProblemDetailsJsonConverterTest.cs | 2 +- ...icrosoft.AspNetCore.Http.Extensions.csproj | 1 + .../src/ProblemDetailsDefaultWriter.cs | 49 +++ .../src/ProblemDetailsOptions.cs | 16 + .../src/ProblemDetailsService.cs | 35 ++ ...oblemDetailsServiceCollectionExtensions.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 7 + .../test/ProblemDetailsDefaultWriterTest.cs} | 101 +++++- ...mDetailsServiceCollectionExtensionsTest.cs | 4 +- .../test/ProblemDetailsServiceTest.cs | 135 +++++++ .../src/DefaultProblemDetailsWriter.cs | 33 -- .../ProblemDetailsOptions.cs | 26 -- ...soft.AspNetCore.Http.ProblemDetails.csproj | 27 -- .../src/ProblemDetailsService.cs | 99 ----- .../ProblemDetails/src/PublicAPI.Shipped.txt | 1 - .../src/PublicAPI.Unshipped.txt | 10 - ...spNetCore.Http.ProblemDetails.Tests.csproj | 12 - .../test/ProblemDetailsServiceTest.cs | 285 --------------- .../src/Matching/AcceptsMatcherPolicy.cs | 2 +- .../src/Matching/HttpMethodMatcherPolicy.cs | 5 +- .../src/Microsoft.AspNetCore.Routing.csproj | 1 - .../DeveloperExceptionPageExtensions.cs | 25 +- .../DeveloperExceptionPageMiddleware.cs | 324 +---------------- .../DeveloperExceptionPageMiddlewareImpl.cs | 337 ++++++++++++++++++ .../ExceptionHandlerExtensions.cs | 10 +- .../ExceptionHandlerMiddleware.cs | 201 +---------- .../ExceptionHandlerMiddlewareImpl.cs | 216 +++++++++++ .../Microsoft.AspNetCore.Diagnostics.csproj | 1 - .../Diagnostics/src/PublicAPI.Unshipped.txt | 2 - .../StatusCodePage/StatusCodePagesOptions.cs | 10 +- .../FunctionalTests/StatusCodeSampleTest.cs | 8 +- .../DeveloperExceptionPageSample.csproj | 5 - .../DeveloperExceptionPageSample/Startup.cs | 3 +- .../ExceptionHandlerSample.csproj | 5 - .../StartupWithProblemDetails.cs | 7 - .../StatusCodePagesSample/Startup.cs | 12 - .../StatusCodePagesSample.csproj | 6 - .../ApiBehaviorApplicationModelProvider.cs | 5 +- .../EndpointMetadataConvention.cs | 24 +- .../src/Formatters/TextOutputFormatter.cs | 5 +- .../DefaultApiProblemDetailsWriter.cs | 23 +- .../DefaultProblemDetailsFactory.cs | 6 +- .../Infrastructure/ObjectResultExecutor.cs | 3 +- .../ProblemDetailsClientErrorFactory.cs | 16 +- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 - .../EndpointMetadataConventionTest.cs | 59 +-- .../DefaultApiProblemDetailsWriterTest.cs | 200 +++++++++++ .../ProblemDetalsClientErrorFactoryTest.cs | 8 +- .../ProblemDetails/ProblemDetailsDefaults.cs | 6 + src/Shared/ProblemDetails/ProblemMetadata.cs | 17 - .../ProblemDetails/RoutingProblemMetadata.cs | 18 - 58 files changed, 1169 insertions(+), 1377 deletions(-) delete mode 100644 src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs delete mode 100644 src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsTypes.cs rename src/Http/{ProblemDetails => Http.Abstractions}/test/HttpValidationProblemDetailsJsonConverterTest.cs (98%) rename src/Http/{ProblemDetails => Http.Abstractions}/test/ProblemDetailsJsonConverterTest.cs (99%) create mode 100644 src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs create mode 100644 src/Http/Http.Extensions/src/ProblemDetailsOptions.cs create mode 100644 src/Http/Http.Extensions/src/ProblemDetailsService.cs rename src/Http/{ProblemDetails/src/DependencyInjection => Http.Extensions/src}/ProblemDetailsServiceCollectionExtensions.cs (97%) rename src/Http/{ProblemDetails/test/DefaultProblemDetailsWriterTest.cs => Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs} (61%) rename src/Http/{ProblemDetails => Http.Extensions}/test/ProblemDetailsServiceCollectionExtensionsTest.cs (93%) create mode 100644 src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs delete mode 100644 src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs delete mode 100644 src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs delete mode 100644 src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj delete mode 100644 src/Http/ProblemDetails/src/ProblemDetailsService.cs delete mode 100644 src/Http/ProblemDetails/src/PublicAPI.Shipped.txt delete mode 100644 src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt delete mode 100644 src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj delete mode 100644 src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs create mode 100644 src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs create mode 100644 src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs create mode 100644 src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs delete mode 100644 src/Shared/ProblemDetails/ProblemMetadata.cs delete mode 100644 src/Shared/ProblemDetails/RoutingProblemMetadata.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index f303a53e9034..28b0be916a4a 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1728,10 +1728,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.ProblemDetails", "Http.ProblemDetails", "{41AF137D-4181-42F9-9B53-BEDB9532F29B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.ProblemDetails", "src\Http\ProblemDetails\src\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "{2333E682-F9E9-4235-BF63-6403C114EA76}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression.Microbenchmarks", "src\Middleware\RequestDecompression\perf\Microbenchmarks\Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj", "{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}" @@ -1750,8 +1746,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestInfrastructure", "TestI src\ProjectTemplates\TestInfrastructure\runtimeconfig.norollforward.json = src\ProjectTemplates\TestInfrastructure\runtimeconfig.norollforward.json EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.ProblemDetails.Tests", "src\Http\ProblemDetails\test\Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj", "{13384A43-5CF9-4CBE-9F20-B4D6FB304C11}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Mvc.Tests", "src\ProjectTemplates\test\Templates.Mvc.Tests\Templates.Mvc.Tests.csproj", "{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Blazor.Server.Tests", "src\ProjectTemplates\test\Templates.Blazor.Server.Tests\Templates.Blazor.Server.Tests.csproj", "{281BF9DB-7B8A-446B-9611-10A60903F125}" @@ -10397,22 +10391,6 @@ Global {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x64.Build.0 = Release|Any CPU {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.ActiveCfg = Release|Any CPU {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.Build.0 = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|arm64.ActiveCfg = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|arm64.Build.0 = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x64.ActiveCfg = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x64.Build.0 = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x86.ActiveCfg = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Debug|x86.Build.0 = Debug|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|Any CPU.Build.0 = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|arm64.ActiveCfg = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|arm64.Build.0 = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x64.ActiveCfg = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x64.Build.0 = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.ActiveCfg = Release|Any CPU - {2333E682-F9E9-4235-BF63-6403C114EA76}.Release|x86.Build.0 = Release|Any CPU {3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.Build.0 = Debug|Any CPU {3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -10477,22 +10455,6 @@ Global {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x64.Build.0 = Release|Any CPU {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.ActiveCfg = Release|Any CPU {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.Build.0 = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|arm64.ActiveCfg = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|arm64.Build.0 = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x64.ActiveCfg = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x64.Build.0 = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x86.ActiveCfg = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Debug|x86.Build.0 = Debug|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|Any CPU.Build.0 = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|arm64.ActiveCfg = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|arm64.Build.0 = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x64.ActiveCfg = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x64.Build.0 = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x86.ActiveCfg = Release|Any CPU - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11}.Release|x86.Build.0 = Release|Any CPU {AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -11413,15 +11375,12 @@ Global {51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F} {487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088} {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} - {41AF137D-4181-42F9-9B53-BEDB9532F29B} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724} - {2333E682-F9E9-4235-BF63-6403C114EA76} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} {3309FA1E-4E95-49E9-BE2A-827D01FD63C0} = {5465F96F-33D5-454E-9C40-494E58AEEE5D} {B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730} {AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} {7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67} {89896261-C5DD-4901-BCA7-7A5F718BC008} = {AB4B9E75-719C-4589-B852-20FBFD727730} {F0FBA346-D8BC-4FAE-A4B2-85B33FA23055} = {08D53E58-4AAE-40C4-8497-63EC8664F304} - {13384A43-5CF9-4CBE-9F20-B4D6FB304C11} = {41AF137D-4181-42F9-9B53-BEDB9532F29B} {AA7445F5-BD28-400C-8507-E2E0D3CF7D7E} = {08D53E58-4AAE-40C4-8497-63EC8664F304} {281BF9DB-7B8A-446B-9611-10A60903F125} = {08D53E58-4AAE-40C4-8497-63EC8664F304} {A5946454-4788-4871-8F23-A9471D55F115} = {4FDDC525-4E60-4CAF-83A3-261C5B43721F} diff --git a/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs deleted file mode 100644 index 96d427024d0e..000000000000 --- a/src/Http/Http.Abstractions/src/Metadata/IProblemDetailsMetadata.cs +++ /dev/null @@ -1,22 +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.Metadata; - -/// -/// Defines a contract used to specify metadata,in , -/// for configure generation. -/// -public interface IProblemDetailsMetadata -{ - /// - /// Gets the HTTP status code of the response. - /// - int? StatusCode { get; } - - /// - /// Gets the Problem Details Types - /// associated to the . - /// - ProblemDetailsTypes ProblemType { get; } -} diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index 2b1cd2181b76..09d2e920e086 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -14,5 +14,5 @@ public interface IProblemDetailsWriter /// /// The associated with the current request/response. /// Flag that indicates if the response was started. - ValueTask WriteAsync(ProblemDetailsContext context); + ValueTask TryWriteAsync(ProblemDetailsContext context); } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs index 7b07bd18e53a..4ef374a3ec5d 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -10,21 +10,10 @@ namespace Microsoft.AspNetCore.Http; ///
public sealed class ProblemDetailsContext { - /// - /// Creates a new instance of the . - /// - /// The associated with the current request being processed by the filter. - public ProblemDetailsContext(HttpContext httpContext) - { - ArgumentNullException.ThrowIfNull(httpContext); - - HttpContext = httpContext; - } - /// /// The associated with the current request being processed by the filter. /// - public HttpContext HttpContext { get; } + public required HttpContext HttpContext { get; init; } /// /// A collection of additional arbitrary metadata associated with the current request endpoint. diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsTypes.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsTypes.cs deleted file mode 100644 index dbe2e8925478..000000000000 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsTypes.cs +++ /dev/null @@ -1,36 +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; - -/// -/// Represents the possible problem details types. -/// -[Flags] -public enum ProblemDetailsTypes : uint -{ - /// - /// Specifies no types. - /// - None = 0, - - /// - /// HTTP Status code 5xx - /// - Server = 1, - - /// - /// Failures occurred during the routing system processing. - /// - Routing = 2, - - /// - /// HTTP Status code 4xx - /// - Client = 4, - - /// - /// Specifies all types. - /// - All = Routing | Server | Client, -} diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index a10c3ae197f5..ae8cd390cbd6 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -19,28 +19,20 @@ Microsoft.AspNetCore.Http.IBindableFromHttpContext.BindAsync(Microsoft.As Microsoft.AspNetCore.Http.IProblemDetailsService Microsoft.AspNetCore.Http.IProblemDetailsService.WriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.IProblemDetailsWriter -Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Http.IProblemDetailsWriter.TryWriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? -Microsoft.AspNetCore.Http.Metadata.IProblemDetailsMetadata -Microsoft.AspNetCore.Http.Metadata.IProblemDetailsMetadata.ProblemType.get -> Microsoft.AspNetCore.Http.ProblemDetailsTypes -Microsoft.AspNetCore.Http.Metadata.IProblemDetailsMetadata.StatusCode.get -> int? Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long? Microsoft.AspNetCore.Http.ProblemDetailsContext Microsoft.AspNetCore.Http.ProblemDetailsContext.AdditionalMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection? Microsoft.AspNetCore.Http.ProblemDetailsContext.AdditionalMetadata.init -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! +Microsoft.AspNetCore.Http.ProblemDetailsContext.HttpContext.init -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.get -> Microsoft.AspNetCore.Mvc.ProblemDetails! Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.init -> void -Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetailsContext(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> void -Microsoft.AspNetCore.Http.ProblemDetailsTypes -Microsoft.AspNetCore.Http.ProblemDetailsTypes.All = Microsoft.AspNetCore.Http.ProblemDetailsTypes.Server | Microsoft.AspNetCore.Http.ProblemDetailsTypes.Routing | Microsoft.AspNetCore.Http.ProblemDetailsTypes.Client -> Microsoft.AspNetCore.Http.ProblemDetailsTypes -Microsoft.AspNetCore.Http.ProblemDetailsTypes.Client = 4 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes -Microsoft.AspNetCore.Http.ProblemDetailsTypes.None = 0 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes -Microsoft.AspNetCore.Http.ProblemDetailsTypes.Routing = 2 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes -Microsoft.AspNetCore.Http.ProblemDetailsTypes.Server = 1 -> Microsoft.AspNetCore.Http.ProblemDetailsTypes +Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetailsContext() -> void Microsoft.AspNetCore.Http.RouteHandlerContext Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> System.Collections.Generic.IList! diff --git a/src/Http/ProblemDetails/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs similarity index 98% rename from src/Http/ProblemDetails/test/HttpValidationProblemDetailsJsonConverterTest.cs rename to src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs index ea8073ce6867..94bce60ddc89 100644 --- a/src/Http/ProblemDetails/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -5,7 +5,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Http.Json; -namespace Microsoft.AspNetCore.Http.Extensions; +namespace Microsoft.AspNetCore.Http.Abstractions.Tests; public class HttpValidationProblemDetailsJsonConverterTest { diff --git a/src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs similarity index 99% rename from src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs rename to src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs index 066f346fa638..77259e56e9a5 100644 --- a/src/Http/ProblemDetails/test/ProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc; -namespace Microsoft.AspNetCore.Http.Extensions; +namespace Microsoft.AspNetCore.Http.Abstractions.Tests; public class ProblemDetailsJsonConverterTest { diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index dd196773100a..1d181f224b21 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs new file mode 100644 index 000000000000..f1d5056db36a --- /dev/null +++ b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.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. + +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http; + +internal sealed partial class ProblemDetailsDefaultWriter : IProblemDetailsWriter +{ + private readonly MediaTypeHeaderValue _jsonMediaType = new("application/json"); + private readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json"); + private readonly ProblemDetailsOptions _options; + + public ProblemDetailsDefaultWriter(IOptions options) + { + _options = options.Value; + } + + public async ValueTask TryWriteAsync(ProblemDetailsContext context) + { + var httpContext = context.HttpContext; + var acceptHeader = httpContext.Request.GetTypedHeaders().Accept; + + if (acceptHeader is { Count: > 0 } && + !acceptHeader.Any(h => _jsonMediaType.IsSubsetOf(h) || _problemDetailsJsonMediaType.IsSubsetOf(h))) + { + return false; + } + + ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); + _options.CustomizeProblemDetails?.Invoke(context); + + await httpContext.Response.WriteAsJsonAsync( + context.ProblemDetails, + typeof(ProblemDetails), + ProblemDetailsJsonContext.Default, + contentType: "application/problem+json"); + + return httpContext.Response.HasStarted; + } + + [JsonSerializable(typeof(ProblemDetails))] + internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext + { } +} diff --git a/src/Http/Http.Extensions/src/ProblemDetailsOptions.cs b/src/Http/Http.Extensions/src/ProblemDetailsOptions.cs new file mode 100644 index 000000000000..13fd59d82d47 --- /dev/null +++ b/src/Http/Http.Extensions/src/ProblemDetailsOptions.cs @@ -0,0 +1,16 @@ +// 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; + +/// +/// Options for controlling the behavior of +/// and similar methods. +/// +public class ProblemDetailsOptions +{ + /// + /// The operation that customizes the current instance. + /// + public Action? CustomizeProblemDetails { get; set; } +} diff --git a/src/Http/Http.Extensions/src/ProblemDetailsService.cs b/src/Http/Http.Extensions/src/ProblemDetailsService.cs new file mode 100644 index 000000000000..02021a864c85 --- /dev/null +++ b/src/Http/Http.Extensions/src/ProblemDetailsService.cs @@ -0,0 +1,35 @@ +// 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; + +internal sealed class ProblemDetailsService : IProblemDetailsService +{ + private readonly IEnumerable _writers; + + public ProblemDetailsService( + IEnumerable writers) + { + _writers = writers; + } + + public async ValueTask WriteAsync(ProblemDetailsContext context) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(context.ProblemDetails); + ArgumentNullException.ThrowIfNull(context.HttpContext); + + if (context.HttpContext.Response.HasStarted || context.HttpContext.Response.StatusCode < 400) + { + return; + } + + foreach (var writer in _writers) + { + if (await writer.TryWriteAsync(context)) + { + break; + } + } + } +} diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs similarity index 97% rename from src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs rename to src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index 5675b31208dd..145b411c8110 100644 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -23,7 +23,7 @@ public static IServiceCollection AddProblemDetails(this IServiceCollection servi // Adding default services; services.TryAddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); return services; } diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 3187db380ad4..6d8944593792 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -17,6 +17,10 @@ Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext! context) -> void Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext! parameterContext) -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions +Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails.get -> System.Action? +Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails.set -> void +Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointMetadata.get -> System.Collections.Generic.IList? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? @@ -34,11 +38,14 @@ Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactor *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void +Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, TValue value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions.ConfigureRouteHandlerJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! Microsoft.AspNetCore.Http.EndpointDescriptionAttribute Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.EndpointDescriptionAttribute(string! description) -> void diff --git a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs similarity index 61% rename from src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs rename to src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index e67de3066d4a..19cf2a4838d5 100644 --- a/src/Http/ProblemDetails/test/DefaultProblemDetailsWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Http.Tests; +namespace Microsoft.AspNetCore.Http.Extensions.Tests; public class DefaultProblemDetailsWriterTest { @@ -27,13 +27,14 @@ public async Task WriteAsync_Works() Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1-custom", Title = "Custom Bad Request", }; - var problemDetailsContext = new ProblemDetailsContext(context) + var problemDetailsContext = new ProblemDetailsContext() { + HttpContext = context, ProblemDetails = expectedProblem }; //Act - await writer.WriteAsync(problemDetailsContext); + await writer.TryWriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -57,13 +58,14 @@ public async Task WriteAsync_AddExtensions() expectedProblem.Extensions["Extension1"] = "Extension1-Value"; expectedProblem.Extensions["Extension2"] = "Extension2-Value"; - var problemDetailsContext = new ProblemDetailsContext(context) + var problemDetailsContext = new ProblemDetailsContext() { + HttpContext = context, ProblemDetails = expectedProblem }; //Act - await writer.WriteAsync(problemDetailsContext); + await writer.TryWriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -91,7 +93,7 @@ public async Task WriteAsync_Applies_Defaults() var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.WriteAsync(new ProblemDetailsContext(context)); + await writer.TryWriteAsync(new ProblemDetailsContext() { HttpContext = context }); //Assert stream.Position = 0; @@ -108,11 +110,11 @@ public async Task WriteAsync_Applies_CustomConfiguration() // Arrange var options = new ProblemDetailsOptions() { - ConfigureDetails = (context, problem) => + CustomizeProblemDetails = (context) => { - problem.Status = StatusCodes.Status406NotAcceptable; - problem.Title = "Custom Title"; - problem.Extensions["new-extension"] = new { TraceId = Guid.NewGuid() }; + context.ProblemDetails.Status = StatusCodes.Status406NotAcceptable; + context.ProblemDetails.Title = "Custom Title"; + context.ProblemDetails.Extensions["new-extension"] = new { TraceId = Guid.NewGuid() }; } }; var writer = GetWriter(options); @@ -120,9 +122,10 @@ public async Task WriteAsync_Applies_CustomConfiguration() var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.WriteAsync(new ProblemDetailsContext(context) + await writer.TryWriteAsync(new ProblemDetailsContext() { - ProblemDetails = new() { Status = StatusCodes.Status400BadRequest } + HttpContext = context, + ProblemDetails = { Status = StatusCodes.Status400BadRequest } }); //Assert @@ -144,9 +147,10 @@ public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified() var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.WriteAsync(new ProblemDetailsContext(context) + await writer.TryWriteAsync(new ProblemDetailsContext() { - ProblemDetails = new() { Status = StatusCodes.Status400BadRequest } + HttpContext = context, + ProblemDetails = { Status = StatusCodes.Status400BadRequest } }); //Assert @@ -158,12 +162,73 @@ await writer.WriteAsync(new ProblemDetailsContext(context) Assert.Equal("Bad Request", problemDetails.Title); } - private HttpContext CreateContext(Stream body, int statusCode = StatusCodes.Status400BadRequest) + [Theory] + [InlineData("*/*")] + [InlineData("application/*")] + [InlineData("application/json")] + [InlineData("application/problem+json")] + public async Task WriteAsync_Works_WhenJsonAccepted(string contentType) + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream); + context.Request.Headers.Accept = contentType; + + var expectedProblem = new ProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1-custom", + Title = "Custom Bad Request", + }; + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.TryWriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + } + + [Theory] + [InlineData("application/xml")] + [InlineData("application/problem+xml")] + public async Task WriteAsync_Skips_WhenJsonNotAccepted(string contentType) + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream); + context.Request.Headers.Accept = contentType; + + //Act + var result = await writer.TryWriteAsync(new() { HttpContext = context }); + + //Assert + Assert.False(result); + Assert.Equal(0, stream.Position); + Assert.Equal(0, stream.Length); + } + + private static HttpContext CreateContext(Stream body, int statusCode = StatusCodes.Status400BadRequest) { return new DefaultHttpContext() { Response = { Body = body, StatusCode = statusCode }, - RequestServices = CreateServices(), + RequestServices = CreateServices() }; } @@ -176,9 +241,9 @@ private static IServiceProvider CreateServices() return services.BuildServiceProvider(); } - private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null) + private static ProblemDetailsDefaultWriter GetWriter(ProblemDetailsOptions options = null) { options ??= new ProblemDetailsOptions(); - return new DefaultProblemDetailsWriter(Options.Create(options)); + return new ProblemDetailsDefaultWriter(Options.Create(options)); } } diff --git a/src/Http/ProblemDetails/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs similarity index 93% rename from src/Http/ProblemDetails/test/ProblemDetailsServiceCollectionExtensionsTest.cs rename to src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs index 1775deea24f2..a98d42733e84 100644 --- a/src/Http/ProblemDetails/test/ProblemDetailsServiceCollectionExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Moq; -namespace Microsoft.AspNetCore.Http.Tests; +namespace Microsoft.AspNetCore.Http.Extensions.Tests; public class ProblemDetailsServiceCollectionExtensionsTest { @@ -20,7 +20,7 @@ public void AddProblemDetails_AddsNeededServices() // Assert Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsService) && sd.ImplementationType == typeof(ProblemDetailsService)); - Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsWriter) && sd.ImplementationType == typeof(DefaultProblemDetailsWriter)); + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsWriter) && sd.ImplementationType == typeof(ProblemDetailsDefaultWriter)); } [Fact] diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs new file mode 100644 index 000000000000..02f1323c82a3 --- /dev/null +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs @@ -0,0 +1,135 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class ProblemDetailsServiceTest +{ + [Fact] + public async Task WriteAsync_Skip_NextWriters_WhenResponseAlreadyStarted() + { + // Arrange + var service = CreateService( + writers: new List + { + new MetadataBasedWriter("FirstWriter", canWrite: false), + new MetadataBasedWriter("SecondWriter"), + new MetadataBasedWriter("FirstWriter"), + }); + + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + }; + + // Act + await service.WriteAsync(new() { HttpContext = context, AdditionalMetadata = metadata }); + + // Assert + Assert.Equal("\"SecondWriter\"", Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public async Task WriteAsync_Skip_WhenNoWriterRegistered() + { + // Arrange + var service = CreateService(); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + }; + + // Act + await service.WriteAsync(new() { HttpContext = context }); + + // Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public async Task WriteAsync_Skip_WhenNoWriterCanWrite() + { + // Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + }; + + // Act + await service.WriteAsync(new() { HttpContext = context }); + + // Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); + } + + [Theory] + [InlineData(StatusCodes.Status100Continue)] + [InlineData(StatusCodes.Status200OK)] + [InlineData(StatusCodes.Status300MultipleChoices)] + [InlineData(399)] + public async Task WriteAsync_Skip_WhenSuccessStatusCode(int statusCode) + { + // Arrange + var service = CreateService( + writers: new List { new MetadataBasedWriter() }); + var stream = new MemoryStream(); + var context = new DefaultHttpContext() + { + Response = { Body = stream, StatusCode = statusCode }, + }; + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); + context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); + + // Act + await service.WriteAsync(new() { HttpContext = context }); + + // Assert + Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); + } + + private static ProblemDetailsService CreateService( + IEnumerable writers = null) + { + writers ??= Array.Empty(); + return new ProblemDetailsService(writers); + } + + private class SampleMetadata + { + public string ContentType { get; set; } + } + + private class MetadataBasedWriter : IProblemDetailsWriter + { + private readonly string _content; + private readonly bool _canWrite; + + public MetadataBasedWriter(string content = "Content", bool canWrite = true) + { + _content = content; + _canWrite = canWrite; + } + + public async ValueTask TryWriteAsync(ProblemDetailsContext context) + { + var metadata = context.AdditionalMetadata?.GetMetadata() ?? + context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + if (metadata != null && _canWrite) + { + await context.HttpContext.Response.WriteAsJsonAsync(_content); + return true; + } + + return false; + } + } +} diff --git a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs b/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs deleted file mode 100644 index 5671885520b2..000000000000 --- a/src/Http/ProblemDetails/src/DefaultProblemDetailsWriter.cs +++ /dev/null @@ -1,33 +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.Serialization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -internal sealed partial class DefaultProblemDetailsWriter : IProblemDetailsWriter -{ - private readonly ProblemDetailsOptions _options; - - public DefaultProblemDetailsWriter(IOptions options) - { - _options = options.Value; - } - - public async ValueTask WriteAsync(ProblemDetailsContext context) - { - var httpContext = context.HttpContext; - - ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); - _options.ConfigureDetails?.Invoke(httpContext, context.ProblemDetails); - - await httpContext.Response.WriteAsJsonAsync(context.ProblemDetails, typeof(ProblemDetails), ProblemDetailsJsonContext.Default, contentType: "application/problem+json"); - return httpContext.Response.HasStarted; - } - - [JsonSerializable(typeof(ProblemDetails))] - internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext - { } -} diff --git a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs b/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs deleted file mode 100644 index 0e52cd130668..000000000000 --- a/src/Http/ProblemDetails/src/DependencyInjection/ProblemDetailsOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http; - -using Microsoft.AspNetCore.Mvc; - -/// -/// Options for controlling the behavior of -/// and similar methods. -/// -public class ProblemDetailsOptions -{ - /// - /// Controls the ProblemDetails types allowed when auto-generating the response payload. - /// - /// - /// Defaults to . - /// - public ProblemDetailsTypes AllowedProblemTypes { get; set; } = ProblemDetailsTypes.All; - - /// - /// The operation that configures the current instance. - /// - public Action? ConfigureDetails { get; set; } -} diff --git a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj b/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj deleted file mode 100644 index d43064f94702..000000000000 --- a/src/Http/ProblemDetails/src/Microsoft.AspNetCore.Http.ProblemDetails.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - ASP.NET Core. - $(DefaultNetCoreTargetFramework) - true - true - aspnetcore - false - true - - - - - - - - - - - - - - - - - diff --git a/src/Http/ProblemDetails/src/ProblemDetailsService.cs b/src/Http/ProblemDetails/src/ProblemDetailsService.cs deleted file mode 100644 index 1a7d6e9ca748..000000000000 --- a/src/Http/ProblemDetails/src/ProblemDetailsService.cs +++ /dev/null @@ -1,99 +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.Linq; -using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -internal sealed class ProblemDetailsService : IProblemDetailsService -{ - private readonly IProblemDetailsWriter[] _writers; - private readonly ProblemDetailsOptions _options; - - public ProblemDetailsService( - IEnumerable writers, - IOptions options) - { - _writers = writers.ToArray(); - _options = options.Value; - } - - public async ValueTask WriteAsync(ProblemDetailsContext context) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.ProblemDetails); - ArgumentNullException.ThrowIfNull(context.HttpContext); - - if (_options.AllowedProblemTypes != ProblemDetailsTypes.None && _writers is { Length: > 0 }) - { - var problemStatusCode = context.ProblemDetails?.Status ?? context.HttpContext.Response.StatusCode; - var calculatedProblemType = CalculateProblemType(problemStatusCode, context.HttpContext.GetEndpoint()?.Metadata, context.AdditionalMetadata); - - if ((_options.AllowedProblemTypes & calculatedProblemType) != ProblemDetailsTypes.None) - { - var counter = 0; - var responseHasStarted = context.HttpContext.Response.HasStarted; - - while (counter < _writers.Length && !responseHasStarted) - { - responseHasStarted = await _writers[counter].WriteAsync(context); - counter++; - } - } - } - } - - // internal for testing - internal static ProblemDetailsTypes CalculateProblemType( - int statusCode, - EndpointMetadataCollection? metadataCollection, - EndpointMetadataCollection? additionalMetadata) - { - if (statusCode < 400) - { - return ProblemDetailsTypes.None; - } - - ProblemDetailsTypes? statusCodeProblemType = null; - ProblemDetailsTypes? generalProblemType = null; - - void SetProblemType(IProblemDetailsMetadata metadata) - { - if (!metadata.StatusCode.HasValue) - { - generalProblemType = metadata.ProblemType; - } - else if (statusCode == metadata.StatusCode) - { - statusCodeProblemType = metadata.ProblemType; - } - } - - if (metadataCollection?.GetOrderedMetadata() is { Count: > 0 } problemDetailsCollection) - { - for (var i = 0; i < problemDetailsCollection.Count; i++) - { - SetProblemType(problemDetailsCollection[i]); - } - } - - if (additionalMetadata?.GetOrderedMetadata() is { Count: > 0 } additionalProblemDetailsCollection) - { - for (var i = 0; i < additionalProblemDetailsCollection.Count; i++) - { - SetProblemType(additionalProblemDetailsCollection[i]); - } - } - - var problemTypeFromMetadata = statusCodeProblemType ?? generalProblemType ?? ProblemDetailsTypes.None; - var expectedProblemType = statusCode >= 500 ? ProblemDetailsTypes.Server : ProblemDetailsTypes.Client; - - var problemType = problemTypeFromMetadata & expectedProblemType; - return problemType != ProblemDetailsTypes.None ? - problemType : - // We need to special case Routing, since it could generate any status code - problemTypeFromMetadata & ProblemDetailsTypes.Routing; - } -} diff --git a/src/Http/ProblemDetails/src/PublicAPI.Shipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Shipped.txt deleted file mode 100644 index 7dc5c58110bf..000000000000 --- a/src/Http/ProblemDetails/src/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt b/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt deleted file mode 100644 index a518e81233d5..000000000000 --- a/src/Http/ProblemDetails/src/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable -Microsoft.AspNetCore.Http.ProblemDetailsOptions -Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedProblemTypes.get -> Microsoft.AspNetCore.Http.ProblemDetailsTypes -Microsoft.AspNetCore.Http.ProblemDetailsOptions.AllowedProblemTypes.set -> void -Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.get -> System.Action? -Microsoft.AspNetCore.Http.ProblemDetailsOptions.ConfigureDetails.set -> void -Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void -Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj b/src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj deleted file mode 100644 index 94c17bd2261e..000000000000 --- a/src/Http/ProblemDetails/test/Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - $(DefaultNetCoreTargetFramework) - - - - - - - - diff --git a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs b/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs deleted file mode 100644 index d62e45926da8..000000000000 --- a/src/Http/ProblemDetails/test/ProblemDetailsServiceTest.cs +++ /dev/null @@ -1,285 +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.Http.Metadata; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http.Tests; - -public class ProblemDetailsServiceTest -{ - [Theory] - [InlineData(100)] - [InlineData(200)] - [InlineData(400)] - [InlineData(500)] - public void CalculateProblemType_IsNone_WhenNoMetadata(int statusCode) - { - // Arrange & Act - var problemType = ProblemDetailsService.CalculateProblemType( - statusCode, - metadataCollection: EndpointMetadataCollection.Empty, - additionalMetadata: EndpointMetadataCollection.Empty); - - // Assert - Assert.Equal(ProblemDetailsTypes.None, problemType); - } - - [Theory] - [InlineData(100, ProblemDetailsTypes.All)] - [InlineData(200, ProblemDetailsTypes.All)] - [InlineData(400, ProblemDetailsTypes.Server)] - [InlineData(400, ProblemDetailsTypes.None)] - [InlineData(500, ProblemDetailsTypes.Client)] - [InlineData(500, ProblemDetailsTypes.None)] - public void CalculateProblemType_IsNone_WhenNotAllowed(int statusCode, ProblemDetailsTypes metadataProblemType) - { - // Arrange & Act - var problemType = ProblemDetailsService.CalculateProblemType( - statusCode, - metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(metadataProblemType)), - additionalMetadata: EndpointMetadataCollection.Empty); - - // Assert - Assert.Equal(ProblemDetailsTypes.None, problemType); - } - - [Theory] - [InlineData(400)] - [InlineData(500)] - public void CalculateProblemType_CanBeRouting_ForAllStatusCode(int statusCode) - { - // Arrange & Act - var problemType = ProblemDetailsService.CalculateProblemType( - statusCode, - metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(ProblemDetailsTypes.Routing)), - additionalMetadata: EndpointMetadataCollection.Empty); - - // Assert - Assert.Equal(ProblemDetailsTypes.Routing, problemType); - } - - [Theory] - [InlineData(400, ProblemDetailsTypes.Client)] - [InlineData(400, ProblemDetailsTypes.Routing)] - [InlineData(500, ProblemDetailsTypes.Server)] - [InlineData(500, ProblemDetailsTypes.Routing)] - public void CalculateProblemType_IsCorrect_WhenMetadata_WithStatusCode(int statusCode, ProblemDetailsTypes metadataProblemType) - { - // Arrange & Act - var problemType = ProblemDetailsService.CalculateProblemType( - statusCode, - metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, metadataProblemType)), - additionalMetadata: EndpointMetadataCollection.Empty); - - // Assert - Assert.Equal(metadataProblemType, problemType); - } - - [Fact] - public void CalculateProblemType_PrefersAdditionalMetadata() - { - // Arrange & Act - var statusCode = StatusCodes.Status400BadRequest; - var problemType = ProblemDetailsService.CalculateProblemType( - statusCode, - metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, ProblemDetailsTypes.Client)), - additionalMetadata: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, ProblemDetailsTypes.Routing))); - - // Assert - Assert.Equal(ProblemDetailsTypes.Routing, problemType); - } - - [Fact] - public void CalculateProblemType_PrefersMetadataWithStatusCode() - { - // Arrange & Act - var statusCode = StatusCodes.Status400BadRequest; - var problemType = ProblemDetailsService.CalculateProblemType( - statusCode, - metadataCollection: new EndpointMetadataCollection(new TestProblemMetadata(statusCode, ProblemDetailsTypes.Client)), - additionalMetadata: new EndpointMetadataCollection(new TestProblemMetadata(ProblemDetailsTypes.Routing))); - - // Assert - Assert.Equal(ProblemDetailsTypes.Client, problemType); - } - - [Fact] - public async Task WriteAsync_Skip_NextWriters_WhenResponseAlreadyStarted() - { - // Arrange - var service = CreateService( - writers: new List - { - new MetadataBasedWriter("FirstWriter", canWrite: false), - new MetadataBasedWriter("SecondWriter"), - new MetadataBasedWriter("FirstWriter"), - }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.Client | ProblemDetailsTypes.Server }); - - var metadata = new EndpointMetadataCollection( - new SampleMetadata() { ContentType = "application/problem+json" }, - new TestProblemMetadata()); - var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - }; - - // Act - await service.WriteAsync(new ProblemDetailsContext(context) { AdditionalMetadata = metadata }); - - // Assert - Assert.Equal("\"SecondWriter\"", Encoding.UTF8.GetString(stream.ToArray())); - } - - [Fact] - public async Task WriteAsync_Skip_WhenNoWriterRegistered() - { - // Arrange - var service = CreateService( - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - }; - - // Act - await service.WriteAsync(new ProblemDetailsContext(context)); - - // Assert - Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); - } - - [Fact] - public async Task WriteAsync_Skip_WhenNoWriterSelected() - { - // Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - }; - - // Act - await service.WriteAsync(new ProblemDetailsContext(context)); - - // Assert - Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); - } - - [Fact] - public async Task WriteAsync_Skip_WhenNotEnabled() - { - // Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.None }); - var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - }; - var metadata = new EndpointMetadataCollection( - new SampleMetadata() { ContentType = "application/problem+json" }, - new TestProblemMetadata(context.Response.StatusCode, ProblemDetailsTypes.All)); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - - // Act - await service.WriteAsync(new ProblemDetailsContext(context)); - - // Assert - Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); - } - - [Fact] - public async Task WriteAsync_Skip_WhenNotAllowed() - { - // Arrange - var service = CreateService( - writers: new List { new MetadataBasedWriter() }, - options: new ProblemDetailsOptions() { AllowedProblemTypes = ProblemDetailsTypes.All }); - var stream = new MemoryStream(); - var context = new DefaultHttpContext() - { - Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - }; - var metadata = new EndpointMetadataCollection( - new SampleMetadata() { ContentType = "application/problem+json" }, - new TestProblemMetadata(context.Response.StatusCode, ProblemDetailsTypes.None)); - context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null)); - - // Act - await service.WriteAsync(new ProblemDetailsContext(context)); - - // Assert - Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray())); - } - - private static ProblemDetailsService CreateService( - ProblemDetailsOptions options, - IEnumerable writers = null) - { - writers ??= Array.Empty(); - return new ProblemDetailsService(writers, Options.Create(options)); - } - - private class SampleMetadata - { - public string ContentType { get; set; } - } - - private class MetadataBasedWriter : IProblemDetailsWriter - { - private readonly string _content; - private readonly bool _canWrite; - - public MetadataBasedWriter(string content = "Content", bool canWrite = true) - { - _content = content; - _canWrite = canWrite; - } - - public async ValueTask WriteAsync(ProblemDetailsContext context) - { - var metadata = context.AdditionalMetadata?.GetMetadata() ?? - context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - - if (metadata != null && _canWrite) - { - await context.HttpContext.Response.WriteAsJsonAsync(_content); - return true; - } - - return false; - } - } - - private class TestProblemMetadata : IProblemDetailsMetadata - { - public TestProblemMetadata() - { - ProblemType = ProblemDetailsTypes.All; - } - - public TestProblemMetadata(ProblemDetailsTypes problemTypes) - { - ProblemType = problemTypes; - } - - public TestProblemMetadata(int status, ProblemDetailsTypes problemTypes) - { - StatusCode = status; - ProblemType = problemTypes; - } - - public int? StatusCode { get;} - - public ProblemDetailsTypes ProblemType { get; } - } -} diff --git a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs index 9e8bfe7e1635..87931a0306f0 100644 --- a/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs @@ -264,7 +264,7 @@ private static Endpoint CreateRejectionEndpoint() context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; return Task.CompletedTask; }, - new EndpointMetadataCollection(new RoutingProblemMetadata(StatusCodes.Status415UnsupportedMediaType)), + EndpointMetadataCollection.Empty, Http415EndpointDisplayName); } diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 0c26b2b8debf..cea2791414da 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -16,8 +15,6 @@ namespace Microsoft.AspNetCore.Routing.Matching; /// public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { - private static readonly EndpointMetadataCollection Http405EndpointMetadata = new(new RoutingProblemMetadata(StatusCodes.Status405MethodNotAllowed)); - // Used in tests internal static readonly string PreflightHttpMethod = HttpMethods.Options; @@ -413,7 +410,7 @@ private static Endpoint CreateRejectionEndpoint(IEnumerable? httpMethods context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; return Task.CompletedTask; }, - Http405EndpointMetadata, + EndpointMetadataCollection.Empty, Http405EndpointDisplayName); } diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 8739aa10ce1a..0e1f47d2ab67 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -32,7 +32,6 @@ - diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs index bd77359b3112..cd42758a107a 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder; @@ -28,7 +26,7 @@ public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBui throw new ArgumentNullException(nameof(app)); } - return SetMiddleware(app); + return app.UseMiddleware(); } /// @@ -54,25 +52,6 @@ public static IApplicationBuilder UseDeveloperExceptionPage( throw new ArgumentNullException(nameof(options)); } - return SetMiddleware(app, options); - } - - private static IApplicationBuilder SetMiddleware( - IApplicationBuilder app, - DeveloperExceptionPageOptions? options = null) - { - var problemDetailsService = app.ApplicationServices.GetService(); - - if (options is null) - { - return problemDetailsService is null ? - app.UseMiddleware() : - app.UseMiddleware(problemDetailsService); - } - - return problemDetailsService is null ? - app.UseMiddleware(Options.Create(options)) : - app.UseMiddleware(Options.Create(options), problemDetailsService); - + return app.UseMiddleware(Options.Create(options)); } } diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index eb12655f7f9a..5ac530518fd9 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -2,22 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.RazorViews; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.StackTrace.Sources; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Diagnostics; @@ -26,15 +15,7 @@ namespace Microsoft.AspNetCore.Diagnostics; /// public class DeveloperExceptionPageMiddleware { - private readonly RequestDelegate _next; - private readonly DeveloperExceptionPageOptions _options; - private readonly ILogger _logger; - private readonly IFileProvider _fileProvider; - private readonly DiagnosticSource _diagnosticSource; - private readonly ExceptionDetailsProvider _exceptionDetailsProvider; - private readonly Func _exceptionHandler; - private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); - private readonly IProblemDetailsService? _problemDetailsService; + private readonly DeveloperExceptionPageMiddlewareImpl _innerMiddlewareImpl; /// /// Initializes a new instance of the class @@ -52,58 +33,15 @@ public DeveloperExceptionPageMiddleware( IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable filters) - : this(next, options, loggerFactory, hostingEnvironment, diagnosticSource, filters, problemDetailsService: null) { - } - - /// - /// Initializes a new instance of the class - /// - /// The representing the next middleware in the pipeline. - /// The options for configuring the middleware. - /// The used for logging. - /// - /// The used for writing diagnostic messages. - /// The list of registered . - /// The used for writing messages. - public DeveloperExceptionPageMiddleware( - RequestDelegate next, - IOptions options, - ILoggerFactory loggerFactory, - IWebHostEnvironment hostingEnvironment, - DiagnosticSource diagnosticSource, - IEnumerable filters, - IProblemDetailsService? problemDetailsService) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (filters == null) - { - throw new ArgumentNullException(nameof(filters)); - } - - _next = next; - _options = options.Value; - _logger = loggerFactory.CreateLogger(); - _fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider; - _diagnosticSource = diagnosticSource; - _exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _logger, _options.SourceCodeLineCount); - _exceptionHandler = DisplayException; - _problemDetailsService = problemDetailsService; - - foreach (var filter in filters.Reverse()) - { - var nextFilter = _exceptionHandler; - _exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter); - } + _innerMiddlewareImpl = new( + next, + options, + loggerFactory, + hostingEnvironment, + diagnosticSource, + filters, + problemDetailsService: null); } /// @@ -111,246 +49,6 @@ public DeveloperExceptionPageMiddleware( /// /// /// - public async Task Invoke(HttpContext context) - { - try - { - await _next(context); - } - catch (Exception ex) - { - _logger.UnhandledException(ex); - - if (context.Response.HasStarted) - { - _logger.ResponseStartedErrorPageMiddleware(); - throw; - } - - try - { - context.Response.Clear(); - - // Preserve the status code that would have been written by the server automatically when a BadHttpRequestException is thrown. - if (ex is BadHttpRequestException badHttpRequestException) - { - context.Response.StatusCode = badHttpRequestException.StatusCode; - } - else - { - context.Response.StatusCode = 500; - } - - await _exceptionHandler(new ErrorContext(context, ex)); - - const string eventName = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; - if (_diagnosticSource.IsEnabled(eventName)) - { - WriteDiagnosticEvent(_diagnosticSource, eventName, new { httpContext = context, exception = ex }); - } - - return; - } - catch (Exception ex2) - { - // If there's a Exception while generating the error page, re-throw the original exception. - _logger.DisplayErrorPageException(ex2); - } - throw; - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", - Justification = "The values being passed into Write have the commonly used properties being preserved with DynamicDependency.")] - static void WriteDiagnosticEvent(DiagnosticSource diagnosticSource, string name, TValue value) - => diagnosticSource.Write(name, value); - } - - // Assumes the response headers have not been sent. If they have, still attempt to write to the body. - private Task DisplayException(ErrorContext errorContext) - { - var httpContext = errorContext.HttpContext; - var headers = httpContext.Request.GetTypedHeaders(); - var acceptHeader = headers.Accept; - - // If the client does not ask for HTML just format the exception as plain text - if (acceptHeader == null || !acceptHeader.Any(h => h.IsSubsetOf(_textHtmlMediaType))) - { - return DisplayExceptionContent(errorContext); - } - - if (errorContext.Exception is ICompilationException compilationException) - { - return DisplayCompilationException(httpContext, compilationException); - } - - return DisplayRuntimeException(httpContext, errorContext.Exception); - } - - private async Task DisplayExceptionContent(ErrorContext errorContext) - { - var httpContext = errorContext.HttpContext; - - if (_problemDetailsService != null) - { - var problemDetails = new ProblemDetails - { - Title = TypeNameHelper.GetTypeDisplayName(errorContext.Exception.GetType()), - Detail = errorContext.Exception.Message, - Status = httpContext.Response.StatusCode - }; - - var exceptionDetails = _exceptionDetailsProvider.GetDetails(errorContext.Exception); - problemDetails.Extensions["exception"] = new - { - Details = exceptionDetails.Select(d => new - { - Message = d.ErrorMessage ?? d.Error?.Message, - Type = TypeNameHelper.GetTypeDisplayName(d.Error), - StackFrames = d.StackFrames, - }), - Headers = httpContext.Request.Headers, - Error = errorContext.Exception.ToString(), - Path = httpContext.Request.Path, - Endpoint = httpContext.GetEndpoint()?.ToString(), - RouteValues = httpContext.Features.Get()?.RouteValues, - }; - - await _problemDetailsService.WriteAsync(new ProblemDetailsContext(httpContext) - { - ProblemDetails = problemDetails - }); - } - - // If the response has not started, assume the problem details was not written. - if (!httpContext.Response.HasStarted) - { - httpContext.Response.ContentType = "text/plain; charset=utf-8"; - - var sb = new StringBuilder(); - sb.AppendLine(errorContext.Exception.ToString()); - sb.AppendLine(); - sb.AppendLine("HEADERS"); - sb.AppendLine("======="); - foreach (var pair in httpContext.Request.Headers) - { - sb.AppendLine(FormattableString.Invariant($"{pair.Key}: {pair.Value}")); - } - - await httpContext.Response.WriteAsync(sb.ToString()); - } - } - - private Task DisplayCompilationException( - HttpContext context, - ICompilationException compilationException) - { - var model = new CompilationErrorPageModel(_options); - - var errorPage = new CompilationErrorPage(model); - - if (compilationException.CompilationFailures == null) - { - return errorPage.ExecuteAsync(context); - } - - foreach (var compilationFailure in compilationException.CompilationFailures) - { - if (compilationFailure == null) - { - continue; - } - - var stackFrames = new List(); - var exceptionDetails = new ExceptionDetails(compilationFailure.FailureSummary!, stackFrames); - model.ErrorDetails.Add(exceptionDetails); - model.CompiledContent.Add(compilationFailure.CompiledContent); - - if (compilationFailure.Messages == null) - { - continue; - } - - var sourceLines = compilationFailure - .SourceFileContent? - .Split(new[] { Environment.NewLine }, StringSplitOptions.None); - - foreach (var item in compilationFailure.Messages) - { - if (item == null) - { - continue; - } - - var frame = new StackFrameSourceCodeInfo - { - File = compilationFailure.SourceFilePath, - Line = item.StartLine, - Function = string.Empty - }; - - if (sourceLines != null) - { - _exceptionDetailsProvider.ReadFrameContent(frame, sourceLines, item.StartLine, item.EndLine); - } - - frame.ErrorDetails = item.Message; - - stackFrames.Add(frame); - } - } - - return errorPage.ExecuteAsync(context); - } - - private Task DisplayRuntimeException(HttpContext context, Exception ex) - { - var endpoint = context.GetEndpoint(); - - EndpointModel? endpointModel = null; - if (endpoint != null) - { - endpointModel = new EndpointModel(); - endpointModel.DisplayName = endpoint.DisplayName; - - if (endpoint is RouteEndpoint routeEndpoint) - { - endpointModel.RoutePattern = routeEndpoint.RoutePattern.RawText; - endpointModel.Order = routeEndpoint.Order; - - var httpMethods = endpoint.Metadata.GetMetadata()?.HttpMethods; - if (httpMethods != null) - { - endpointModel.HttpMethods = string.Join(", ", httpMethods); - } - } - } - - var request = context.Request; - var title = Resources.ErrorPageHtml_Title; - - if (ex is BadHttpRequestException badHttpRequestException) - { - var badRequestReasonPhrase = WebUtilities.ReasonPhrases.GetReasonPhrase(badHttpRequestException.StatusCode); - - if (!string.IsNullOrEmpty(badRequestReasonPhrase)) - { - title = badRequestReasonPhrase; - } - } - - var model = new ErrorPageModel - { - Options = _options, - ErrorDetails = _exceptionDetailsProvider.GetDetails(ex), - Query = request.Query, - Cookies = request.Cookies, - Headers = request.Headers, - RouteValues = request.RouteValues, - Endpoint = endpointModel, - Title = title, - }; - - var errorPage = new ErrorPage(model); - return errorPage.ExecuteAsync(context); - } + public Task Invoke(HttpContext context) + => _innerMiddlewareImpl.Invoke(context); } diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs new file mode 100644 index 000000000000..595ecabda93e --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs @@ -0,0 +1,337 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.RazorViews; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.StackTrace.Sources; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Diagnostics; + +/// +/// Captures synchronous and asynchronous exceptions from the pipeline and generates error responses. +/// +internal class DeveloperExceptionPageMiddlewareImpl +{ + private readonly RequestDelegate _next; + private readonly DeveloperExceptionPageOptions _options; + private readonly ILogger _logger; + private readonly IFileProvider _fileProvider; + private readonly DiagnosticSource _diagnosticSource; + private readonly ExceptionDetailsProvider _exceptionDetailsProvider; + private readonly Func _exceptionHandler; + private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); + private readonly IProblemDetailsService? _problemDetailsService; + + /// + /// Initializes a new instance of the class + /// + /// The representing the next middleware in the pipeline. + /// The options for configuring the middleware. + /// The used for logging. + /// + /// The used for writing diagnostic messages. + /// The list of registered . + /// The used for writing messages. + public DeveloperExceptionPageMiddlewareImpl( + RequestDelegate next, + IOptions options, + ILoggerFactory loggerFactory, + IWebHostEnvironment hostingEnvironment, + DiagnosticSource diagnosticSource, + IEnumerable filters, + IProblemDetailsService? problemDetailsService = null) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (filters == null) + { + throw new ArgumentNullException(nameof(filters)); + } + + _next = next; + _options = options.Value; + _logger = loggerFactory.CreateLogger(); + _fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider; + _diagnosticSource = diagnosticSource; + _exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _logger, _options.SourceCodeLineCount); + _exceptionHandler = DisplayException; + _problemDetailsService = problemDetailsService; + + foreach (var filter in filters.Reverse()) + { + var nextFilter = _exceptionHandler; + _exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter); + } + } + + /// + /// Process an individual request. + /// + /// + /// + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.UnhandledException(ex); + + if (context.Response.HasStarted) + { + _logger.ResponseStartedErrorPageMiddleware(); + throw; + } + + try + { + context.Response.Clear(); + + // Preserve the status code that would have been written by the server automatically when a BadHttpRequestException is thrown. + if (ex is BadHttpRequestException badHttpRequestException) + { + context.Response.StatusCode = badHttpRequestException.StatusCode; + } + else + { + context.Response.StatusCode = 500; + } + + await _exceptionHandler(new ErrorContext(context, ex)); + + const string eventName = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; + if (_diagnosticSource.IsEnabled(eventName)) + { + WriteDiagnosticEvent(_diagnosticSource, eventName, new { httpContext = context, exception = ex }); + } + + return; + } + catch (Exception ex2) + { + // If there's a Exception while generating the error page, re-throw the original exception. + _logger.DisplayErrorPageException(ex2); + } + throw; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "The values being passed into Write have the commonly used properties being preserved with DynamicDependency.")] + static void WriteDiagnosticEvent(DiagnosticSource diagnosticSource, string name, TValue value) + => diagnosticSource.Write(name, value); + } + + // Assumes the response headers have not been sent. If they have, still attempt to write to the body. + private Task DisplayException(ErrorContext errorContext) + { + var httpContext = errorContext.HttpContext; + var headers = httpContext.Request.GetTypedHeaders(); + var acceptHeader = headers.Accept; + + // If the client does not ask for HTML just format the exception as plain text + if (acceptHeader == null || !acceptHeader.Any(h => h.IsSubsetOf(_textHtmlMediaType))) + { + return DisplayExceptionContent(errorContext); + } + + if (errorContext.Exception is ICompilationException compilationException) + { + return DisplayCompilationException(httpContext, compilationException); + } + + return DisplayRuntimeException(httpContext, errorContext.Exception); + } + + private async Task DisplayExceptionContent(ErrorContext errorContext) + { + var httpContext = errorContext.HttpContext; + + if (_problemDetailsService != null) + { + var problemDetails = new ProblemDetails + { + Title = TypeNameHelper.GetTypeDisplayName(errorContext.Exception.GetType()), + Detail = errorContext.Exception.Message, + Status = httpContext.Response.StatusCode + }; + + var exceptionDetails = _exceptionDetailsProvider.GetDetails(errorContext.Exception); + problemDetails.Extensions["exception"] = new + { + Details = exceptionDetails.Select(d => new + { + Message = d.ErrorMessage ?? d.Error?.Message, + Type = TypeNameHelper.GetTypeDisplayName(d.Error), + StackFrames = d.StackFrames, + }), + Headers = httpContext.Request.Headers, + Error = errorContext.Exception.ToString(), + Path = httpContext.Request.Path, + Endpoint = httpContext.GetEndpoint()?.ToString(), + RouteValues = httpContext.Features.Get()?.RouteValues, + }; + + await _problemDetailsService.WriteAsync(new() + { + HttpContext = httpContext, + ProblemDetails = problemDetails + }); + } + + // If the response has not started, assume the problem details was not written. + if (!httpContext.Response.HasStarted) + { + httpContext.Response.ContentType = "text/plain; charset=utf-8"; + + var sb = new StringBuilder(); + sb.AppendLine(errorContext.Exception.ToString()); + sb.AppendLine(); + sb.AppendLine("HEADERS"); + sb.AppendLine("======="); + foreach (var pair in httpContext.Request.Headers) + { + sb.AppendLine(FormattableString.Invariant($"{pair.Key}: {pair.Value}")); + } + + await httpContext.Response.WriteAsync(sb.ToString()); + } + } + + private Task DisplayCompilationException( + HttpContext context, + ICompilationException compilationException) + { + var model = new CompilationErrorPageModel(_options); + + var errorPage = new CompilationErrorPage(model); + + if (compilationException.CompilationFailures == null) + { + return errorPage.ExecuteAsync(context); + } + + foreach (var compilationFailure in compilationException.CompilationFailures) + { + if (compilationFailure == null) + { + continue; + } + + var stackFrames = new List(); + var exceptionDetails = new ExceptionDetails(compilationFailure.FailureSummary!, stackFrames); + model.ErrorDetails.Add(exceptionDetails); + model.CompiledContent.Add(compilationFailure.CompiledContent); + + if (compilationFailure.Messages == null) + { + continue; + } + + var sourceLines = compilationFailure + .SourceFileContent? + .Split(new[] { Environment.NewLine }, StringSplitOptions.None); + + foreach (var item in compilationFailure.Messages) + { + if (item == null) + { + continue; + } + + var frame = new StackFrameSourceCodeInfo + { + File = compilationFailure.SourceFilePath, + Line = item.StartLine, + Function = string.Empty + }; + + if (sourceLines != null) + { + _exceptionDetailsProvider.ReadFrameContent(frame, sourceLines, item.StartLine, item.EndLine); + } + + frame.ErrorDetails = item.Message; + + stackFrames.Add(frame); + } + } + + return errorPage.ExecuteAsync(context); + } + + private Task DisplayRuntimeException(HttpContext context, Exception ex) + { + var endpoint = context.GetEndpoint(); + + EndpointModel? endpointModel = null; + if (endpoint != null) + { + endpointModel = new EndpointModel(); + endpointModel.DisplayName = endpoint.DisplayName; + + if (endpoint is RouteEndpoint routeEndpoint) + { + endpointModel.RoutePattern = routeEndpoint.RoutePattern.RawText; + endpointModel.Order = routeEndpoint.Order; + + var httpMethods = endpoint.Metadata.GetMetadata()?.HttpMethods; + if (httpMethods != null) + { + endpointModel.HttpMethods = string.Join(", ", httpMethods); + } + } + } + + var request = context.Request; + var title = Resources.ErrorPageHtml_Title; + + if (ex is BadHttpRequestException badHttpRequestException) + { + var badRequestReasonPhrase = WebUtilities.ReasonPhrases.GetReasonPhrase(badHttpRequestException.StatusCode); + + if (!string.IsNullOrEmpty(badRequestReasonPhrase)) + { + title = badRequestReasonPhrase; + } + } + + var model = new ErrorPageModel + { + Options = _options, + ErrorDetails = _exceptionDetailsProvider.GetDetails(ex), + Query = request.Query, + Cookies = request.Cookies, + Headers = request.Headers, + RouteValues = request.RouteValues, + Endpoint = endpointModel, + Title = title, + }; + + var errorPage = new ErrorPage(model); + return errorPage.ExecuteAsync(context); + } +} diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index 91d05a774e2f..d1857fc03245 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -133,19 +133,15 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui options.Value.ExceptionHandler = builder.Build(); } - return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener, problemDetailsService).Invoke; + return new ExceptionHandlerMiddlewareImpl(next, loggerFactory, options, diagnosticListener, problemDetailsService).Invoke; }); } if (options is null) { - return problemDetailsService is null ? - app.UseMiddleware() : - app.UseMiddleware(problemDetailsService); + return app.UseMiddleware(); } - return problemDetailsService is null ? - app.UseMiddleware(options) : - app.UseMiddleware(options, problemDetailsService); + return app.UseMiddleware(options); } } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs index cf390982786c..5b9f87514326 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -2,12 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -18,14 +14,7 @@ namespace Microsoft.AspNetCore.Diagnostics; /// public class ExceptionHandlerMiddleware { - private const int DefaultStatusCode = StatusCodes.Status500InternalServerError; - - private readonly RequestDelegate _next; - private readonly ExceptionHandlerOptions _options; - private readonly ILogger _logger; - private readonly Func _clearCacheHeadersDelegate; - private readonly DiagnosticListener _diagnosticListener; - private readonly IProblemDetailsService? _problemDetailsService; + private readonly ExceptionHandlerMiddlewareImpl _innerMiddlewareImpl; /// /// Creates a new @@ -39,46 +28,13 @@ public ExceptionHandlerMiddleware( ILoggerFactory loggerFactory, IOptions options, DiagnosticListener diagnosticListener) - : this(next, loggerFactory, options, diagnosticListener, problemDetailsService: null) { - } - - /// - /// Creates a new - /// - /// The representing the next middleware in the pipeline. - /// The used for logging. - /// The options for configuring the middleware. - /// The used for writing diagnostic messages. - /// The used for writing messages. - public ExceptionHandlerMiddleware( - RequestDelegate next, - ILoggerFactory loggerFactory, - IOptions options, - DiagnosticListener diagnosticListener, - IProblemDetailsService? problemDetailsService) - { - _next = next; - _options = options.Value; - _logger = loggerFactory.CreateLogger(); - _clearCacheHeadersDelegate = ClearCacheHeaders; - _diagnosticListener = diagnosticListener; - _problemDetailsService = problemDetailsService; - - if (_options.ExceptionHandler == null) - { - if (_options.ExceptionHandlingPath == null) - { - if (problemDetailsService == null) - { - throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); - } - } - else - { - _options.ExceptionHandler = _next; - } - } + _innerMiddlewareImpl = new ( + next, + loggerFactory, + options, + diagnosticListener, + problemDetailsService: null); } /// @@ -86,146 +42,5 @@ public ExceptionHandlerMiddleware( /// /// The for the current request. public Task Invoke(HttpContext context) - { - ExceptionDispatchInfo edi; - try - { - var task = _next(context); - if (!task.IsCompletedSuccessfully) - { - return Awaited(this, context, task); - } - - return Task.CompletedTask; - } - catch (Exception exception) - { - // Get the Exception, but don't continue processing in the catch block as its bad for stack usage. - edi = ExceptionDispatchInfo.Capture(exception); - } - - return HandleException(context, edi); - - static async Task Awaited(ExceptionHandlerMiddleware middleware, HttpContext context, Task task) - { - ExceptionDispatchInfo? edi = null; - try - { - await task; - } - catch (Exception exception) - { - // Get the Exception, but don't continue processing in the catch block as its bad for stack usage. - edi = ExceptionDispatchInfo.Capture(exception); - } - - if (edi != null) - { - await middleware.HandleException(context, edi); - } - } - } - - private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi) - { - _logger.UnhandledException(edi.SourceException); - // We can't do anything if the response has already started, just abort. - if (context.Response.HasStarted) - { - _logger.ResponseStartedErrorHandler(); - edi.Throw(); - } - - PathString originalPath = context.Request.Path; - if (_options.ExceptionHandlingPath.HasValue) - { - context.Request.Path = _options.ExceptionHandlingPath; - } - try - { - var exceptionHandlerFeature = new ExceptionHandlerFeature() - { - Error = edi.SourceException, - Path = originalPath.Value!, - Endpoint = context.GetEndpoint(), - RouteValues = context.Features.Get()?.RouteValues - }; - - ClearHttpContext(context); - - context.Features.Set(exceptionHandlerFeature); - context.Features.Set(exceptionHandlerFeature); - context.Response.StatusCode = DefaultStatusCode; - context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response); - - if (_options.ExceptionHandler != null) - { - await _options.ExceptionHandler!(context); - } - else - { - await _problemDetailsService!.WriteAsync(new ProblemDetailsContext(context) - { - AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata, - ProblemDetails = new ProblemDetails() { Status = DefaultStatusCode } - }); - } - - // If the response has already started, assume exception handler was successful. - if (context.Response.HasStarted || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response) - { - const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException"; - if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName)) - { - WriteDiagnosticEvent(_diagnosticListener, eventName, new { httpContext = context, exception = edi.SourceException }); - } - - return; - } - - edi = ExceptionDispatchInfo.Capture(new InvalidOperationException($"The exception handler configured on {nameof(ExceptionHandlerOptions)} produced a 404 status response. " + - $"This {nameof(InvalidOperationException)} containing the original exception was thrown since this is often due to a misconfigured {nameof(ExceptionHandlerOptions.ExceptionHandlingPath)}. " + - $"If the exception handler is expected to return 404 status responses then set {nameof(ExceptionHandlerOptions.AllowStatusCode404Response)} to true.", edi.SourceException)); - } - catch (Exception ex2) - { - // Suppress secondary exceptions, re-throw the original. - _logger.ErrorHandlerException(ex2); - } - finally - { - context.Request.Path = originalPath; - } - - edi.Throw(); // Re-throw wrapped exception or the original if we couldn't handle it - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", - Justification = "The values being passed into Write have the commonly used properties being preserved with DynamicDependency.")] - static void WriteDiagnosticEvent(DiagnosticSource diagnosticSource, string name, TValue value) - => diagnosticSource.Write(name, value); - } - - private static void ClearHttpContext(HttpContext context) - { - context.Response.Clear(); - - // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset - // the endpoint and route values to ensure things are re-calculated. - context.SetEndpoint(endpoint: null); - var routeValuesFeature = context.Features.Get(); - if (routeValuesFeature != null) - { - routeValuesFeature.RouteValues = null!; - } - } - - private static Task ClearCacheHeaders(object state) - { - var headers = ((HttpResponse)state).Headers; - headers.CacheControl = "no-cache,no-store"; - headers.Pragma = "no-cache"; - headers.Expires = "-1"; - headers.ETag = default; - return Task.CompletedTask; - } + => _innerMiddlewareImpl.Invoke(context); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs new file mode 100644 index 000000000000..48939e0e1f9a --- /dev/null +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs @@ -0,0 +1,216 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Diagnostics; + +/// +/// A middleware for handling exceptions in the application. +/// +internal class ExceptionHandlerMiddlewareImpl +{ + private const int DefaultStatusCode = StatusCodes.Status500InternalServerError; + + private readonly RequestDelegate _next; + private readonly ExceptionHandlerOptions _options; + private readonly ILogger _logger; + private readonly Func _clearCacheHeadersDelegate; + private readonly DiagnosticListener _diagnosticListener; + private readonly IProblemDetailsService? _problemDetailsService; + + /// + /// Creates a new + /// + /// The representing the next middleware in the pipeline. + /// The used for logging. + /// The options for configuring the middleware. + /// The used for writing diagnostic messages. + /// The used for writing messages. + public ExceptionHandlerMiddlewareImpl( + RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions options, + DiagnosticListener diagnosticListener, + IProblemDetailsService? problemDetailsService = null) + { + _next = next; + _options = options.Value; + _logger = loggerFactory.CreateLogger(); + _clearCacheHeadersDelegate = ClearCacheHeaders; + _diagnosticListener = diagnosticListener; + _problemDetailsService = problemDetailsService; + + if (_options.ExceptionHandler == null) + { + if (_options.ExceptionHandlingPath == null) + { + if (problemDetailsService == null) + { + throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); + } + } + else + { + _options.ExceptionHandler = _next; + } + } + } + + /// + /// Executes the middleware. + /// + /// The for the current request. + public Task Invoke(HttpContext context) + { + ExceptionDispatchInfo edi; + try + { + var task = _next(context); + if (!task.IsCompletedSuccessfully) + { + return Awaited(this, context, task); + } + + return Task.CompletedTask; + } + catch (Exception exception) + { + // Get the Exception, but don't continue processing in the catch block as its bad for stack usage. + edi = ExceptionDispatchInfo.Capture(exception); + } + + return HandleException(context, edi); + + static async Task Awaited(ExceptionHandlerMiddlewareImpl middleware, HttpContext context, Task task) + { + ExceptionDispatchInfo? edi = null; + try + { + await task; + } + catch (Exception exception) + { + // Get the Exception, but don't continue processing in the catch block as its bad for stack usage. + edi = ExceptionDispatchInfo.Capture(exception); + } + + if (edi != null) + { + await middleware.HandleException(context, edi); + } + } + } + + private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi) + { + _logger.UnhandledException(edi.SourceException); + // We can't do anything if the response has already started, just abort. + if (context.Response.HasStarted) + { + _logger.ResponseStartedErrorHandler(); + edi.Throw(); + } + + PathString originalPath = context.Request.Path; + if (_options.ExceptionHandlingPath.HasValue) + { + context.Request.Path = _options.ExceptionHandlingPath; + } + try + { + var exceptionHandlerFeature = new ExceptionHandlerFeature() + { + Error = edi.SourceException, + Path = originalPath.Value!, + Endpoint = context.GetEndpoint(), + RouteValues = context.Features.Get()?.RouteValues + }; + + ClearHttpContext(context); + + context.Features.Set(exceptionHandlerFeature); + context.Features.Set(exceptionHandlerFeature); + context.Response.StatusCode = DefaultStatusCode; + context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response); + + if (_options.ExceptionHandler != null) + { + await _options.ExceptionHandler!(context); + } + else + { + await _problemDetailsService!.WriteAsync(new () + { + HttpContext = context, + AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata, + ProblemDetails = { Status = DefaultStatusCode } + }); + } + + // If the response has already started, assume exception handler was successful. + if (context.Response.HasStarted || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response) + { + const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException"; + if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName)) + { + WriteDiagnosticEvent(_diagnosticListener, eventName, new { httpContext = context, exception = edi.SourceException }); + } + + return; + } + + edi = ExceptionDispatchInfo.Capture(new InvalidOperationException($"The exception handler configured on {nameof(ExceptionHandlerOptions)} produced a 404 status response. " + + $"This {nameof(InvalidOperationException)} containing the original exception was thrown since this is often due to a misconfigured {nameof(ExceptionHandlerOptions.ExceptionHandlingPath)}. " + + $"If the exception handler is expected to return 404 status responses then set {nameof(ExceptionHandlerOptions.AllowStatusCode404Response)} to true.", edi.SourceException)); + } + catch (Exception ex2) + { + // Suppress secondary exceptions, re-throw the original. + _logger.ErrorHandlerException(ex2); + } + finally + { + context.Request.Path = originalPath; + } + + edi.Throw(); // Re-throw wrapped exception or the original if we couldn't handle it + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "The values being passed into Write have the commonly used properties being preserved with DynamicDependency.")] + static void WriteDiagnosticEvent(DiagnosticSource diagnosticSource, string name, TValue value) + => diagnosticSource.Write(name, value); + } + + private static void ClearHttpContext(HttpContext context) + { + context.Response.Clear(); + + // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset + // the endpoint and route values to ensure things are re-calculated. + context.SetEndpoint(endpoint: null); + var routeValuesFeature = context.Features.Get(); + if (routeValuesFeature != null) + { + routeValuesFeature.RouteValues = null!; + } + } + + private static Task ClearCacheHeaders(object state) + { + var headers = ((HttpResponse)state).Headers; + headers.CacheControl = "no-cache,no-store"; + headers.Pragma = "no-cache"; + headers.Expires = "-1"; + headers.ETag = default; + return Task.CompletedTask; + } +} diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index 150107b9589b..ebf506b0a0ae 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index c41c78796a7b..eb10f3ac155a 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! hostingEnvironment, System.Diagnostics.DiagnosticSource! diagnosticSource, System.Collections.Generic.IEnumerable! filters, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService) -> void -Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.ExceptionHandlerMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options, System.Diagnostics.DiagnosticListener! diagnosticListener, Microsoft.AspNetCore.Http.IProblemDetailsService? problemDetailsService) -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.Endpoint.set -> void Microsoft.AspNetCore.Diagnostics.StatusCodeReExecuteFeature.RouteValues.get -> Microsoft.AspNetCore.Routing.RouteValueDictionary? diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs index 5eb8c16b38e9..496e02d763e3 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs @@ -4,8 +4,6 @@ using System.Globalization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -16,8 +14,6 @@ namespace Microsoft.AspNetCore.Builder; /// public class StatusCodePagesOptions { - private static readonly ProblemMetadata NotFoundProblemMetadata = new(StatusCodes.Status404NotFound, ProblemDetailsTypes.Routing | ProblemDetailsTypes.Client); - /// /// Creates a default which produces a plaintext response /// containing the status code and the reason phrase. @@ -30,10 +26,10 @@ public StatusCodePagesOptions() if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - await problemDetailsService.WriteAsync(new ProblemDetailsContext(context.HttpContext) + await problemDetailsService.WriteAsync(new () { - AdditionalMetadata = new EndpointMetadataCollection(NotFoundProblemMetadata), - ProblemDetails = new ProblemDetails() { Status = statusCode } + HttpContext = context.HttpContext, + ProblemDetails = { Status = statusCode } }); } diff --git a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs index 6cdc8be18299..ce65c8720234 100644 --- a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs +++ b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs @@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Mvc; using System.Net.Http.Json; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; +using System.Net.Http.Headers; namespace Microsoft.AspNetCore.Diagnostics.FunctionalTests; @@ -23,6 +25,7 @@ public async Task StatusCodePage_ShowsError() { // Arrange var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/errors/417"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); // Act var response = await Client.SendAsync(request); @@ -39,6 +42,7 @@ public async Task StatusCodePageOptions_ExcludesSemicolon_WhenReasonPhrase_IsUnk //Arrange var httpStatusCode = 541; var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost/?statuscode={httpStatusCode}"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); //Act var response = await Client.SendAsync(request); @@ -58,6 +62,7 @@ public async Task StatusCodePageOptions_IncludesSemicolon__AndReasonPhrase_WhenR //Arrange var httpStatusCode = 400; var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost/?statuscode={httpStatusCode}"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); //Act var response = await Client.SendAsync(request); @@ -78,7 +83,8 @@ public async Task StatusCodePage_ProducesProblemDetails() { // Arrange var httpStatusCode = 400; - var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost/status?includeProblemMetadata=true&statuscode={httpStatusCode}"); + var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost?statuscode={httpStatusCode}"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // Act var response = await Client.SendAsync(request); diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj index ca44620292a6..39fb247b9283 100644 --- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj +++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj @@ -9,14 +9,9 @@ - - - - - diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs index 4f15ba4bbeb2..c5fbdaf845ab 100644 --- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs @@ -28,8 +28,7 @@ public void Configure(IApplicationBuilder app) RoutePatternFactory.Parse("/"), 0, new EndpointMetadataCollection( - new HttpMethodMetadata(new[] { "GET", "POST" }), - new ProblemMetadata()), + new HttpMethodMetadata(new[] { "GET", "POST" })), "Endpoint display name"); context.SetEndpoint(endpoint); diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj index 002682a4ab76..2343d8e73752 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj @@ -6,15 +6,10 @@ - - - - - diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs index 67d44709be8d..ff6cb2ae2a33 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs @@ -16,13 +16,6 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { - // Add a problemMetadata to all requests - app.Use((context, next) => - { - context.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(new ProblemMetadata()), string.Empty)); - return next(context); - }); - // Configure the error handler to produces a ProblemDetails. app.UseExceptionHandler(); diff --git a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs index 063f4c48611b..ae33e7231011 100644 --- a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs @@ -19,18 +19,6 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { - // Add a problemMetadata to all requests - app.Use((context, next) => - { - var includeProblemMetadata = context.Request.Query["includeProblemMetadata"]; - if (includeProblemMetadata == "true") - { - context.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(new ProblemMetadata()), string.Empty)); - } - - return next(context); - }); - app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); // There is a default response but any of the following can be used to change the behavior. diff --git a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj index f87ca5956c7b..9c24fc8c3f64 100644 --- a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj +++ b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/StatusCodePagesSample.csproj @@ -6,14 +6,8 @@ - - - - - - diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs index 2f70ddb08544..09e0d633f395 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs @@ -20,12 +20,10 @@ public ApiBehaviorApplicationModelProvider( { var options = apiBehaviorOptions.Value; - var defaultErrorType = options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails); - ActionModelConventions = new List() { new ApiVisibilityConvention(), - new EndpointMetadataConvention(serviceProvider, defaultErrorType) + new EndpointMetadataConvention(serviceProvider) }; if (!options.SuppressMapClientErrors) @@ -43,6 +41,7 @@ public ApiBehaviorApplicationModelProvider( ActionModelConventions.Add(new ConsumesConstraintForFormFileParameterConvention()); } + var defaultErrorType = options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails); var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType); ActionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute)); diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs index 0645a7b8edfa..0ac32688ef67 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -14,18 +12,14 @@ internal sealed class EndpointMetadataConvention : IActionModelConvention private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointMetadataConvention).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointMetadataConvention).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; private readonly IServiceProvider _serviceProvider; - private readonly Type _defaultErrorType; - public EndpointMetadataConvention(IServiceProvider serviceProvider, Type defaultErrorType) + public EndpointMetadataConvention(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - _defaultErrorType = defaultErrorType; } public void Apply(ActionModel action) { - // Set the problem metadata when defaultError is ProblemDetails - ApplyProblemMetadata(action); // Get metadata from parameter types ApplyParametersMetadata(action); @@ -34,22 +28,6 @@ public void Apply(ActionModel action) ApplyReturnTypeMetadata(action); } - private void ApplyProblemMetadata(ActionModel action) - { - if (_defaultErrorType == typeof(ProblemDetails)) - { - var problemDetailsService = _serviceProvider.GetService(); - - if (problemDetailsService != null) - { - for (var i = 0; i < action.Selectors.Count; i++) - { - action.Selectors[i].EndpointMetadata.Add(new ProblemMetadata()); - } - } - } - } - private void ApplyReturnTypeMetadata(ActionModel action) { var returnType = action.ActionMethod.ReturnType; diff --git a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs index 81ebcbfd062c..709acb78b0a6 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs @@ -138,9 +138,10 @@ public override Task WriteAsync(OutputFormatterWriteContext context) if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsService.WriteAsync(new ProblemDetailsContext(context.HttpContext) + return problemDetailsService.WriteAsync(new () { - ProblemDetails = new ProblemDetails { Status = statusCode } + HttpContext = context.HttpContext, + ProblemDetails = { Status = statusCode } }).AsTask(); } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index cee660bb7f50..6d31c60472e8 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Infrastructure; @@ -11,6 +12,8 @@ internal sealed class DefaultApiProblemDetailsWriter : IProblemDetailsWriter private readonly OutputFormatterSelector _formatterSelector; private readonly IHttpResponseStreamWriterFactory _writerFactory; private readonly ProblemDetailsFactory _problemDetailsFactory; + private readonly ApiBehaviorOptions _apiBehaviorOptions; + private static readonly MediaTypeCollection _problemContentTypes = new() { "application/problem+json", @@ -20,21 +23,33 @@ internal sealed class DefaultApiProblemDetailsWriter : IProblemDetailsWriter public DefaultApiProblemDetailsWriter( OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, - ProblemDetailsFactory problemDetailsFactory) + ProblemDetailsFactory problemDetailsFactory, + IOptions apiBehaviorOptions) { _formatterSelector = formatterSelector; _writerFactory = writerFactory; _problemDetailsFactory = problemDetailsFactory; + _apiBehaviorOptions = apiBehaviorOptions.Value; } - public async ValueTask WriteAsync(ProblemDetailsContext context) + public async ValueTask TryWriteAsync(ProblemDetailsContext context) { + var controllerAttribute = context.AdditionalMetadata?.GetMetadata() ?? + context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + if (controllerAttribute == null) + { + return false; + } + var apiControllerAttribute = context.AdditionalMetadata?.GetMetadata() ?? context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - if (apiControllerAttribute is null) + if (apiControllerAttribute is null || _apiBehaviorOptions.SuppressMapClientErrors) { - return false; + // In this case we don't want to move + // to the next registered writers. + return true; } // Recreating the problem details to get all customizations diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs index 5a91c4922cf3..c08e86de6b9f 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs @@ -13,14 +13,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory { private readonly ApiBehaviorOptions _options; - private readonly Action? _configure; + private readonly Action? _configure; public DefaultProblemDetailsFactory( IOptions options, IOptions? problemDetailsOptions = null) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - _configure = problemDetailsOptions?.Value?.ConfigureDetails; + _configure = problemDetailsOptions?.Value?.CustomizeProblemDetails; } public override ProblemDetails CreateProblemDetails( @@ -98,6 +98,6 @@ private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails.Extensions["traceId"] = traceId; } - _configure?.Invoke(httpContext!, problemDetails); + _configure?.Invoke(new() { HttpContext = httpContext!, ProblemDetails = problemDetails }); } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 9abdedb154af..64c8e4182c5f 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -121,8 +121,9 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsService.WriteAsync(new ProblemDetailsContext(context.HttpContext) + return problemDetailsService.WriteAsync(new () { + HttpContext = context.HttpContext, ProblemDetails = new ProblemDetails { Status = statusCode } }).AsTask(); } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index 4b11c4a3ac26..a9e1b9c49837 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -1,34 +1,20 @@ // 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; -using Microsoft.Extensions.Options; - namespace Microsoft.AspNetCore.Mvc.Infrastructure; internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; - private readonly ProblemDetailsOptions _options; public ProblemDetailsClientErrorFactory( - ProblemDetailsFactory problemDetailsFactory, - IOptions options) + ProblemDetailsFactory problemDetailsFactory) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); - _options = options.Value; } public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { - var statusCode = clientError.StatusCode ?? 500; - var problemType = statusCode >= 500 ? ProblemDetailsTypes.Server : ProblemDetailsTypes.Client; - - if ((_options.AllowedProblemTypes & problemType) == ProblemDetailsTypes.None) - { - return null; - } - var problemDetails = _problemDetailsFactory.CreateProblemDetails(actionContext.HttpContext, clientError.StatusCode); return new ObjectResult(problemDetails) diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 5f8a80cef356..304b7408abaf 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -45,7 +45,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs index 71590f349cae..f74ec553df55 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs @@ -13,60 +13,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels; public class EndpointMetadataConventionTest { - [Fact] - public void Apply_DefaultErrorTypeMetadata_WhenIProblemDetailsServiceRegistered() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); - var errorType = typeof(ProblemDetails); - var convention = GetConvention(services: CreateServicesWithProblemDetatils(), errorType: errorType); - - //Act - convention.Apply(action); - - // Assert - foreach (var selector in action.Selectors) - { - Assert.Contains(selector.EndpointMetadata, m => m is IProblemDetailsMetadata attribute && attribute.ProblemType == ProblemDetailsTypes.All); - } - } - - [Fact] - public void Apply_SkipDefaultErrorTypeMetadata_WhenIProblemDetailsServiceNotRegistered() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); - var errorType = typeof(ProblemDetails); - var convention = GetConvention(errorType: errorType); - - //Act - convention.Apply(action); - - // Assert - foreach (var selector in action.Selectors) - { - Assert.DoesNotContain(selector.EndpointMetadata, m => m is IProblemDetailsMetadata); - } - } - - [Fact] - public void Apply_SkipDefaultErrorTypeMetadata_WhenVoid() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); - var errorType = typeof(void); - var convention = GetConvention(errorType: errorType); - - //Act - convention.Apply(action); - - // Assert - foreach (var selector in action.Selectors) - { - Assert.DoesNotContain(selector.EndpointMetadata, m => m is IProblemDetailsMetadata); - } - } - [Theory] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfActionResult))] @@ -240,11 +186,10 @@ public void Apply_AllowsRemovalOfMetadata_ByParameterTypeImplementingIEndpointPa Assert.DoesNotContain(action.Selectors[0].EndpointMetadata, m => m is IAcceptsMetadata); } - private static EndpointMetadataConvention GetConvention(IServiceProvider services = null, Type errorType = null) + private static EndpointMetadataConvention GetConvention(IServiceProvider services = null) { - errorType ??= typeof(void); services ??= Mock.Of(); - return new EndpointMetadataConvention(services, errorType); + return new EndpointMetadataConvention(services); } private static ApplicationModelProviderContext GetContext(Type type) diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs new file mode 100644 index 000000000000..e69f9c0d4795 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs @@ -0,0 +1,200 @@ +// 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.Http; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure; + +public class DefaultApiProblemDetailsWriterTest +{ + + [Fact] + public async Task WriteAsync_Works() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream); + + var expectedProblem = new ProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1-custom", + Title = "Custom Bad Request", + }; + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.TryWriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + } + + [Fact] + public async Task WriteAsync_AddExtensions() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new ProblemDetails(); + expectedProblem.Extensions["Extension1"] = "Extension1-Value"; + expectedProblem.Extensions["Extension2"] = "Extension2-Value"; + + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.TryWriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Contains("Extension1", problemDetails.Extensions); + Assert.Contains("Extension2", problemDetails.Extensions); + } + + [Fact] + public async Task WriteAsync_ReturnsFalseAndSkip_WhenNotController() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream, metadata: EndpointMetadataCollection.Empty); + + //Act + var result = await writer.TryWriteAsync(new() { HttpContext = context }); + + //Assert + Assert.False(result); + Assert.Equal(0, stream.Position); + Assert.Equal(0, stream.Length); + } + + [Fact] + public async Task WriteAsync_ReturnsTrueAndSkip_WhenNotApiController() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream, metadata: new EndpointMetadataCollection(new ControllerAttribute())); + + //Act + var result = await writer.TryWriteAsync(new() { HttpContext = context }); + + //Assert + Assert.True(result); + Assert.Equal(0, stream.Position); + Assert.Equal(0, stream.Length); + } + + [Fact] + public async Task WriteAsync_ReturnsTrueAndSkip_WhenSuppressMapClientErrors() + { + // Arrange + var writer = GetWriter(options: new ApiBehaviorOptions() { SuppressMapClientErrors = true }); + var stream = new MemoryStream(); + var context = CreateContext(stream); + + //Act + var result = await writer.TryWriteAsync(new() { HttpContext = context }); + + //Assert + Assert.True(result); + Assert.Equal(0, stream.Position); + Assert.Equal(0, stream.Length); + } + + [Fact] + public async Task WriteAsync_ReturnsFalseAndSkip_WhenNoFormatter() + { + // Arrange + var formatter = new Mock(); + formatter.Setup(f => f.CanWriteResult(It.IsAny())).Returns(false); + var writer = GetWriter(formatter: formatter.Object); + var stream = new MemoryStream(); + var context = CreateContext(stream); + + //Act + var result = await writer.TryWriteAsync(new() { HttpContext = context }); + + //Assert + Assert.False(result); + Assert.Equal(0, stream.Position); + Assert.Equal(0, stream.Length); + } + + private static HttpContext CreateContext(Stream body, int statusCode = StatusCodes.Status400BadRequest, EndpointMetadataCollection metadata = null) + { + metadata ??= new EndpointMetadataCollection(new ApiControllerAttribute(), new ControllerAttribute()); + + var context = new DefaultHttpContext() + { + Response = { Body = body, StatusCode = statusCode }, + RequestServices = CreateServices() + }; + context.SetEndpoint(new Endpoint(null, metadata, string.Empty)); + + return context; + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddTransient(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddSingleton(NullLoggerFactory.Instance); + + return services.BuildServiceProvider(); + } + + private static DefaultApiProblemDetailsWriter GetWriter(ApiBehaviorOptions options = null, IOutputFormatter formatter = null) + { + options ??= new ApiBehaviorOptions(); + formatter ??= new TestFormatter(); + + var mvcOptions = Options.Create(new MvcOptions()); + mvcOptions.Value.OutputFormatters.Add(formatter); + + return new DefaultApiProblemDetailsWriter( + new DefaultOutputFormatterSelector(mvcOptions, NullLoggerFactory.Instance), + new TestHttpResponseStreamWriterFactory(), + new DefaultProblemDetailsFactory(Options.Create(options), null), + Options.Create(options)); + } + + private class TestFormatter : IOutputFormatter + { + public bool CanWriteResult(OutputFormatterCanWriteContext context) => true; + + public Task WriteAsync(OutputFormatterWriteContext context) + { + return context.HttpContext.Response.WriteAsJsonAsync(context.Object); + } + } + +} diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs index 978e9d560d5c..692b1f9decb5 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs @@ -21,7 +21,7 @@ public void GetClientError_ReturnsProblemDetails_IfNoMappingWasFound() [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -48,7 +48,7 @@ public void GetClientError_ReturnsProblemDetails() [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -78,7 +78,7 @@ public void GetClientError_UsesActivityId_ToSetTraceId() [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -104,7 +104,7 @@ public void GetClientError_UsesHttpContext_ToSetTraceIdIfActivityIdIsNotSet() [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); - var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory, Options.Create(new ProblemDetailsOptions())); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); diff --git a/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs b/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs index 6a6deb674e3f..3242b5c3ff97 100644 --- a/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs +++ b/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs @@ -36,6 +36,12 @@ internal static class ProblemDetailsDefaults "Not Found" ), + [405] = + ( + "https://tools.ietf.org/html/rfc7231#section-6.5.5", + "Method Not Allowed" + ), + [406] = ( "https://tools.ietf.org/html/rfc7231#section-6.5.6", diff --git a/src/Shared/ProblemDetails/ProblemMetadata.cs b/src/Shared/ProblemDetails/ProblemMetadata.cs deleted file mode 100644 index 227970ca1c67..000000000000 --- a/src/Shared/ProblemDetails/ProblemMetadata.cs +++ /dev/null @@ -1,17 +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.Metadata; - -internal sealed class ProblemMetadata : IProblemDetailsMetadata -{ - public ProblemMetadata(int? statusCode = null, ProblemDetailsTypes problemType = ProblemDetailsTypes.All) - { - ProblemType = problemType; - StatusCode = statusCode; - } - - public int? StatusCode { get; } - - public ProblemDetailsTypes ProblemType { get; } -} diff --git a/src/Shared/ProblemDetails/RoutingProblemMetadata.cs b/src/Shared/ProblemDetails/RoutingProblemMetadata.cs deleted file mode 100644 index 060c1bb58f4c..000000000000 --- a/src/Shared/ProblemDetails/RoutingProblemMetadata.cs +++ /dev/null @@ -1,18 +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.Metadata; - -using Microsoft.AspNetCore.Http; - -internal sealed class RoutingProblemMetadata : IProblemDetailsMetadata -{ - public RoutingProblemMetadata(int statusCode = StatusCodes.Status404NotFound) - { - StatusCode = statusCode; - } - - public int? StatusCode { get; } - - public ProblemDetailsTypes ProblemType => ProblemDetailsTypes.Routing; -} From 5be8bd981853c165d07ae43b2cbd08304d413cdd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 Jul 2022 15:02:41 -0700 Subject: [PATCH 42/59] Clean up --- eng/ProjectReferences.props | 1 - eng/SharedFramework.Local.props | 1 - eng/TrimmableProjects.props | 1 - src/Framework/test/TestData.cs | 2 -- ...oblemDetailsServiceCollectionExtensions.cs | 22 +++++++++---------- .../src/PublicAPI.Unshipped.txt | 2 +- .../DeveloperExceptionPageSample/Startup.cs | 1 - .../ExceptionHandlerSample.csproj | 1 - .../ExceptionHandlerSample/Program.cs | 22 +++++++++++++++++++ .../EndpointMetadataConvention.cs | 1 - .../ProblemDetailsClientErrorFactory.cs | 5 ++--- ...ApiBehaviorApplicationModelProviderTest.cs | 5 ++--- .../EndpointMetadataConventionTest.cs | 7 ------ src/Mvc/Mvc.slnf | 4 +--- .../ProblemDetails/ProblemDetailsDefaults.cs | 3 --- src/Tools/Tools.slnf | 5 +---- 16 files changed, 39 insertions(+), 44 deletions(-) create mode 100644 src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Program.cs diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 4885943cd49d..e502668e6709 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -32,7 +32,6 @@ - diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index a84e70171a18..df28fe453b25 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -50,7 +50,6 @@ - diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index 87bb3b97a557..f87e23bba220 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -26,7 +26,6 @@ - diff --git a/src/Framework/test/TestData.cs b/src/Framework/test/TestData.cs index ad125d433d57..28aaa88dcc7b 100644 --- a/src/Framework/test/TestData.cs +++ b/src/Framework/test/TestData.cs @@ -52,7 +52,6 @@ static TestData() "Microsoft.AspNetCore.Http.Connections.Common", "Microsoft.AspNetCore.Http.Extensions", "Microsoft.AspNetCore.Http.Features", - "Microsoft.AspNetCore.Http.ProblemDetails", "Microsoft.AspNetCore.Http.Results", "Microsoft.AspNetCore.HttpLogging", "Microsoft.AspNetCore.HttpOverrides", @@ -190,7 +189,6 @@ static TestData() { "Microsoft.AspNetCore.Http.Connections.Common", "7.0.0.0" }, { "Microsoft.AspNetCore.Http.Extensions", "7.0.0.0" }, { "Microsoft.AspNetCore.Http.Features", "7.0.0.0" }, - { "Microsoft.AspNetCore.Http.ProblemDetails", "7.0.0.0" }, { "Microsoft.AspNetCore.Http.Results", "7.0.0.0" }, { "Microsoft.AspNetCore.HttpLogging", "7.0.0.0" }, { "Microsoft.AspNetCore.HttpOverrides", "7.0.0.0" }, diff --git a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index 145b411c8110..37ec08f391b1 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -20,31 +20,29 @@ public static class ProblemDetailsServiceCollectionExtensions public static IServiceCollection AddProblemDetails(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); - - // Adding default services; - services.TryAddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - return services; + return services.AddProblemDetails(configure: null); } /// /// Adds services required for creation of for failed requests. /// /// The to add the services to. - /// The to configure the services with. + /// The to configure the services with. /// The so that additional calls can be chained. public static IServiceCollection AddProblemDetails( this IServiceCollection services, - Action configureOptions) + Action? configure) { ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configureOptions); - // Adding default services - services.AddProblemDetails(); + // Adding default services; + services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.Configure(configureOptions); + if (configure != null) + { + services.Configure(configure); + } return services; } diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 6d8944593792..da9c33047572 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -45,7 +45,7 @@ static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync System.Threading.Tasks.Task! static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, TValue value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions.ConfigureRouteHandlerJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! Microsoft.AspNetCore.Http.EndpointDescriptionAttribute Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.EndpointDescriptionAttribute(string! description) -> void diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs index c5fbdaf845ab..a81b1593ca4d 100644 --- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Patterns; namespace DeveloperExceptionPageSample; diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj index 2343d8e73752..2faf636c4914 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj @@ -11,5 +11,4 @@ - diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Program.cs b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Program.cs new file mode 100644 index 000000000000..f49dd9f04212 --- /dev/null +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Program.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace ExceptionHandlerSample; + +public class Program +{ + public static Task Main(string[] args) + { + var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .UseIISIntegration() + .UseStartup(); + }) + .Build(); + + return host.RunAsync(); + } +} diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs index 0ac32688ef67..b830993746ab 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs @@ -20,7 +20,6 @@ public EndpointMetadataConvention(IServiceProvider serviceProvider) public void Apply(ActionModel action) { - // Get metadata from parameter types ApplyParametersMetadata(action); diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index a9e1b9c49837..75bfeae22093 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -7,13 +7,12 @@ internal sealed class ProblemDetailsClientErrorFactory : IClientErrorFactory { private readonly ProblemDetailsFactory _problemDetailsFactory; - public ProblemDetailsClientErrorFactory( - ProblemDetailsFactory problemDetailsFactory) + public ProblemDetailsClientErrorFactory(ProblemDetailsFactory problemDetailsFactory) { _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); } - public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) + public IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { var problemDetails = _problemDetailsFactory.CreateProblemDetails(actionContext.HttpContext, clientError.StatusCode); diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs index feacaf0e1eeb..c4dddb5d711a 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs @@ -3,11 +3,8 @@ using System.Reflection; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; @@ -80,6 +77,7 @@ public void OnProvidersExecuting_AppliesConventions() Assert.NotEmpty(actionModel.Filters.OfType()); Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource); Assert.NotEmpty(actionModel.Selectors); + Assert.Empty(actionModel.Selectors[0].EndpointMetadata); } [Fact] @@ -122,6 +120,7 @@ public void OnProvidersExecuting_AppliesConventionsForIResult() Assert.NotEmpty(actionModel.Filters.OfType()); Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource); Assert.NotEmpty(actionModel.Selectors); + Assert.Empty(actionModel.Selectors[0].EndpointMetadata); } [Fact] diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs index f74ec553df55..37be61a23f0b 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs @@ -211,13 +211,6 @@ private static ActionModel GetActionModel( return Assert.Single(controller.Actions, m => m.ActionName == actionName); } - private static IServiceProvider CreateServicesWithProblemDetatils() - { - var services = new ServiceCollection(); - services.AddSingleton(Mock.Of()); - return services.BuildServiceProvider(); - } - private class TestController { public ActionResult ActionWithParameterMetadata(AddsCustomParameterMetadata param1) => null; diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index d46687ae6c96..25a8b28c745c 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -32,8 +32,6 @@ "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", - "src\\Http\\ProblemDetails\\src\\Microsoft.AspNetCore.Http.ProblemDetails.csproj", - "src\\Http\\ProblemDetails\\test\\Microsoft.AspNetCore.Http.ProblemDetails.Tests.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", @@ -150,4 +148,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs b/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs index 3242b5c3ff97..0a4ad2c2c74d 100644 --- a/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs +++ b/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Mvc; namespace Microsoft.AspNetCore.Http; diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index 52472af8ff4c..ee9faad18757 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -5,7 +5,6 @@ "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj", "src\\Components\\Authorization\\src\\Microsoft.AspNetCore.Components.Authorization.csproj", "src\\Components\\Components\\src\\Microsoft.AspNetCore.Components.csproj", - "src\\Components\\CustomElements\\src\\Microsoft.AspNetCore.Components.CustomElements.csproj", "src\\Components\\Forms\\src\\Microsoft.AspNetCore.Components.Forms.csproj", "src\\Components\\WebAssembly\\Authentication.Msal\\src\\Microsoft.Authentication.WebAssembly.Msal.csproj", "src\\Components\\WebAssembly\\JSInterop\\src\\Microsoft.JSInterop.WebAssembly.csproj", @@ -34,7 +33,6 @@ "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", - "src\\Http\\ProblemDetails\\src\\Microsoft.AspNetCore.Http.ProblemDetails.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", @@ -54,7 +52,6 @@ "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", "src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", - "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\RateLimiting\\src\\Microsoft.AspNetCore.RateLimiting.csproj", "src\\Middleware\\RequestDecompression\\src\\Microsoft.AspNetCore.RequestDecompression.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", @@ -110,4 +107,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} From 86401b7bf9e579d7d124321bd1b787df44694d4a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 Jul 2022 15:14:41 -0700 Subject: [PATCH 43/59] Fixing publicapi warnings --- .../src/Properties/AssemblyInfo.cs | 2 -- .../Http.Extensions/src/PublicAPI.Unshipped.txt | 17 +++++++++++++++++ src/Mvc/Mvc.Core/src/Properties/AssemblyInfo.cs | 2 -- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 13 +++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs b/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs index e3cf753d8886..2122b7773ae1 100644 --- a/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs +++ b/src/Http/Http.Extensions/src/Properties/AssemblyInfo.cs @@ -3,9 +3,7 @@ using System.Runtime.CompilerServices; -#pragma warning disable RS0016 // Suppress PublicAPI analyzer [assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Mvc.ProblemDetails))] [assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Http.HttpValidationProblemDetails))] -#pragma warning restore RS0016 [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index da9c33047572..7ab26906ac22 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -3,6 +3,10 @@ *REMOVED*Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! *REMOVED*Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void *REMOVED*Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void +Microsoft.AspNetCore.Http.HttpValidationProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.ApplicationServices.get -> System.IServiceProvider! Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadataContext(System.Reflection.MethodInfo! method, System.Collections.Generic.IList! endpointMetadata, System.IServiceProvider! applicationServices) -> void @@ -38,6 +42,19 @@ Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactor *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void +Microsoft.AspNetCore.Mvc.ProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/src/Mvc/Mvc.Core/src/Properties/AssemblyInfo.cs b/src/Mvc/Mvc.Core/src/Properties/AssemblyInfo.cs index bd724d5ea759..a82122536794 100644 --- a/src/Mvc/Mvc.Core/src/Properties/AssemblyInfo.cs +++ b/src/Mvc/Mvc.Core/src/Properties/AssemblyInfo.cs @@ -6,9 +6,7 @@ using Microsoft.AspNetCore.Mvc.Formatters; [assembly: TypeForwardedTo(typeof(InputFormatterException))] -#pragma warning disable RS0016 // Suppress PublicAPI analyzer [assembly: TypeForwardedTo(typeof(ProblemDetails))] -#pragma warning restore RS0016 [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.ApiExplorer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index dc8c6f2136b6..9742d6caa946 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -7,6 +7,19 @@ Microsoft.AspNetCore.Mvc.ApplicationModels.InferParameterBindingInfoConvention.I *REMOVED*Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, Microsoft.AspNetCore.Mvc.ModelBinding.IValueProvider! valueProvider, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Mvc.ProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.get -> System.Collections.Generic.IDictionary! (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Instance.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.ProblemDetails() -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.get -> int? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Status.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) +Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TryParseModelBinderProvider Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TryParseModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TryParseModelBinderProvider.TryParseModelBinderProvider() -> void From 3f58002e264b80b58b0ecaf7a8cbe73dbf3666f5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 10 Jul 2022 21:34:25 -0700 Subject: [PATCH 44/59] Clean up --- .../HttpValidationProblemDetailsJsonConverterTest.cs | 9 +++++---- .../test/ProblemDetailsJsonConverterTest.cs | 9 +++++---- .../UnitTests/Matching/HttpMethodMatcherPolicyTest.cs | 5 +---- .../test/testassets/ExceptionHandlerSample/Startup.cs | 1 - .../test/testassets/StatusCodePagesSample/Startup.cs | 1 - 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs index 94bce60ddc89..ab4408dd7561 100644 --- a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -40,7 +40,7 @@ public void Read_Works() kvp => { Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); + Assert.Equal(traceId, kvp.Value?.ToString()); }); Assert.Collection( problemDetails.Errors.OrderBy(kvp => kvp.Key), @@ -81,7 +81,7 @@ public void Read_WithSomeMissingValues_Works() kvp => { Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); + Assert.Equal(traceId, kvp.Value?.ToString()); }); Assert.Collection( problemDetails.Errors.OrderBy(kvp => kvp.Key), @@ -111,7 +111,8 @@ public void ReadUsingJsonSerializerWorks() // Act var problemDetails = JsonSerializer.Deserialize(json, JsonSerializerOptions); - Assert.Equal(type, problemDetails.Type); + Assert.NotNull(problemDetails); + Assert.Equal(type, problemDetails!.Type); Assert.Equal(title, problemDetails.Title); Assert.Equal(status, problemDetails.Status); Assert.Collection( @@ -119,7 +120,7 @@ public void ReadUsingJsonSerializerWorks() kvp => { Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); + Assert.Equal(traceId, kvp.Value?.ToString()); }); Assert.Collection( problemDetails.Errors.OrderBy(kvp => kvp.Key), diff --git a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs index 77259e56e9a5..995655ba2947 100644 --- a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs @@ -57,7 +57,7 @@ public void Read_Works() kvp => { Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); + Assert.Equal(traceId, kvp.Value?.ToString()); }); } @@ -77,7 +77,8 @@ public void Read_UsingJsonSerializerWorks() var problemDetails = JsonSerializer.Deserialize(json, JsonSerializerOptions); // Assert - Assert.Equal(type, problemDetails.Type); + Assert.NotNull(problemDetails); + Assert.Equal(type, problemDetails!.Type); Assert.Equal(title, problemDetails.Title); Assert.Equal(status, problemDetails.Status); Assert.Equal(instance, problemDetails.Instance); @@ -87,7 +88,7 @@ public void Read_UsingJsonSerializerWorks() kvp => { Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); + Assert.Equal(traceId, kvp.Value?.ToString()); }); } @@ -116,7 +117,7 @@ public void Read_WithSomeMissingValues_Works() kvp => { Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); + Assert.Equal(traceId, kvp.Value?.ToString()); }); } diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs index 8b8c703904ff..b5cf4b1b7e2e 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using static Microsoft.AspNetCore.Routing.Matching.HttpMethodMatcherPolicy; @@ -170,9 +169,7 @@ public async Task IEndpointSelectorPolicy_ApplyAsync_ProcessesInvalidCandidate(i await policy.ApplyAsync(httpContext, candidates); - var metadata = Assert.Single(httpContext.GetEndpoint().Metadata); - var problemMetadata = Assert.IsAssignableFrom(metadata); - Assert.Equal(405, problemMetadata.StatusCode); + Assert.Equal(httpContext.GetEndpoint().Metadata, EndpointMetadataCollection.Empty); Assert.True(string.Equals(httpContext.GetEndpoint().DisplayName, Http405EndpointDisplayName, StringComparison.OrdinalIgnoreCase)); } diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs index 40bb8ab3f5e7..b54320e19583 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs @@ -3,7 +3,6 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http.Metadata; namespace ExceptionHandlerSample; diff --git a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs index ae33e7231011..ba0bda543a72 100644 --- a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs +++ b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Startup.cs @@ -6,7 +6,6 @@ using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http.Metadata; namespace StatusCodePagesSample; From 7dadaedae42c52a9c2a9b31ada09f64f37e37f55 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 10 Jul 2022 21:44:52 -0700 Subject: [PATCH 45/59] PR Feedback --- .../src/ProblemDetails/ProblemDetailsContext.cs | 8 +++++++- .../Http.Extensions/src/ProblemDetailsDefaultWriter.cs | 4 ++-- .../Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs index 4ef374a3ec5d..2a7de91c4fef 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Http; /// public sealed class ProblemDetailsContext { + private ProblemDetails? _problemDetails; + /// /// The associated with the current request being processed by the filter. /// @@ -24,5 +26,9 @@ public sealed class ProblemDetailsContext /// A instance of that will be /// used during the response payload generation. ///
- public ProblemDetails ProblemDetails { get; init; } = new ProblemDetails(); + public ProblemDetails ProblemDetails + { + get => _problemDetails ??= new ProblemDetails(); + init => _problemDetails = value; + } } diff --git a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs index f1d5056db36a..8c379ff03935 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs @@ -11,8 +11,8 @@ namespace Microsoft.AspNetCore.Http; internal sealed partial class ProblemDetailsDefaultWriter : IProblemDetailsWriter { - private readonly MediaTypeHeaderValue _jsonMediaType = new("application/json"); - private readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json"); + private static readonly MediaTypeHeaderValue _jsonMediaType = new("application/json"); + private static readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json"); private readonly ProblemDetailsOptions _options; public ProblemDetailsDefaultWriter(IOptions options) diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs index 64c8e4182c5f..d8bab1ab8ef6 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs @@ -121,10 +121,10 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? if (context.HttpContext.RequestServices.GetService() is { } problemDetailsService) { - return problemDetailsService.WriteAsync(new () + return problemDetailsService.WriteAsync(new() { HttpContext = context.HttpContext, - ProblemDetails = new ProblemDetails { Status = statusCode } + ProblemDetails = { Status = statusCode } }).AsTask(); } From 5cb3286568a1a73c1841e7d74e7886b833fce56f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 10 Jul 2022 21:53:46 -0700 Subject: [PATCH 46/59] Fixing build --- .../StartupWithProblemDetails.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs index ff6cb2ae2a33..ec842f666713 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs @@ -36,20 +36,5 @@ public void Configure(IApplicationBuilder app) await context.Response.WriteAsync("\r\n"); }); } - - public static Task Main(string[] args) - { - var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseKestrel() - .UseIISIntegration() - .UseStartup(); - }) - .Build(); - - return host.RunAsync(); - } } From e03c3e3910890cad0a6adf1f78c46273579572b2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 10 Jul 2022 22:38:23 -0700 Subject: [PATCH 47/59] Fix unit test --- .../MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs b/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs index 6b3562b06dfe..3412b2d3d281 100644 --- a/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs +++ b/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs @@ -53,6 +53,6 @@ public async Task ExceptionWrittenToDiagnostics() Assert.Equal("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareAnalysisTests+<>c", listener.MiddlewareException[0]); // reversed "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware" Assert.Equal(1, listener.MiddlewareFinished.Count); - Assert.Equal("Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware", listener.MiddlewareFinished[0]); + Assert.Equal("Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl", listener.MiddlewareFinished[0]); } } From 988a20e1a071d9aa3c1f75814724200bb22b504f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2022 09:12:57 -0700 Subject: [PATCH 48/59] Fix unit tests --- .../src/ProblemDetailsDefaultWriter.cs | 2 +- .../test/ProblemDetailsDefaultWriterTest.cs | 15 +++++++++------ .../ProblemDetailsExceptionHandlerSampleTest.cs | 2 ++ .../test/FunctionalTests/StatusCodeSampleTest.cs | 3 --- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs index 8c379ff03935..bab869dcdd4c 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs @@ -25,7 +25,7 @@ public async ValueTask TryWriteAsync(ProblemDetailsContext context) var httpContext = context.HttpContext; var acceptHeader = httpContext.Request.GetTypedHeaders().Accept; - if (acceptHeader is { Count: > 0 } && + if (acceptHeader == null || !acceptHeader.Any(h => _jsonMediaType.IsSubsetOf(h) || _problemDetailsJsonMediaType.IsSubsetOf(h))) { return false; diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index 19cf2a4838d5..3af6ae2c0309 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -172,8 +172,7 @@ public async Task WriteAsync_Works_WhenJsonAccepted(string contentType) // Arrange var writer = GetWriter(); var stream = new MemoryStream(); - var context = CreateContext(stream); - context.Request.Headers.Accept = contentType; + var context = CreateContext(stream, contentType: contentType); var expectedProblem = new ProblemDetails() { @@ -211,8 +210,7 @@ public async Task WriteAsync_Skips_WhenJsonNotAccepted(string contentType) // Arrange var writer = GetWriter(); var stream = new MemoryStream(); - var context = CreateContext(stream); - context.Request.Headers.Accept = contentType; + var context = CreateContext(stream, contentType: contentType); //Act var result = await writer.TryWriteAsync(new() { HttpContext = context }); @@ -223,13 +221,18 @@ public async Task WriteAsync_Skips_WhenJsonNotAccepted(string contentType) Assert.Equal(0, stream.Length); } - private static HttpContext CreateContext(Stream body, int statusCode = StatusCodes.Status400BadRequest) + private static HttpContext CreateContext( + Stream body, + int statusCode = StatusCodes.Status400BadRequest, + string contentType = "application/json") { - return new DefaultHttpContext() + var context = new DefaultHttpContext() { Response = { Body = body, StatusCode = statusCode }, RequestServices = CreateServices() }; + context.Request.Headers.Accept = contentType; + return context; } private static IServiceProvider CreateServices() diff --git a/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs b/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs index 400d420d1817..06b54ccfd05f 100644 --- a/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs +++ b/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs @@ -5,6 +5,7 @@ using System.Net.Http; using Microsoft.AspNetCore.Mvc; using System.Net.Http.Json; +using System.Net.Http.Headers; namespace Microsoft.AspNetCore.Diagnostics.FunctionalTests; @@ -22,6 +23,7 @@ public async Task ExceptionHandlerPage_ProducesProblemDetails() { // Arrange var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/throw"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // Act var response = await Client.SendAsync(request); diff --git a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs index ce65c8720234..3299200c2827 100644 --- a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs +++ b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs @@ -25,7 +25,6 @@ public async Task StatusCodePage_ShowsError() { // Arrange var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/errors/417"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); // Act var response = await Client.SendAsync(request); @@ -42,7 +41,6 @@ public async Task StatusCodePageOptions_ExcludesSemicolon_WhenReasonPhrase_IsUnk //Arrange var httpStatusCode = 541; var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost/?statuscode={httpStatusCode}"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); //Act var response = await Client.SendAsync(request); @@ -62,7 +60,6 @@ public async Task StatusCodePageOptions_IncludesSemicolon__AndReasonPhrase_WhenR //Arrange var httpStatusCode = 400; var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost/?statuscode={httpStatusCode}"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); //Act var response = await Client.SendAsync(request); From 8c823a4c37f392ec7104ae13539de7ba8beecdb2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2022 10:30:51 -0700 Subject: [PATCH 49/59] PR review --- .../src/ProblemDetails/IProblemDetailsService.cs | 8 +++++++- .../src/ProblemDetails/ProblemDetailsContext.cs | 2 +- .../Http.Extensions/src/ProblemDetailsDefaultWriter.cs | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs index 7a5be8ec1304..564ee29403ad 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs @@ -10,8 +10,14 @@ namespace Microsoft.AspNetCore.Http; public interface IProblemDetailsService { /// - /// Write a response to the current context + /// Try to write a response to the current context, + /// using the registered services. /// /// The associated with the current request/response. + /// The registered services + /// are processed in sequence and the processing is completed when: + /// One of them reports that the response was written successfully, or. + /// All were executed and none of them was able to write the response successfully. + /// ValueTask WriteAsync(ProblemDetailsContext context); } diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs index 2a7de91c4fef..f0a3742513f8 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Http; /// -/// Represent the current problem detatils context for the request. +/// Represent the current problem details context for the request. /// public sealed class ProblemDetailsContext { diff --git a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs index bab869dcdd4c..0e2e12fde86d 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs @@ -23,7 +23,7 @@ public ProblemDetailsDefaultWriter(IOptions options) public async ValueTask TryWriteAsync(ProblemDetailsContext context) { var httpContext = context.HttpContext; - var acceptHeader = httpContext.Request.GetTypedHeaders().Accept; + var acceptHeader = httpContext.Request.Headers.Accept.GetList(); if (acceptHeader == null || !acceptHeader.Any(h => _jsonMediaType.IsSubsetOf(h) || _problemDetailsJsonMediaType.IsSubsetOf(h))) From 483f54eeca31053f5029f1c3a70a8502c90ebb29 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2022 15:15:54 -0700 Subject: [PATCH 50/59] Fixing JsonSerializationContext issues --- .../src/ProblemDetailsDefaultWriter.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs index 0e2e12fde86d..3f77ffcae673 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.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.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; @@ -20,6 +21,9 @@ public ProblemDetailsDefaultWriter(IOptions options) _options = options.Value; } + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "JSON serialization of ProblemDetails.Extensions might require types that cannot be statically analyzed and we need to fallback" + + "to reflection-based. The ProblemDetailsConverter is marked as RequiresUnreferencedCode already.")] public async ValueTask TryWriteAsync(ProblemDetailsContext context) { var httpContext = context.HttpContext; @@ -34,11 +38,21 @@ public async ValueTask TryWriteAsync(ProblemDetailsContext context) ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); _options.CustomizeProblemDetails?.Invoke(context); + if (context.ProblemDetails.Extensions is { Count: 0 }) + { + // We can use the source generation in this case + await httpContext.Response.WriteAsJsonAsync( + context.ProblemDetails, + ProblemDetailsJsonContext.Default.ProblemDetails, + contentType: "application/problem+json"); + return httpContext.Response.HasStarted; + + } + await httpContext.Response.WriteAsJsonAsync( - context.ProblemDetails, - typeof(ProblemDetails), - ProblemDetailsJsonContext.Default, - contentType: "application/problem+json"); + context.ProblemDetails, + options: null, + contentType: "application/problem+json"); return httpContext.Response.HasStarted; } From 35de3fed4182c233cfd3e15cb064ae6b4c7d440f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2022 18:36:24 -0700 Subject: [PATCH 51/59] Adding statuscode 405 --- .../test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs index 4cec9eb27fdd..d1e7be87785f 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -16,7 +16,7 @@ public class ApiBehaviorOptionsSetupTest public void Configure_AddsClientErrorMappings() { // Arrange - var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, 500, }; + var expected = new[] { 400, 401, 403, 404, 405, 406, 409, 415, 422, 500, }; var optionsSetup = new ApiBehaviorOptionsSetup(); var options = new ApiBehaviorOptions(); From 1ba1f109d66bf6b84a2e23a8562b2ca1eb6a2a3a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2022 21:16:10 -0700 Subject: [PATCH 52/59] Update src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs Co-authored-by: Brennan --- src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs index 3f77ffcae673..14350bfdce76 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs @@ -45,8 +45,8 @@ await httpContext.Response.WriteAsJsonAsync( context.ProblemDetails, ProblemDetailsJsonContext.Default.ProblemDetails, contentType: "application/problem+json"); - return httpContext.Response.HasStarted; + return httpContext.Response.HasStarted; } await httpContext.Response.WriteAsJsonAsync( From 2455ccb5607012e05c60168d3bec31048dce2e52 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 12 Jul 2022 12:57:46 -0700 Subject: [PATCH 53/59] PR review --- .../src/ProblemDetails/ProblemDetailsContext.cs | 2 +- ...DetailsDefaultWriter.cs => DefaultProblemDetailsWriter.cs} | 4 ++-- .../src/ProblemDetailsServiceCollectionExtensions.cs | 2 +- .../Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs | 4 ++-- .../test/ProblemDetailsServiceCollectionExtensionsTest.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/Http/Http.Extensions/src/{ProblemDetailsDefaultWriter.cs => DefaultProblemDetailsWriter.cs} (95%) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs index f0a3742513f8..62058cf8ec36 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -23,7 +23,7 @@ public sealed class ProblemDetailsContext public EndpointMetadataCollection? AdditionalMetadata { get; init; } /// - /// A instance of that will be + /// An instance of that will be /// used during the response payload generation. /// public ProblemDetails ProblemDetails diff --git a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs similarity index 95% rename from src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs rename to src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs index 14350bfdce76..8a89c9ab2300 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs +++ b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs @@ -10,13 +10,13 @@ namespace Microsoft.AspNetCore.Http; -internal sealed partial class ProblemDetailsDefaultWriter : IProblemDetailsWriter +internal sealed partial class DefaultProblemDetailsWriter : IProblemDetailsWriter { private static readonly MediaTypeHeaderValue _jsonMediaType = new("application/json"); private static readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json"); private readonly ProblemDetailsOptions _options; - public ProblemDetailsDefaultWriter(IOptions options) + public DefaultProblemDetailsWriter(IOptions options) { _options = options.Value; } diff --git a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index 37ec08f391b1..c8508a3fa668 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -37,7 +37,7 @@ public static IServiceCollection AddProblemDetails( // Adding default services; services.TryAddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); if (configure != null) { diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index 3af6ae2c0309..3742a4502e11 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -244,9 +244,9 @@ private static IServiceProvider CreateServices() return services.BuildServiceProvider(); } - private static ProblemDetailsDefaultWriter GetWriter(ProblemDetailsOptions options = null) + private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null) { options ??= new ProblemDetailsOptions(); - return new ProblemDetailsDefaultWriter(Options.Create(options)); + return new DefaultProblemDetailsWriter(Options.Create(options)); } } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs index a98d42733e84..0760838e00fd 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -20,7 +20,7 @@ public void AddProblemDetails_AddsNeededServices() // Assert Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsService) && sd.ImplementationType == typeof(ProblemDetailsService)); - Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsWriter) && sd.ImplementationType == typeof(ProblemDetailsDefaultWriter)); + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsWriter) && sd.ImplementationType == typeof(DefaultProblemDetailsWriter)); } [Fact] From cd92e30a7adb927c82e04c4e4606a8ac1d556c0e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 12 Jul 2022 15:32:09 -0700 Subject: [PATCH 54/59] Apply suggestions from code review Co-authored-by: Stephen Halter --- src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs | 4 ++-- .../src/Infrastructure/DefaultApiProblemDetailsWriter.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs index 8a89c9ab2300..64496e27bb51 100644 --- a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs @@ -46,7 +46,7 @@ await httpContext.Response.WriteAsJsonAsync( ProblemDetailsJsonContext.Default.ProblemDetails, contentType: "application/problem+json"); - return httpContext.Response.HasStarted; + return true; } await httpContext.Response.WriteAsJsonAsync( @@ -54,7 +54,7 @@ await httpContext.Response.WriteAsJsonAsync( options: null, contentType: "application/problem+json"); - return httpContext.Response.HasStarted; + return true; } [JsonSerializable(typeof(ProblemDetails))] diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index 6d31c60472e8..0360fd05b5dc 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -83,10 +83,10 @@ public async ValueTask TryWriteAsync(ProblemDetailsContext context) if (selectedFormatter == null) { - return context.HttpContext.Response.HasStarted; + return false; } await selectedFormatter.WriteAsync(formatterContext); - return context.HttpContext.Response.HasStarted; + return true; } } From 55cb7dadef688f717799917e1a7b559d4ac45e93 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Jul 2022 13:34:08 -0700 Subject: [PATCH 55/59] Fixing bad merge --- .../Http.Abstractions/src/PublicAPI.Unshipped.txt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index d016b137ffb9..b4eaf731778c 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -58,14 +58,6 @@ Microsoft.AspNetCore.Http.ProblemDetailsContext.HttpContext.init -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.get -> Microsoft.AspNetCore.Mvc.ProblemDetails! Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.init -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetailsContext() -> void -Microsoft.AspNetCore.Http.RouteHandlerContext -Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider! -Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Http.RouteHandlerContext.MethodInfo.get -> System.Reflection.MethodInfo! -Microsoft.AspNetCore.Http.RouteHandlerContext.RouteHandlerContext(System.Reflection.MethodInfo! methodInfo, System.Collections.Generic.IList! endpointMetadata, System.IServiceProvider! applicationServices) -> void -Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate -Microsoft.AspNetCore.Http.RouteHandlerInvocationContext -Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.RouteHandlerInvocationContext() -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(Microsoft.AspNetCore.Routing.RouteValueDictionary? dictionary) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable>? values) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable>? values) -> void @@ -88,12 +80,6 @@ Microsoft.AspNetCore.Mvc.ProblemDetails.Title.get -> string? Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void -abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.Arguments.get -> System.Collections.Generic.IList! -abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.GetArgument(int index) -> T -abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! -override Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext.Arguments.get -> System.Collections.Generic.IList! -override Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext.GetArgument(int index) -> T -override Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! override Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.Arguments.get -> System.Collections.Generic.IList! override Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.GetArgument(int index) -> T override Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! From 85850705897e9985748e33f58c55c78b91cf5e5d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Jul 2022 13:41:24 -0700 Subject: [PATCH 56/59] Fix bad merge --- src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index d71fe2cfabe0..4a1707193af5 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -25,10 +25,6 @@ Microsoft.AspNetCore.Http.ProblemDetailsOptions Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails.get -> System.Action? Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails.set -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointMetadata.get -> System.Collections.Generic.IList? -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointMetadata.init -> void -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void From 2c12741146116d638bdf429b822bb1d1c1d11e3c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Jul 2022 14:28:13 -0700 Subject: [PATCH 57/59] Adding analysis.NextMiddlewareName --- .../DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs | 2 ++ .../src/ExceptionHandler/ExceptionHandlerExtensions.cs | 2 ++ .../MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs index cd42758a107a..39e1ecb24dd6 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs @@ -26,6 +26,7 @@ public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBui throw new ArgumentNullException(nameof(app)); } + app.Properties["analysis.NextMiddlewareName"] = "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware"; return app.UseMiddleware(); } @@ -52,6 +53,7 @@ public static IApplicationBuilder UseDeveloperExceptionPage( throw new ArgumentNullException(nameof(options)); } + app.Properties["analysis.NextMiddlewareName"] = "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware"; return app.UseMiddleware(Options.Create(options)); } } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index d1857fc03245..b84e60b0c7e3 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -106,6 +106,8 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; var problemDetailsService = app.ApplicationServices.GetService(); + app.Properties["analysis.NextMiddlewareName"] = "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware"; + // Only use this path if there's a global router (in the 'WebApplication' case). if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) { diff --git a/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs b/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs index 3412b2d3d281..6b3562b06dfe 100644 --- a/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs +++ b/src/Middleware/MiddlewareAnalysis/test/MiddlewareAnalysisTests.cs @@ -53,6 +53,6 @@ public async Task ExceptionWrittenToDiagnostics() Assert.Equal("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareAnalysisTests+<>c", listener.MiddlewareException[0]); // reversed "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware" Assert.Equal(1, listener.MiddlewareFinished.Count); - Assert.Equal("Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl", listener.MiddlewareFinished[0]); + Assert.Equal("Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware", listener.MiddlewareFinished[0]); } } From 4b84cf3f8ccfee1863295f2a55995de5058d002a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Jul 2022 14:31:09 -0700 Subject: [PATCH 58/59] PR review --- .../src/Microsoft.AspNetCore.Http.Abstractions.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 2ca6c07bce83..bcd4d403f322 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -26,7 +26,8 @@ Microsoft.AspNetCore.Http.HttpResponse - + + From 1fe3f3bc6f2b76a7bffa97bf4d30671000d484fa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Jul 2022 14:35:58 -0700 Subject: [PATCH 59/59] Changing to CanWrite/WriteAsync --- .../ProblemDetails/IProblemDetailsWriter.cs | 10 ++++- .../src/PublicAPI.Unshipped.txt | 3 +- .../src/DefaultProblemDetailsWriter.cs | 31 ++++++------- .../src/ProblemDetailsService.cs | 37 +++++++++++++--- .../test/ProblemDetailsDefaultWriterTest.cs | 43 +++++-------------- .../test/ProblemDetailsServiceTest.cs | 12 +++--- .../DefaultApiProblemDetailsWriter.cs | 20 ++++----- .../DefaultApiProblemDetailsWriterTest.cs | 38 ++++++++++------ 8 files changed, 104 insertions(+), 90 deletions(-) diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs index 09d2e920e086..852e6b2e5789 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs @@ -13,6 +13,12 @@ public interface IProblemDetailsWriter /// Write a response to the current context ///
/// The associated with the current request/response. - /// Flag that indicates if the response was started. - ValueTask TryWriteAsync(ProblemDetailsContext context); + ValueTask WriteAsync(ProblemDetailsContext context); + + /// + /// Determines whether this instance can write a to the current context. + /// + /// The associated with the current request/response. + /// Flag that indicates if that the writer can write to the current . + bool CanWrite(ProblemDetailsContext context); } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index b4eaf731778c..43a78d85ac62 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -37,9 +37,10 @@ Microsoft.AspNetCore.Http.IFileHttpResult.FileDownloadName.get -> string? Microsoft.AspNetCore.Http.IProblemDetailsService Microsoft.AspNetCore.Http.IProblemDetailsService.WriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.IProblemDetailsWriter -Microsoft.AspNetCore.Http.IProblemDetailsWriter.TryWriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.INestedHttpResult Microsoft.AspNetCore.Http.INestedHttpResult.Result.get -> Microsoft.AspNetCore.Http.IResult! +Microsoft.AspNetCore.Http.IProblemDetailsWriter.CanWrite(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> bool +Microsoft.AspNetCore.Http.IProblemDetailsWriter.WriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.IStatusCodeHttpResult Microsoft.AspNetCore.Http.IStatusCodeHttpResult.StatusCode.get -> int? Microsoft.AspNetCore.Http.IValueHttpResult diff --git a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs index 64496e27bb51..2bedccd8254a 100644 --- a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs @@ -21,40 +21,41 @@ public DefaultProblemDetailsWriter(IOptions options) _options = options.Value; } - [UnconditionalSuppressMessage("Trimming", "IL2026", - Justification = "JSON serialization of ProblemDetails.Extensions might require types that cannot be statically analyzed and we need to fallback" + - "to reflection-based. The ProblemDetailsConverter is marked as RequiresUnreferencedCode already.")] - public async ValueTask TryWriteAsync(ProblemDetailsContext context) + public bool CanWrite(ProblemDetailsContext context) { var httpContext = context.HttpContext; var acceptHeader = httpContext.Request.Headers.Accept.GetList(); - if (acceptHeader == null || - !acceptHeader.Any(h => _jsonMediaType.IsSubsetOf(h) || _problemDetailsJsonMediaType.IsSubsetOf(h))) + if (acceptHeader?.Any(h => _jsonMediaType.IsSubsetOf(h) || _problemDetailsJsonMediaType.IsSubsetOf(h)) == true) { - return false; + return true; } + return false; + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "JSON serialization of ProblemDetails.Extensions might require types that cannot be statically analyzed and we need to fallback" + + "to reflection-based. The ProblemDetailsConverter is marked as RequiresUnreferencedCode already.")] + public ValueTask WriteAsync(ProblemDetailsContext context) + { + var httpContext = context.HttpContext; ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); _options.CustomizeProblemDetails?.Invoke(context); if (context.ProblemDetails.Extensions is { Count: 0 }) { // We can use the source generation in this case - await httpContext.Response.WriteAsJsonAsync( + return new ValueTask(httpContext.Response.WriteAsJsonAsync( context.ProblemDetails, ProblemDetailsJsonContext.Default.ProblemDetails, - contentType: "application/problem+json"); - - return true; + contentType: "application/problem+json")); } - await httpContext.Response.WriteAsJsonAsync( + return new ValueTask(httpContext.Response.WriteAsJsonAsync( context.ProblemDetails, options: null, - contentType: "application/problem+json"); - - return true; + contentType: "application/problem+json")); } [JsonSerializable(typeof(ProblemDetails))] diff --git a/src/Http/Http.Extensions/src/ProblemDetailsService.cs b/src/Http/Http.Extensions/src/ProblemDetailsService.cs index 02021a864c85..68002af77b96 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsService.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsService.cs @@ -3,33 +3,56 @@ namespace Microsoft.AspNetCore.Http; +using System.Linq; + internal sealed class ProblemDetailsService : IProblemDetailsService { - private readonly IEnumerable _writers; + private readonly IProblemDetailsWriter[] _writers; public ProblemDetailsService( IEnumerable writers) { - _writers = writers; + _writers = writers.ToArray(); } - public async ValueTask WriteAsync(ProblemDetailsContext context) + public ValueTask WriteAsync(ProblemDetailsContext context) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(context.ProblemDetails); ArgumentNullException.ThrowIfNull(context.HttpContext); - if (context.HttpContext.Response.HasStarted || context.HttpContext.Response.StatusCode < 400) + if (context.HttpContext.Response.HasStarted || + context.HttpContext.Response.StatusCode < 400 || + _writers.Length == 0) + { + return ValueTask.CompletedTask; + } + + IProblemDetailsWriter? selectedWriter = null; + + if (_writers.Length == 1) { - return; + selectedWriter = _writers[0]; + + return selectedWriter.CanWrite(context) ? + selectedWriter.WriteAsync(context) : + ValueTask.CompletedTask; } - foreach (var writer in _writers) + for (var i = 0; i < _writers.Length; i++) { - if (await writer.TryWriteAsync(context)) + if (_writers[i].CanWrite(context)) { + selectedWriter = _writers[i]; break; } } + + if (selectedWriter != null) + { + return selectedWriter.WriteAsync(context); + } + + return ValueTask.CompletedTask; } } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index 3742a4502e11..997641d42f44 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -34,7 +34,7 @@ public async Task WriteAsync_Works() }; //Act - await writer.TryWriteAsync(problemDetailsContext); + await writer.WriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -65,7 +65,7 @@ public async Task WriteAsync_AddExtensions() }; //Act - await writer.TryWriteAsync(problemDetailsContext); + await writer.WriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -93,7 +93,7 @@ public async Task WriteAsync_Applies_Defaults() var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.TryWriteAsync(new ProblemDetailsContext() { HttpContext = context }); + await writer.WriteAsync(new ProblemDetailsContext() { HttpContext = context }); //Assert stream.Position = 0; @@ -122,7 +122,7 @@ public async Task WriteAsync_Applies_CustomConfiguration() var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.TryWriteAsync(new ProblemDetailsContext() + await writer.WriteAsync(new ProblemDetailsContext() { HttpContext = context, ProblemDetails = { Status = StatusCodes.Status400BadRequest } @@ -147,7 +147,7 @@ public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified() var context = CreateContext(stream, StatusCodes.Status500InternalServerError); //Act - await writer.TryWriteAsync(new ProblemDetailsContext() + await writer.WriteAsync(new ProblemDetailsContext() { HttpContext = context, ProblemDetails = { Status = StatusCodes.Status400BadRequest } @@ -167,45 +167,24 @@ await writer.TryWriteAsync(new ProblemDetailsContext() [InlineData("application/*")] [InlineData("application/json")] [InlineData("application/problem+json")] - public async Task WriteAsync_Works_WhenJsonAccepted(string contentType) + public void CanWrite_ReturnsTrue_WhenJsonAccepted(string contentType) { // Arrange var writer = GetWriter(); var stream = new MemoryStream(); var context = CreateContext(stream, contentType: contentType); - var expectedProblem = new ProblemDetails() - { - Detail = "Custom Bad Request", - Instance = "Custom Bad Request", - Status = StatusCodes.Status400BadRequest, - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1-custom", - Title = "Custom Bad Request", - }; - var problemDetailsContext = new ProblemDetailsContext() - { - HttpContext = context, - ProblemDetails = expectedProblem - }; - //Act - await writer.TryWriteAsync(problemDetailsContext); + var result = writer.CanWrite(new() { HttpContext = context }); //Assert - stream.Position = 0; - var problemDetails = await JsonSerializer.DeserializeAsync(stream); - Assert.NotNull(problemDetails); - Assert.Equal(expectedProblem.Status, problemDetails.Status); - Assert.Equal(expectedProblem.Type, problemDetails.Type); - Assert.Equal(expectedProblem.Title, problemDetails.Title); - Assert.Equal(expectedProblem.Detail, problemDetails.Detail); - Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + Assert.True(result); } [Theory] [InlineData("application/xml")] [InlineData("application/problem+xml")] - public async Task WriteAsync_Skips_WhenJsonNotAccepted(string contentType) + public void CanWrite_ReturnsFalse_WhenJsonNotAccepted(string contentType) { // Arrange var writer = GetWriter(); @@ -213,12 +192,10 @@ public async Task WriteAsync_Skips_WhenJsonNotAccepted(string contentType) var context = CreateContext(stream, contentType: contentType); //Act - var result = await writer.TryWriteAsync(new() { HttpContext = context }); + var result = writer.CanWrite(new() { HttpContext = context }); //Assert Assert.False(result); - Assert.Equal(0, stream.Position); - Assert.Equal(0, stream.Length); } private static HttpContext CreateContext( diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs index 02f1323c82a3..66abd0de36a3 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs @@ -118,18 +118,16 @@ public MetadataBasedWriter(string content = "Content", bool canWrite = true) _canWrite = canWrite; } - public async ValueTask TryWriteAsync(ProblemDetailsContext context) + public bool CanWrite(ProblemDetailsContext context) { var metadata = context.AdditionalMetadata?.GetMetadata() ?? context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - if (metadata != null && _canWrite) - { - await context.HttpContext.Response.WriteAsJsonAsync(_content); - return true; - } + return metadata != null && _canWrite; - return false; } + + public ValueTask WriteAsync(ProblemDetailsContext context) + => new(context.HttpContext.Response.WriteAsJsonAsync(_content)); } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs index 0360fd05b5dc..f480c386358e 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultApiProblemDetailsWriter.cs @@ -32,24 +32,23 @@ public DefaultApiProblemDetailsWriter( _apiBehaviorOptions = apiBehaviorOptions.Value; } - public async ValueTask TryWriteAsync(ProblemDetailsContext context) + public bool CanWrite(ProblemDetailsContext context) { var controllerAttribute = context.AdditionalMetadata?.GetMetadata() ?? context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - if (controllerAttribute == null) - { - return false; - } + return controllerAttribute != null; + } + public ValueTask WriteAsync(ProblemDetailsContext context) + { var apiControllerAttribute = context.AdditionalMetadata?.GetMetadata() ?? context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); if (apiControllerAttribute is null || _apiBehaviorOptions.SuppressMapClientErrors) { - // In this case we don't want to move - // to the next registered writers. - return true; + // In this case we don't want to write + return ValueTask.CompletedTask; } // Recreating the problem details to get all customizations @@ -83,10 +82,9 @@ public async ValueTask TryWriteAsync(ProblemDetailsContext context) if (selectedFormatter == null) { - return false; + return ValueTask.CompletedTask; } - await selectedFormatter.WriteAsync(formatterContext); - return true; + return new ValueTask(selectedFormatter.WriteAsync(formatterContext)); } } diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs index e69f9c0d4795..e918bcf5c49e 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs @@ -38,7 +38,7 @@ public async Task WriteAsync_Works() }; //Act - await writer.TryWriteAsync(problemDetailsContext); + await writer.WriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -69,7 +69,7 @@ public async Task WriteAsync_AddExtensions() }; //Act - await writer.TryWriteAsync(problemDetailsContext); + await writer.WriteAsync(problemDetailsContext); //Assert stream.Position = 0; @@ -80,7 +80,7 @@ public async Task WriteAsync_AddExtensions() } [Fact] - public async Task WriteAsync_ReturnsFalseAndSkip_WhenNotController() + public void CanWrite_ReturnsFalse_WhenNotController() { // Arrange var writer = GetWriter(); @@ -88,16 +88,14 @@ public async Task WriteAsync_ReturnsFalseAndSkip_WhenNotController() var context = CreateContext(stream, metadata: EndpointMetadataCollection.Empty); //Act - var result = await writer.TryWriteAsync(new() { HttpContext = context }); + var result = writer.CanWrite(new() { HttpContext = context }); //Assert Assert.False(result); - Assert.Equal(0, stream.Position); - Assert.Equal(0, stream.Length); } [Fact] - public async Task WriteAsync_ReturnsTrueAndSkip_WhenNotApiController() + public void CanWrite_ReturnsTrue_WhenController() { // Arrange var writer = GetWriter(); @@ -105,16 +103,30 @@ public async Task WriteAsync_ReturnsTrueAndSkip_WhenNotApiController() var context = CreateContext(stream, metadata: new EndpointMetadataCollection(new ControllerAttribute())); //Act - var result = await writer.TryWriteAsync(new() { HttpContext = context }); + var result = writer.CanWrite(new() { HttpContext = context }); //Assert Assert.True(result); + } + + [Fact] + public async Task WriteAsync_Skip_WhenNotApiController() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream, metadata: new EndpointMetadataCollection(new ControllerAttribute())); + + //Act + await writer.WriteAsync(new() { HttpContext = context }); + + //Assert Assert.Equal(0, stream.Position); Assert.Equal(0, stream.Length); } [Fact] - public async Task WriteAsync_ReturnsTrueAndSkip_WhenSuppressMapClientErrors() + public async Task WriteAsync_Skip_WhenSuppressMapClientErrors() { // Arrange var writer = GetWriter(options: new ApiBehaviorOptions() { SuppressMapClientErrors = true }); @@ -122,16 +134,15 @@ public async Task WriteAsync_ReturnsTrueAndSkip_WhenSuppressMapClientErrors() var context = CreateContext(stream); //Act - var result = await writer.TryWriteAsync(new() { HttpContext = context }); + await writer.WriteAsync(new() { HttpContext = context }); //Assert - Assert.True(result); Assert.Equal(0, stream.Position); Assert.Equal(0, stream.Length); } [Fact] - public async Task WriteAsync_ReturnsFalseAndSkip_WhenNoFormatter() + public async Task WriteAsync_Skip_WhenNoFormatter() { // Arrange var formatter = new Mock(); @@ -141,10 +152,9 @@ public async Task WriteAsync_ReturnsFalseAndSkip_WhenNoFormatter() var context = CreateContext(stream); //Act - var result = await writer.TryWriteAsync(new() { HttpContext = context }); + await writer.WriteAsync(new() { HttpContext = context }); //Assert - Assert.False(result); Assert.Equal(0, stream.Position); Assert.Equal(0, stream.Length); }