Skip to content
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

Merged
merged 65 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
15126b5
MVC Changes
brunolins16 Jun 1, 2022
00e7615
ExceptionHandler changes
brunolins16 Jun 1, 2022
9adc269
Routing changes
brunolins16 Jun 1, 2022
c430ae2
Http.Extensions changes
brunolins16 Jun 1, 2022
a8939e5
Minimal APi draft changes
brunolins16 Jun 1, 2022
79690d8
HttpResults changes
brunolins16 Jun 1, 2022
c548e2b
New ProblemDetails project
brunolins16 Jun 1, 2022
7f368a8
Using ProblemDetailsOptions in mvc
brunolins16 Jun 2, 2022
acb590b
ProblemDetails project simplification
brunolins16 Jun 2, 2022
62eb53e
Using metadata
brunolins16 Jun 7, 2022
9618302
Using metadata
brunolins16 Jun 7, 2022
a747f04
Merge branch 'main' into brunolins16/issues/32957
brunolins16 Jun 7, 2022
8e25910
Latest version
brunolins16 Jun 10, 2022
22b0932
Removing changes
brunolins16 Jun 10, 2022
f6a5c37
Initial cleanup
brunolins16 Jun 10, 2022
b2fabfb
Merge branch 'main' into brunolins16/issues/32957
brunolins16 Jun 13, 2022
c13a1bd
Clean up
brunolins16 Jun 16, 2022
80aa845
Public api clean up
brunolins16 Jun 17, 2022
e0d4333
Updating public API
brunolins16 Jun 17, 2022
3fa6af0
More clean ups
brunolins16 Jun 17, 2022
89fd8ec
Adding initial unit tests
brunolins16 Jun 20, 2022
dca5c73
Updating unit tests
brunolins16 Jun 21, 2022
e885b3a
Adding more unit tests
brunolins16 Jun 23, 2022
a593c2b
Merge branch 'main' into brunolins16/issues/32957
brunolins16 Jun 23, 2022
9877120
Cleanup
brunolins16 Jun 23, 2022
2e0c98e
Clean up
brunolins16 Jun 23, 2022
e8f0e1a
clean up
brunolins16 Jun 23, 2022
6316af7
clean up
brunolins16 Jun 23, 2022
0ed099e
Removing nullable
brunolins16 Jun 23, 2022
a883706
Simplifying public api
brunolins16 Jun 25, 2022
1ecb0e0
API Review feedback
brunolins16 Jun 27, 2022
cfb8f88
Merge branch 'main' into brunolins16/issues/32957
brunolins16 Jun 27, 2022
6813d58
API review feedback
brunolins16 Jun 28, 2022
6b15f58
Clean up
brunolins16 Jun 28, 2022
a50ef62
Clean up
brunolins16 Jun 28, 2022
6565091
clean up
brunolins16 Jun 28, 2022
550dd9d
Reusing Endpoint & EndpointMetadataCollection
brunolins16 Jun 29, 2022
f9fd5f6
Fix build issues
brunolins16 Jun 29, 2022
804ccac
Updates based on docs/Trimming.md
brunolins16 Jun 29, 2022
54e369d
Adding Functional tests
brunolins16 Jun 30, 2022
1e695aa
Fix unittest
brunolins16 Jun 30, 2022
2a743db
Seal context
brunolins16 Jun 30, 2022
8bbe54c
Seal ProblemMetadata
brunolins16 Jun 30, 2022
a04b7ae
PR Feeback
brunolins16 Jul 6, 2022
cf188c2
API Review
brunolins16 Jul 8, 2022
5be8bd9
Clean up
brunolins16 Jul 8, 2022
86401b7
Fixing publicapi warnings
brunolins16 Jul 8, 2022
72b97a1
Merge branch 'main' into brunolins16/issues/32957
brunolins16 Jul 8, 2022
3f58002
Clean up
brunolins16 Jul 11, 2022
7dadaed
PR Feedback
brunolins16 Jul 11, 2022
5cb3286
Fixing build
brunolins16 Jul 11, 2022
e03c3e3
Fix unit test
brunolins16 Jul 11, 2022
988a20e
Fix unit tests
brunolins16 Jul 11, 2022
8c823a4
PR review
brunolins16 Jul 11, 2022
483f54e
Fixing JsonSerializationContext issues
brunolins16 Jul 11, 2022
35de3fe
Adding statuscode 405
brunolins16 Jul 12, 2022
1ba1f10
Update src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs
brunolins16 Jul 12, 2022
2455ccb
PR review
brunolins16 Jul 12, 2022
cd92e30
Apply suggestions from code review
brunolins16 Jul 12, 2022
d35ff2b
Merge branch 'main' into brunolins16/issues/32957
brunolins16 Jul 13, 2022
55cb7da
Fixing bad merge
brunolins16 Jul 13, 2022
8585070
Fix bad merge
brunolins16 Jul 13, 2022
2c12741
Adding analysis.NextMiddlewareName
brunolins16 Jul 13, 2022
4b84cf3
PR review
brunolins16 Jul 13, 2022
1fe3f3b
Changing to CanWrite/WriteAsync
brunolins16 Jul 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
<Compile Include="$(SharedSourceRoot)PropertyHelper\**\*.cs" />
<Compile Include="$(SharedSourceRoot)\UrlDecoder\UrlDecoder.cs" Link="UrlDecoder.cs" />
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
<Compile Include="$(SharedSourceRoot)ProblemDetails\*JsonConverter.cs" LinkBase="ProblemDetails\Converters" />
brunolins16 marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

