-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Adding ProblemDetailsService #42384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding ProblemDetailsService #42384
Changes from 57 commits
15126b5
00e7615
9adc269
c430ae2
a8939e5
79690d8
c548e2b
7f368a8
acb590b
62eb53e
9618302
a747f04
8e25910
22b0932
f6a5c37
b2fabfb
c13a1bd
80aa845
e0d4333
3fa6af0
89fd8ec
dca5c73
e885b3a
a593c2b
9877120
2e0c98e
e8f0e1a
6316af7
0ed099e
a883706
1ecb0e0
cfb8f88
6813d58
6b15f58
a50ef62
6565091
550dd9d
f9fd5f6
804ccac
54e369d
1e695aa
2a743db
8bbe54c
a04b7ae
cf188c2
5be8bd9
86401b7
72b97a1
3f58002
7dadaed
5cb3286
e03c3e3
988a20e
8c823a4
483f54e
35de3fe
1ba1f10
2455ccb
cd92e30
d35ff2b
55cb7da
8585070
2c12741
4b84cf3
1fe3f3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// Defines a type that provide functionality to | ||
/// create a <see cref="Mvc.ProblemDetails"/> response. | ||
/// </summary> | ||
public interface IProblemDetailsService | ||
{ | ||
/// <summary> | ||
/// Try to write a <see cref="Mvc.ProblemDetails"/> response to the current context, | ||
/// using the registered <see cref="IProblemDetailsWriter"/> services. | ||
/// </summary> | ||
/// <param name="context">The <see cref="ProblemDetailsContext"/> associated with the current request/response.</param> | ||
/// <remarks>The <see cref="IProblemDetailsWriter"/> registered services | ||
/// are processed in sequence and the processing is completed when: | ||
/// <list type="bullet">One of them reports that the response was written successfully, or.</list> | ||
/// <list type="bullet">All <see cref="IProblemDetailsWriter"/> were executed and none of them was able to write the response successfully.</list> | ||
/// </remarks> | ||
ValueTask WriteAsync(ProblemDetailsContext context); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// Defines a type that write a <see cref="Mvc.ProblemDetails"/> | ||
/// payload to the current <see cref="HttpContext.Response"/>. | ||
/// </summary> | ||
public interface IProblemDetailsWriter | ||
{ | ||
/// <summary> | ||
/// Write a <see cref="Mvc.ProblemDetails"/> response to the current context | ||
/// </summary> | ||
/// <param name="context">The <see cref="ProblemDetailsContext"/> associated with the current request/response.</param> | ||
/// <returns>Flag that indicates if the response was started.</returns> | ||
ValueTask<bool> TryWriteAsync(ProblemDetailsContext context); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// Represent the current problem details context for the request. | ||
/// </summary> | ||
public sealed class ProblemDetailsContext | ||
{ | ||
private ProblemDetails? _problemDetails; | ||
|
||
/// <summary> | ||
/// The <see cref="HttpContext"/> associated with the current request being processed by the filter. | ||
/// </summary> | ||
public required HttpContext HttpContext { get; init; } | ||
|
||
/// <summary> | ||
/// A collection of additional arbitrary metadata associated with the current request endpoint. | ||
/// </summary> | ||
public EndpointMetadataCollection? AdditionalMetadata { get; init; } | ||
|
||
/// <summary> | ||
/// A instance of <see cref="ProblemDetails"/> that will be | ||
brunolins16 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// used during the response payload generation. | ||
/// </summary> | ||
public ProblemDetails ProblemDetails | ||
{ | ||
get => _problemDetails ??= new ProblemDetails(); | ||
init => _problemDetails = value; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.Diagnostics.CodeAnalysis; | ||
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 | ||
brunolins16 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private static readonly MediaTypeHeaderValue _jsonMediaType = new("application/json"); | ||
private static readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json"); | ||
private readonly ProblemDetailsOptions _options; | ||
|
||
public ProblemDetailsDefaultWriter(IOptions<ProblemDetailsOptions> 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<bool> TryWriteAsync(ProblemDetailsContext context) | ||
{ | ||
var httpContext = context.HttpContext; | ||
var acceptHeader = httpContext.Request.Headers.Accept.GetList<MediaTypeHeaderValue>(); | ||
|
||
if (acceptHeader == null || | ||
!acceptHeader.Any(h => _jsonMediaType.IsSubsetOf(h) || _problemDetailsJsonMediaType.IsSubsetOf(h))) | ||
{ | ||
return false; | ||
} | ||
|
||
ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); | ||
_options.CustomizeProblemDetails?.Invoke(context); | ||
|
||
if (context.ProblemDetails.Extensions is { Count: 0 }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since I am ok to remove this and always use the reflection-based if it is a better approach. |
||
{ | ||
// 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, | ||
options: null, | ||
contentType: "application/problem+json"); | ||
|
||
return httpContext.Response.HasStarted; | ||
} | ||
|
||
[JsonSerializable(typeof(ProblemDetails))] | ||
internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext | ||
{ } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// Options for controlling the behavior of <see cref="IProblemDetailsService.WriteAsync(ProblemDetailsContext)"/> | ||
/// and similar methods. | ||
/// </summary> | ||
public class ProblemDetailsOptions | ||
{ | ||
/// <summary> | ||
/// The operation that customizes the current <see cref="Mvc.ProblemDetails"/> instance. | ||
/// </summary> | ||
public Action<ProblemDetailsContext>? CustomizeProblemDetails { get; set; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IProblemDetailsWriter> _writers; | ||
|
||
public ProblemDetailsService( | ||
IEnumerable<IProblemDetailsWriter> 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; | ||
} | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.