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

Backport/pr 42384 to release/7.0 preview7 #42809

Merged
merged 1 commit into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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,8 @@ 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\HttpValidationProblemDetailsJsonConverter.cs" LinkBase="ProblemDetails\Converters" />
<Compile Include="$(SharedSourceRoot)ProblemDetails\ProblemDetailsJsonConverter.cs" LinkBase="ProblemDetails\Converters" />
</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,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;

/// <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>
ValueTask WriteAsync(ProblemDetailsContext context);

/// <summary>
/// Determines whether this instance can write a <see cref="Mvc.ProblemDetails"/> to the current context.
/// </summary>
/// <param name="context">The <see cref="ProblemDetailsContext"/> associated with the current request/response.</param>
/// <returns>Flag that indicates if that the writer can write to the current <see cref="ProblemDetailsContext"/>.</returns>
bool CanWrite(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>
/// An instance of <see cref="ProblemDetails"/> that will be
/// used during the response payload generation.
/// </summary>
public ProblemDetails ProblemDetails
{
get => _problemDetails ??= new ProblemDetails();
init => _problemDetails = value;
}
}
30 changes: 30 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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>() -> 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
Expand All @@ -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
Expand All @@ -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<System.Collections.Generic.KeyValuePair<string!, object?>>? values) -> void
Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string?>>? values) -> void
Expand All @@ -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<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
override Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.Arguments.get -> System.Collections.Generic.IList<object?>!
override Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.GetArgument<T>(int index) -> T
override Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
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
64 changes: 64 additions & 0 deletions src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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 DefaultProblemDetailsWriter : IProblemDetailsWriter
{
private static readonly MediaTypeHeaderValue _jsonMediaType = new("application/json");
private static readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json");
private readonly ProblemDetailsOptions _options;

public DefaultProblemDetailsWriter(IOptions<ProblemDetailsOptions> options)
{
_options = options.Value;
}

public bool CanWrite(ProblemDetailsContext context)
{
var httpContext = context.HttpContext;
var acceptHeader = httpContext.Request.Headers.Accept.GetList<MediaTypeHeaderValue>();

if (acceptHeader?.Any(h => _jsonMediaType.IsSubsetOf(h) || _problemDetailsJsonMediaType.IsSubsetOf(h)) == true)
{
return true;
}

return false;
}

[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "JSON serialization of ProblemDetails.Extensions might require types that cannot be statically analyzed and we need to fallback" +
"to reflection-based. The ProblemDetailsConverter is marked as RequiresUnreferencedCode already.")]
public ValueTask WriteAsync(ProblemDetailsContext context)
{
var httpContext = context.HttpContext;
ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode);
_options.CustomizeProblemDetails?.Invoke(context);

if (context.ProblemDetails.Extensions is { Count: 0 })
{
// We can use the source generation in this case
return new ValueTask(httpContext.Response.WriteAsJsonAsync(
context.ProblemDetails,
ProblemDetailsJsonContext.Default.ProblemDetails,
contentType: "application/problem+json"));
}

return new ValueTask(httpContext.Response.WriteAsJsonAsync(
context.ProblemDetails,
options: null,
contentType: "application/problem+json"));
}

[JsonSerializable(typeof(ProblemDetails))]
internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext
{ }
}
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
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; }
}
Loading