<ItemGroup>
Expand Down
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;
}
}
29 changes: 29 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ 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>() -> T!
Microsoft.AspNetCore.Http.HttpValidationProblemDetails
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary<string!, string![]!>!
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary<string!, string![]!>! errors) -> void
Microsoft.AspNetCore.Http.IBindableFromHttpContext<TSelf>
Microsoft.AspNetCore.Http.IBindableFromHttpContext<TSelf>.BindAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Reflection.ParameterInfo! parameter) -> System.Threading.Tasks.ValueTask<TSelf?>
Microsoft.AspNetCore.Http.IContentTypeHttpResult
Microsoft.AspNetCore.Http.IContentTypeHttpResult.ContentType.get -> string?
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.IProblemDetailsWriter.TryWriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask<bool>
Microsoft.AspNetCore.Http.INestedHttpResult
Microsoft.AspNetCore.Http.INestedHttpResult.Result.get -> Microsoft.AspNetCore.Http.IResult!
Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask<object?>
Expand All @@ -30,6 +38,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.Http.RouteHandlerContext
Microsoft.AspNetCore.Http.RouteHandlerContext.ApplicationServices.get -> System.IServiceProvider!
Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> System.Collections.Generic.IList<object!>!
Expand All @@ -48,6 +64,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<string!, object?>!
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<object?>!
abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.GetArgument<T>(int index) -> T
abstract Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -111,15 +111,16 @@ public void ReadUsingJsonSerializerWorks()
// Act
var problemDetails = JsonSerializer.Deserialize<HttpValidationProblemDetails>(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(
problemDetails.Extensions,
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand All @@ -56,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());
});
}

Expand All @@ -75,7 +76,9 @@ public void Read_UsingJsonSerializerWorks()
// Act
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, JsonSerializerOptions);

Assert.Equal(type, problemDetails.Type);
// Assert
Assert.NotNull(problemDetails);
Assert.Equal(type, problemDetails!.Type);
Assert.Equal(title, problemDetails.Title);
Assert.Equal(status, problemDetails.Status);
Assert.Equal(instance, problemDetails.Instance);
Expand All @@ -85,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());
});
}

Expand All @@ -105,6 +108,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);
Expand All @@ -113,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());
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
<Compile Include="$(SharedSourceRoot)ParameterBindingMethodCache.cs" LinkBase="Shared"/>
<Compile Include="$(SharedSourceRoot)PropertyAsParameterInfo.cs" LinkBase="Shared"/>
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)ProblemDetailsJsonConverter.cs" LinkBase="Shared"/>
<Compile Include="$(SharedSourceRoot)HttpValidationProblemDetailsJsonConverter.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)TypeNameHelper\TypeNameHelper.cs" LinkBase="Shared"/>
<Compile Include="$(SharedSourceRoot)ProblemDetails\ProblemDetailsDefaults.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
Expand Down
63 changes: 63 additions & 0 deletions src/Http/Http.Extensions/src/ProblemDetailsDefaultWriter.cs
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 })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ProblemDetails.Extensions causes issues during the serialization using JsonSerializerContext, maybe because of this dotnet/runtime#71714, I decided to use the source generation only when no extensions are defined.

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
{ }
}
16 changes: 16 additions & 0 deletions src/Http/Http.Extensions/src/ProblemDetailsOptions.cs
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; }
}
35 changes: 35 additions & 0 deletions src/Http/Http.Extensions/src/ProblemDetailsService.cs
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;
}
}
}
}
Loading