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 dc51c9a2f0af..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,6 +26,8 @@ Microsoft.AspNetCore.Http.HttpResponse
+
+
diff --git a/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs b/src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.cs
similarity index 100%
rename from src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs
rename to src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.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..564ee29403ad
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Http;
+
+///
+/// Defines a type that provide functionality to
+/// create a response.
+///
+public interface IProblemDetailsService
+{
+ ///
+ /// 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/IProblemDetailsWriter.cs b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs
new file mode 100644
index 000000000000..852e6b2e5789
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsWriter.cs
@@ -0,0 +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;
+
+///
+/// Defines a type that write a
+/// payload to the current .
+///
+public interface IProblemDetailsWriter
+{
+ ///
+ /// Write a response to the current context
+ ///
+ /// The associated with the current request/response.
+ 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.Extensions/src/ProblemDetails.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs
similarity index 100%
rename from src/Http/Http.Extensions/src/ProblemDetails.cs
rename to src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs
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..62058cf8ec36
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.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.Mvc;
+
+namespace Microsoft.AspNetCore.Http;
+
+///
+/// Represent the current problem details context for the request.
+///
+public sealed class ProblemDetailsContext
+{
+ private ProblemDetails? _problemDetails;
+
+ ///
+ /// The associated with the current request being processed by the filter.
+ ///
+ public required HttpContext HttpContext { get; init; }
+
+ ///
+ /// A collection of additional arbitrary metadata associated with the current request endpoint.
+ ///
+ public EndpointMetadataCollection? AdditionalMetadata { get; init; }
+
+ ///
+ /// An instance of that will be
+ /// used during the response payload generation.
+ ///
+ public ProblemDetails ProblemDetails
+ {
+ get => _problemDetails ??= new ProblemDetails();
+ init => _problemDetails = value;
+ }
+}
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 9722a6e3718d..43a78d85ac62 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -21,6 +21,10 @@ Microsoft.AspNetCore.Http.EndpointFilterInvocationContext
Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.EndpointFilterInvocationContext() -> void
Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator.Current.get -> object!
Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T!
+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.IBindableFromHttpContext
Microsoft.AspNetCore.Http.IBindableFromHttpContext.BindAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Reflection.ParameterInfo! parameter) -> System.Threading.Tasks.ValueTask
Microsoft.AspNetCore.Http.IContentTypeHttpResult
@@ -30,8 +34,13 @@ Microsoft.AspNetCore.Http.IEndpointFilter.InvokeAsync(Microsoft.AspNetCore.Http.
Microsoft.AspNetCore.Http.IFileHttpResult
Microsoft.AspNetCore.Http.IFileHttpResult.ContentType.get -> string?
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.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
@@ -42,6 +51,14 @@ 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.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() -> 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
@@ -51,6 +68,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
override Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.Arguments.get -> System.Collections.Generic.IList
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..68002af77b96
--- /dev/null
+++ b/src/Http/Http.Extensions/src/ProblemDetailsService.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 System.Linq;
+
+internal sealed class ProblemDetailsService : IProblemDetailsService
+{
+ private readonly IProblemDetailsWriter[] _writers;
+
+ public ProblemDetailsService(
+ IEnumerable writers)
+ {
+ _writers = writers.ToArray();
+ }
+
+ 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 ||
+ _writers.Length == 0)
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ IProblemDetailsWriter? selectedWriter = null;
+
+ if (_writers.Length == 1)
+ {
+ selectedWriter = _writers[0];
+
+ return selectedWriter.CanWrite(context) ?
+ selectedWriter.WriteAsync(context) :
+ ValueTask.CompletedTask;
+ }
+
+ for (var i = 0; i < _writers.Length; i++)
+ {
+ 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/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs
new file mode 100644
index 000000000000..c8508a3fa668
--- /dev/null
+++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+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(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 so that additional calls can be chained.
+ public static IServiceCollection AddProblemDetails(
+ this IServiceCollection services,
+ Action? configure)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ // Adding default services;
+ services.TryAddSingleton();
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+
+ if (configure != null)
+ {
+ services.Configure(configure);
+ }
+
+ return services;
+ }
+}
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")]
diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
index 00c713fbc424..4a1707193af5 100644
--- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
@@ -1,4 +1,12 @@
#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.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
diff --git a/src/Http/Http.Results/src/ProblemHttpResult.cs b/src/Http/Http.Results/src/ProblemHttpResult.cs
index 54c2259cd311..b0a42aaa050e 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, IStatusCodeHttpResult, IContent
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 71e5396d0238..36631d141e8b 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/Routing/src/Matching/AcceptsMatcherPolicy.cs b/src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs
index 67c032148016..87931a0306f0 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,8 +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 cc89d26a26a9..cea2791414da 100644
--- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs
+++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs
@@ -404,12 +404,10 @@ 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;
-
+ context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
return Task.CompletedTask;
},
EndpointMetadataCollection.Empty,
diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs
index b8d748b7c729..39e1ecb24dd6 100644
--- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs
+++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs
@@ -26,7 +26,8 @@ public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBui
throw new ArgumentNullException(nameof(app));
}
- return app.UseMiddleware();
+ app.Properties["analysis.NextMiddlewareName"] = "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware";
+ return app.UseMiddleware();
}
///
@@ -52,6 +53,7 @@ public static IApplicationBuilder UseDeveloperExceptionPage(
throw new ArgumentNullException(nameof(options));
}
- return app.UseMiddleware(Options.Create(options));
+ app.Properties["analysis.NextMiddlewareName"] = "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware";
+ 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 979c71a2ad24..5ac530518fd9 100644
--- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs
+++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs
@@ -2,19 +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.Routing;
-using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.StackTrace.Sources;
-using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Diagnostics;
@@ -23,24 +15,17 @@ 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 DeveloperExceptionPageMiddlewareImpl _innerMiddlewareImpl;
///
/// 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,
@@ -49,34 +34,14 @@ public DeveloperExceptionPageMiddleware(
DiagnosticSource diagnosticSource,
IEnumerable filters)
{
- 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;
-
- 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);
}
///
@@ -84,204 +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)))
- {
- 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());
- }
-
- if (errorContext.Exception is ICompilationException compilationException)
- {
- return DisplayCompilationException(httpContext, compilationException);
- }
-
- return DisplayRuntimeException(httpContext, errorContext.Exception);
- }
-
- 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 602b1bc2172d..b84e60b0c7e3 100644
--- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs
+++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs
@@ -104,6 +104,10 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a
private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBuilder app, IOptions? options)
{
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)
{
@@ -131,15 +135,15 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui
options.Value.ExceptionHandler = builder.Build();
}
- return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener).Invoke;
+ return new ExceptionHandlerMiddlewareImpl(next, loggerFactory, options, diagnosticListener, problemDetailsService).Invoke;
});
}
if (options is null)
{
- return app.UseMiddleware();
+ return app.UseMiddleware();
}
- return app.UseMiddleware(options);
+ return app.UseMiddleware(options);
}
}
diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs
index d7dd4ff90e1d..5b9f87514326 100644
--- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs
+++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs
@@ -2,11 +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.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -17,11 +14,7 @@ namespace Microsoft.AspNetCore.Diagnostics;
///
public class ExceptionHandlerMiddleware
{
- private readonly RequestDelegate _next;
- private readonly ExceptionHandlerOptions _options;
- private readonly ILogger _logger;
- private readonly Func _clearCacheHeadersDelegate;
- private readonly DiagnosticListener _diagnosticListener;
+ private readonly ExceptionHandlerMiddlewareImpl _innerMiddlewareImpl;
///
/// Creates a new
@@ -36,22 +29,12 @@ public ExceptionHandlerMiddleware(
IOptions options,
DiagnosticListener diagnosticListener)
{
- _next = next;
- _options = options.Value;
- _logger = loggerFactory.CreateLogger();
- _clearCacheHeadersDelegate = ClearCacheHeaders;
- _diagnosticListener = diagnosticListener;
- if (_options.ExceptionHandler == null)
- {
- if (_options.ExceptionHandlingPath == null)
- {
- throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly);
- }
- else
- {
- _options.ExceptionHandler = _next;
- }
- }
+ _innerMiddlewareImpl = new (
+ next,
+ loggerFactory,
+ options,
+ diagnosticListener,
+ problemDetailsService: null);
}
///
@@ -59,135 +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 = StatusCodes.Status500InternalServerError;
- context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);
-
- 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)
- {
- 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/Resources.resx b/src/Middleware/Diagnostics/src/Resources.resx
index 6e62f494942e..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 => { ... });'.
+ 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.
@@ -280,4 +280,4 @@
Name
-
+
\ No newline at end of file
diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs
index 56c00d2ea67a..496e02d763e3 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,27 @@ 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(new ()
+ {
+ HttpContext = context.HttpContext,
+ ProblemDetails = { Status = statusCode }
+ });
+ }
+
+ // TODO: Render with a pre-compiled html razor view.
+ if (!context.HttpContext.Response.HasStarted)
+ {
+ var body = BuildResponseBody(statusCode);
- context.HttpContext.Response.ContentType = "text/plain";
- return context.HttpContext.Response.WriteAsync(body);
+ context.HttpContext.Response.ContentType = "text/plain";
+ await context.HttpContext.Response.WriteAsync(body);
+ }
};
}
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..06b54ccfd05f
--- /dev/null
+++ b/src/Middleware/Diagnostics/test/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.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 System.Net;
+using System.Net.Http;
+using Microsoft.AspNetCore.Mvc;
+using System.Net.Http.Json;
+using System.Net.Http.Headers;
+
+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");
+ 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);
+ }
+}
diff --git a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs
index 193999955a41..3299200c2827 100644
--- a/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs
+++ b/src/Middleware/Diagnostics/test/FunctionalTests/StatusCodeSampleTest.cs
@@ -1,9 +1,13 @@
-// 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;
+using Microsoft.Net.Http.Headers;
+using System.Net.Http.Headers;
namespace Microsoft.AspNetCore.Diagnostics.FunctionalTests;
@@ -70,4 +74,22 @@ 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?statuscode={httpStatusCode}");
+ 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.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..39fb247b9283 100644
--- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj
+++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj
@@ -1,4 +1,4 @@
-
+
$(DefaultNetCoreTargetFramework)
diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs
index f3592f61c7b8..a81b1593ca4d 100644
--- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs
+++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Startup.cs
@@ -7,6 +7,11 @@ namespace DeveloperExceptionPageSample;
public class Startup
{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddProblemDetails();
+ }
+
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
@@ -21,7 +26,8 @@ public void Configure(IApplicationBuilder app)
c => null,
RoutePatternFactory.Parse("/"),
0,
- new EndpointMetadataCollection(new HttpMethodMetadata(new[] { "GET", "POST" })),
+ new EndpointMetadataCollection(
+ 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 907fa264f876..2faf636c4914 100644
--- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj
+++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/ExceptionHandlerSample.csproj
@@ -1,4 +1,4 @@
-
+
$(DefaultNetCoreTargetFramework)
@@ -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/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs
index 4d9402a535c3..b54320e19583 100644
--- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs
+++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Startup.cs
@@ -53,20 +53,5 @@ public void Configure(IApplicationBuilder app)
await context.Response.WriteAsync("