diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs index 14606936..367c908c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -11,7 +11,9 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using System; +using System.Diagnostics.CodeAnalysis; using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; /// /// Provides extension methods for the interface. @@ -75,6 +77,65 @@ public static IApiVersioningBuilder EnableApiVersionBinding( this IApiVersioning return builder; } + /// + /// Adds error object support in problem details. + /// + /// The services available in the application. + /// The JSON options setup to perform, if any. + /// The original . + /// + /// + /// This method is only intended to provide backward compatibility with previous library versions by converting + /// into Error Objects that conform to the + /// Error Responses + /// in the Microsoft REST API Guidelines and + /// OData Error Responses. + /// + /// + /// This method should be called before . + /// + /// + public static IServiceCollection AddErrorObjects( this IServiceCollection services, Action? setup = default ) => + AddErrorObjects( services, setup ); + + /// + /// Adds error object support in problem details. + /// + /// The type of . + /// The services available in the application. + /// The JSON options setup to perform, if any. + /// The original . + /// + /// + /// This method is only intended to provide backward compatibility with previous library versions by converting + /// into Error Objects that conform to the + /// Error Responses + /// in the Microsoft REST API Guidelines and + /// OData Error Responses. + /// + /// + /// This method should be called before . + /// + /// + public static IServiceCollection AddErrorObjects<[DynamicallyAccessedMembers( PublicConstructors )] TWriter>( + this IServiceCollection services, + Action? setup = default ) + where TWriter : ErrorObjectWriter + { + ArgumentNullException.ThrowIfNull( services ); + + services.TryAddEnumerable( Singleton() ); + services.Configure( setup ?? DefaultErrorObjectJsonConfig ); + + // TODO: remove with TryAddErrorObjectJsonOptions in 9.0+ + services.AddTransient(); + + return services; + } + + private static void DefaultErrorObjectJsonConfig( JsonOptions options ) => + options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default ); + private static void AddApiVersioningServices( IServiceCollection services ) { ArgumentNullException.ThrowIfNull( services ); @@ -180,23 +241,46 @@ static Rfc7231ProblemDetailsWriter NewProblemDetailsWriter( IServiceProvider ser new( (IProblemDetailsWriter) serviceProvider.GetRequiredService( decoratedType ) ); } + // TODO: retain for 8.1.x back-compat, but remove in 9.0+ in favor of AddErrorObjects for perf private static void TryAddErrorObjectJsonOptions( IServiceCollection services ) { var serviceType = typeof( IProblemDetailsWriter ); var implementationType = typeof( ErrorObjectWriter ); + var markerType = typeof( ErrorObjectsAdded ); + var hasErrorObjects = false; + var hasErrorObjectsJsonConfig = false; for ( var i = 0; i < services.Count; i++ ) { var service = services[i]; - // inheritance is intentionally not considered here because it will require a user-defined - // JsonSerlizerContext and IConfigureOptions - if ( service.ServiceType == serviceType && - service.ImplementationType == implementationType ) + if ( !hasErrorObjects && + service.ServiceType == serviceType && + implementationType.IsAssignableFrom( service.ImplementationType ) ) { - services.TryAddEnumerable( Singleton, ErrorObjectJsonOptionsSetup>() ); - return; + hasErrorObjects = true; + + if ( hasErrorObjectsJsonConfig ) + { + break; + } } + else if ( service.ServiceType == markerType ) + { + hasErrorObjectsJsonConfig = true; + + if ( hasErrorObjects ) + { + break; + } + } + } + + if ( hasErrorObjects && !hasErrorObjectsJsonConfig ) + { + services.Configure( DefaultErrorObjectJsonConfig ); } } + + private sealed class ErrorObjectsAdded { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs deleted file mode 100644 index 664c4840..00000000 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -#pragma warning disable CA1812 // Avoid uninstantiated internal classes - -namespace Asp.Versioning; - -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; - -/// -/// Adds the ErrorObjectJsonContext to the current JsonSerializerOptions. -/// -/// This allows for consistent serialization behavior for ErrorObject regardless if the -/// default reflection-based serializer is used and makes it trim/NativeAOT compatible. -/// -internal sealed class ErrorObjectJsonOptionsSetup : IConfigureOptions -{ - // Always insert the ErrorObjectJsonContext to the beginning of the chain at the time this Configure - // is invoked. This JsonTypeInfoResolver will be before the default reflection-based resolver, and - // before any other resolvers currently added. If apps need to customize serialization, they can - // prepend a custom ErrorObject resolver to the chain in an IConfigureOptions registered. - public void Configure( JsonOptions options ) => - options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default ); -} \ No newline at end of file