diff --git a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs index ea3e318a9a2d..25b0f5220604 100644 --- a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs @@ -1,11 +1,8 @@ // 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.Runtime.CompilerServices; using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -15,11 +12,14 @@ internal sealed partial class DefaultProblemDetailsWriter : IProblemDetailsWrite { private static readonly MediaTypeHeaderValue _jsonMediaType = new("application/json"); private static readonly MediaTypeHeaderValue _problemDetailsJsonMediaType = new("application/problem+json"); + private readonly ProblemDetailsOptions _options; + private readonly JsonSerializerOptions _serializerOptions; - public DefaultProblemDetailsWriter(IOptions options) + public DefaultProblemDetailsWriter(IOptions options, IOptions jsonOptions) { _options = options.Value; + _serializerOptions = jsonOptions.Value.SerializerOptions; } public bool CanWrite(ProblemDetailsContext context) @@ -49,49 +49,17 @@ public bool CanWrite(ProblemDetailsContext context) return false; } - [UnconditionalSuppressMessage("Trimming", "IL2026", - Justification = "JSON serialization of ProblemDetails.Extensions might require types that cannot be statically analyzed. The property is annotated with RequiresUnreferencedCode.")] - [UnconditionalSuppressMessage("Trimming", "IL3050", - Justification = "JSON serialization of ProblemDetails.Extensions might require types that cannot be statically analyzed. The property is annotated with RequiresDynamicCode.")] public ValueTask WriteAsync(ProblemDetailsContext context) { var httpContext = context.HttpContext; ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); _options.CustomizeProblemDetails?.Invoke(context); - // Use source generation serialization in two scenarios: - // 1. There are no extensions. Source generation is faster and works well with trimming. - // 2. Native AOT. In this case only the data types specified on ProblemDetailsJsonContext will work. - if (context.ProblemDetails.Extensions is { Count: 0 } || !RuntimeFeature.IsDynamicCodeSupported) - { - return new ValueTask(httpContext.Response.WriteAsJsonAsync( - context.ProblemDetails, - ProblemDetailsJsonContext.Default.ProblemDetails, - contentType: "application/problem+json")); - } + var problemDetailsType = context.ProblemDetails.GetType(); return new ValueTask(httpContext.Response.WriteAsJsonAsync( context.ProblemDetails, - options: null, + _serializerOptions.GetTypeInfo(problemDetailsType), contentType: "application/problem+json")); } - - // Additional values are specified on JsonSerializerContext to support some values for extensions. - // For example, the DeveloperExceptionMiddleware serializes its complex type to JsonElement, which problem details then needs to serialize. - [JsonSerializable(typeof(ProblemDetails))] - [JsonSerializable(typeof(JsonElement))] - [JsonSerializable(typeof(string))] - [JsonSerializable(typeof(decimal))] - [JsonSerializable(typeof(float))] - [JsonSerializable(typeof(double))] - [JsonSerializable(typeof(int))] - [JsonSerializable(typeof(long))] - [JsonSerializable(typeof(Guid))] - [JsonSerializable(typeof(Uri))] - [JsonSerializable(typeof(TimeSpan))] - [JsonSerializable(typeof(DateTime))] - [JsonSerializable(typeof(DateTimeOffset))] - internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext - { - } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index ad718418c09e..a47c77af8f96 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -33,7 +33,7 @@ public class JsonOptions /// /// Gets the . /// - public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions); + public JsonSerializerOptions SerializerOptions { get; internal set; } = new JsonSerializerOptions(DefaultSerializerOptions); #pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml #pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. diff --git a/src/Http/Http.Extensions/src/ProblemDetailsJsonContext.cs b/src/Http/Http.Extensions/src/ProblemDetailsJsonContext.cs new file mode 100644 index 000000000000..b247e359e483 --- /dev/null +++ b/src/Http/Http.Extensions/src/ProblemDetailsJsonContext.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.AspNetCore.Http; + +[JsonSerializable(typeof(ProblemDetails))] +[JsonSerializable(typeof(HttpValidationProblemDetails))] +// Additional values are specified on JsonSerializerContext to support some values for extensions. +// For example, the DeveloperExceptionMiddleware serializes its complex type to JsonElement, which problem details then needs to serialize. +[JsonSerializable(typeof(JsonElement))] +internal sealed partial class ProblemDetailsJsonContext : JsonSerializerContext +{ +} diff --git a/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs new file mode 100644 index 000000000000..b2fd02ba260e --- /dev/null +++ b/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class ProblemDetailsJsonOptionsSetup : IPostConfigureOptions +{ + public void PostConfigure(string? name, JsonOptions options) + { + if (options.SerializerOptions.TypeInfoResolver is not null) + { + if (options.SerializerOptions.IsReadOnly) + { + options.SerializerOptions = new(options.SerializerOptions); + } + + // Combine the current resolver with our internal problem details context + options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver!, ProblemDetailsJsonContext.Default); + } + } +} diff --git a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index c8508a3fa668..f76540a67838 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -38,6 +40,7 @@ public static IServiceCollection AddProblemDetails( // Adding default services; services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton, ProblemDetailsJsonOptionsSetup>()); if (configure != null) { diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index 66ca16837ec4..2e2a97f4365a 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -7,10 +7,14 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Http.Json; +using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Http.Extensions.Tests; -public class DefaultProblemDetailsWriterTest +public partial class DefaultProblemDetailsWriterTest { [Fact] public async Task WriteAsync_Works() @@ -47,6 +51,284 @@ public async Task WriteAsync_Works() Assert.Equal(expectedProblem.Instance, problemDetails.Instance); } + [Fact] + public async Task WriteAsync_Works_WithJsonContext() + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.AddContext(); + + var writer = GetWriter(jsonOptions: options); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new ProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + }; + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + } + + [Fact] + public async Task WriteAsync_Works_WithMultipleJsonContext() + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(CustomProblemDetailsContext.Default, CustomProblemDetailsContext2.Default, ProblemDetailsJsonContext.Default); + + var writer = GetWriter(jsonOptions: options); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new ProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + }; + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + } + + [Fact] + public async Task WriteAsync_Works_WithHttpValidationProblemDetails() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new HttpValidationProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + }; + expectedProblem.Errors.Add("sample", new string[] { "error-message" }); + + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + Assert.Equal(expectedProblem.Errors, problemDetails.Errors); + } + + [Fact] + public async Task WriteAsync_Works_WithHttpValidationProblemDetails_AndJsonContext() + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.AddContext(); + + var writer = GetWriter(jsonOptions: options); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new HttpValidationProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + }; + expectedProblem.Errors.Add("sample", new string[] { "error-message" }); + + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + Assert.Equal(expectedProblem.Errors, problemDetails.Errors); + } + + [Fact] + public async Task WriteAsync_Works_WithCustomDerivedProblemDetails() + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + + var writer = GetWriter(jsonOptions: options); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new CustomProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + ExtraProperty = "My Extra property" + }; + + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream, options.SerializerOptions); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + Assert.Equal(expectedProblem.ExtraProperty, problemDetails.ExtraProperty); + } + + [Fact] + public async Task WriteAsync_Works_WithCustomDerivedProblemDetails_AndJsonContext() + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.AddContext(); + + var writer = GetWriter(jsonOptions: options); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new CustomProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + ExtraProperty = "My Extra property" + }; + + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream, options.SerializerOptions); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + Assert.Equal(expectedProblem.ExtraProperty, problemDetails.ExtraProperty); + } + + [Fact] + public async Task WriteAsync_Works_WithCustomDerivedProblemDetails_AndMultipleJsonContext() + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(CustomProblemDetailsContext.Default, ProblemDetailsJsonContext.Default); + + var writer = GetWriter(jsonOptions: options); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new CustomProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + ExtraProperty = "My Extra property" + }; + + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream, options.SerializerOptions); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + Assert.Equal(expectedProblem.ExtraProperty, problemDetails.ExtraProperty); + } + [Fact] public async Task WriteAsync_AddExtensions() { @@ -84,6 +366,46 @@ public async Task WriteAsync_AddExtensions() }); } + [Fact] + public async Task WriteAsync_AddExtensions_WithJsonContext() + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(CustomProblemDetailsContext.Default, ProblemDetailsJsonContext.Default); + + var writer = GetWriter(jsonOptions: options); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var expectedProblem = new ProblemDetails(); + var customExtensionData = new CustomExtensionData("test"); + expectedProblem.Extensions["Extension"] = customExtensionData; + + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = expectedProblem + }; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + + var problemDetails = await JsonSerializer.DeserializeAsync(stream); + Assert.NotNull(problemDetails); + + Assert.Collection(problemDetails.Extensions, + (extension) => + { + Assert.Equal("Extension", extension.Key); + var expectedExtension = JsonSerializer.SerializeToElement(customExtensionData, options.SerializerOptions); + var value = Assert.IsType(extension.Value); + + Assert.Equal(expectedExtension.GetProperty("data").GetString(), value.GetProperty("data").GetString()); + }); + } + [Fact] public async Task WriteAsync_Applies_Defaults() { @@ -233,9 +555,27 @@ private static IServiceProvider CreateServices() return services.BuildServiceProvider(); } - private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null) + private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null, JsonOptions jsonOptions = null) { options ??= new ProblemDetailsOptions(); - return new DefaultProblemDetailsWriter(Options.Create(options)); + jsonOptions ??= new JsonOptions(); + + return new DefaultProblemDetailsWriter(Options.Create(options), Options.Create(jsonOptions)); + } + + internal class CustomProblemDetails : ProblemDetails + { + public string ExtraProperty { get; set; } } + + [JsonSerializable(typeof(CustomProblemDetails))] + [JsonSerializable(typeof(CustomExtensionData))] + internal partial class CustomProblemDetailsContext : JsonSerializerContext + { } + + [JsonSerializable(typeof(CustomProblemDetails))] + internal partial class CustomProblemDetailsContext2 : JsonSerializerContext + { } + + internal record CustomExtensionData(string Data); } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs index 0760838e00fd..bb0d16c9b039 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -1,13 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Moq; namespace Microsoft.AspNetCore.Http.Extensions.Tests; -public class ProblemDetailsServiceCollectionExtensionsTest +public partial class ProblemDetailsServiceCollectionExtensionsTest { [Fact] public void AddProblemDetails_AddsNeededServices() @@ -21,6 +25,23 @@ public void AddProblemDetails_AddsNeededServices() // Assert Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsService) && sd.ImplementationType == typeof(ProblemDetailsService)); Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsWriter) && sd.ImplementationType == typeof(DefaultProblemDetailsWriter)); + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IPostConfigureOptions) && sd.ImplementationType == typeof(ProblemDetailsJsonOptionsSetup)); + } + + [Fact] + public void AddProblemDetails_DoesNotDuplicate_WhenMultipleCalls() + { + // Arrange + var collection = new ServiceCollection(); + + // Act + collection.AddProblemDetails(); + collection.AddProblemDetails(); + + // Assert + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsService) && sd.ImplementationType == typeof(ProblemDetailsService)); + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsWriter) && sd.ImplementationType == typeof(DefaultProblemDetailsWriter)); + Assert.Single(collection, (sd) => sd.ServiceType == typeof(IPostConfigureOptions) && sd.ImplementationType == typeof(ProblemDetailsJsonOptionsSetup)); } [Fact] @@ -58,4 +79,95 @@ public void AddProblemDetails_KeepCustomRegisteredService() var service = Assert.Single(collection, (sd) => sd.ServiceType == typeof(IProblemDetailsService)); Assert.Same(customService, service.ImplementationInstance); } + + [Fact] + public void AddProblemDetails_CombinesProblemDetailsContext() + { + // Arrange + var collection = new ServiceCollection(); + collection.AddOptions(); + collection.ConfigureAll(options => options.SerializerOptions.TypeInfoResolver = new TestExtensionsJsonContext()); + + // Act + collection.AddProblemDetails(); + + // Assert + var services = collection.BuildServiceProvider(); + var jsonOptions = services.GetService>(); + + Assert.NotNull(jsonOptions.Value); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(ProblemDetails), jsonOptions.Value.SerializerOptions)); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(TypeA), jsonOptions.Value.SerializerOptions)); + } + + [Fact] + public void AddProblemDetails_CombinesProblemDetailsContext_ForReadOnlyJsonOptions() + { + // Arrange + var collection = new ServiceCollection(); + collection.AddOptions(); + collection.ConfigureAll(options => { + options.SerializerOptions.TypeInfoResolver = new TestExtensionsJsonContext(); + options.SerializerOptions.MakeReadOnly(); + }); + + // Act + collection.AddProblemDetails(); + + // Assert + var services = collection.BuildServiceProvider(); + var jsonOptions = services.GetService>(); + + Assert.NotNull(jsonOptions.Value); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(ProblemDetails), jsonOptions.Value.SerializerOptions)); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(TypeA), jsonOptions.Value.SerializerOptions)); + } + + [Fact] + public void AddProblemDetails_CombinesProblemDetailsContext_WhenAddContext() + { + // Arrange + var collection = new ServiceCollection(); + collection.AddOptions(); + collection.ConfigureAll(options => options.SerializerOptions.AddContext()); + + // Act + collection.AddProblemDetails(); + + // Assert + var services = collection.BuildServiceProvider(); + var jsonOptions = services.GetService>(); + + Assert.NotNull(jsonOptions.Value); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(ProblemDetails), jsonOptions.Value.SerializerOptions)); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(TypeA), jsonOptions.Value.SerializerOptions)); + } + + [Fact] + public void AddProblemDetails_DoesNotCombineProblemDetailsContext_WhenNullTypeInfoResolver() + { + // Arrange + var collection = new ServiceCollection(); + collection.AddOptions(); + collection.ConfigureAll(options => options.SerializerOptions.TypeInfoResolver = null); + + // Act + collection.AddProblemDetails(); + + // Assert + var services = collection.BuildServiceProvider(); + var jsonOptions = services.GetService>(); + + Assert.NotNull(jsonOptions.Value); + Assert.Null(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + } + + [JsonSerializable(typeof(TypeA))] + internal partial class TestExtensionsJsonContext : JsonSerializerContext + { } + + public class TypeA { } }