Skip to content

Commit bde8d62

Browse files
committed
Update OpenApi implementation to avoid reflection
1 parent c97a8bd commit bde8d62

9 files changed

Lines changed: 76 additions & 284 deletions

File tree

examples/AspNetCore/WebApi/MinimalOpenApiExample/MinimalOpenApiExample.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<TargetFramework>net10.0</TargetFramework>
55
<AssemblyTitle>Example API</AssemblyTitle>
66
<GenerateDocumentationFile>true</GenerateDocumentationFile>
7+
<OpenApiGenerateDocuments>false</OpenApiGenerateDocuments>
8+
<OpenApiGenerateDocumentsOnBuild>false</OpenApiGenerateDocumentsOnBuild>
79
</PropertyGroup>
810

911
<ItemGroup>

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Builder/IEndpointConventionBuilderExtensions.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ namespace Microsoft.AspNetCore.Builder;
66

77
using Asp.Versioning;
88
using Asp.Versioning.ApiExplorer;
9-
using Asp.Versioning.OpenApi.Reflection;
109
using Microsoft.AspNetCore.Builder;
1110
using Microsoft.AspNetCore.Http;
1211
using Microsoft.Extensions.DependencyInjection;
@@ -44,12 +43,12 @@ private static void ApplyApiVersioning( EndpointBuilder builder )
4443

4544
private static Task InterceptRequestServices( HttpContext context, RequestDelegate action )
4645
{
47-
if ( context.RequestServices is not KeyedServiceContainer requestServices )
46+
if ( context.RequestServices is not AggregateKeyedServiceProvider serviceProvider )
4847
{
49-
requestServices = context.RequestServices.GetRequiredService<KeyedServiceContainer>();
48+
serviceProvider = context.RequestServices.GetRequiredService<AggregateKeyedServiceProvider>();
5049
}
5150

52-
context.RequestServices = requestServices;
51+
context.RequestServices = serviceProvider;
5352
return action( context );
5453
}
5554
}

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Configuration/ConfigureOpenApiOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
namespace Asp.Versioning.OpenApi.Configuration;
66

77
using Asp.Versioning.ApiExplorer;
8-
using Asp.Versioning.OpenApi.Reflection;
98
using Asp.Versioning.OpenApi.Transformers;
109
using Microsoft.AspNetCore.OpenApi;
1110
using Microsoft.Extensions.Options;
11+
using System.Net;
12+
using System.Runtime.CompilerServices;
1213

1314
internal sealed class ConfigureOpenApiOptions(
1415
XmlCommentsTransformer xmlComments,
@@ -48,7 +49,6 @@ private static void Configure( VersionedOpenApiOptions versionedOptions, XmlComm
4849
var options = versionedOptions.Document;
4950
var apiExplorer = new ApiExplorerTransformer( versionedOptions );
5051

51-
options.SetDocumentName( versionedOptions.Description.GroupName );
5252
options.AddDocumentTransformer( apiExplorer );
5353
options.AddSchemaTransformer( apiExplorer );
5454
options.AddOperationTransformer( apiExplorer );
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
#pragma warning disable IDE0130
4+
5+
namespace Microsoft.Extensions.DependencyInjection;
6+
7+
internal sealed class AggregateKeyedServiceProvider( IServiceProvider parent ) : IKeyedServiceProvider
8+
{
9+
private readonly IServiceProvider parent = parent;
10+
private readonly List<IServiceProvider> providers = [];
11+
12+
public object? GetKeyedService( Type serviceType, object? serviceKey )
13+
{
14+
if ( providers.Count == 0 )
15+
{
16+
return parent.GetKeyedService( serviceType, serviceKey );
17+
}
18+
19+
foreach ( var provider in providers )
20+
{
21+
if ( provider.GetKeyedService( serviceType, serviceKey ) is { } service )
22+
{
23+
return service;
24+
}
25+
}
26+
27+
return null;
28+
}
29+
30+
public object GetRequiredKeyedService( Type serviceType, object? serviceKey )
31+
{
32+
if ( providers.Count == 0 )
33+
{
34+
return parent.GetRequiredKeyedService( serviceType, serviceKey );
35+
}
36+
37+
for ( int i = 0; i < providers.Count - 1; i++ )
38+
{
39+
if ( providers[i].GetKeyedService( serviceType, serviceKey ) is { } service )
40+
{
41+
return service;
42+
}
43+
}
44+
45+
return providers[providers.Count - 1].GetRequiredKeyedService( serviceType, serviceKey );
46+
}
47+
48+
public object? GetService( Type serviceType )
49+
=> parent.GetService( serviceType );
50+
51+
public void Add( IServiceCollection serviceCollection, IServiceCollection parentServiceCollection )
52+
{
53+
foreach ( var descriptor in parentServiceCollection )
54+
{
55+
serviceCollection.Add( descriptor );
56+
}
57+
58+
providers.Add( serviceCollection.BuildServiceProvider() );
59+
}
60+
}

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/DependencyInjection/IApiVersioningBuilderExtensions.cs

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ namespace Microsoft.Extensions.DependencyInjection;
88
using Asp.Versioning.ApiExplorer;
99
using Asp.Versioning.OpenApi;
1010
using Asp.Versioning.OpenApi.Configuration;
11-
using Asp.Versioning.OpenApi.Reflection;
1211
using Asp.Versioning.OpenApi.Transformers;
1312
using Microsoft.AspNetCore.Http.Json;
1413
using Microsoft.AspNetCore.OpenApi;
1514
using Microsoft.Extensions.DependencyInjection.Extensions;
1615
using Microsoft.Extensions.Hosting;
1716
using Microsoft.Extensions.Options;
17+
using System.ComponentModel.Design;
18+
using System.Diagnostics;
1819
using System.Reflection;
1920
using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
2021
using EM = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions;
@@ -64,18 +65,13 @@ private static void AddOpenApiServices( IApiVersioningBuilder builder, Assembly[
6465

6566
var services = builder.Services;
6667

67-
services.AddTransient( NewRequestServices );
68-
services.Add( Singleton( Type.IDocumentProvider, ResolveDocumentProvider ) );
68+
services.AddTransient( serviceProvider => NewRequestServices( serviceProvider, services ) );
69+
6970
services.AddSingleton<VersionedOpenApiOptionsFactory>();
7071
services.TryAddEnumerable( Transient<IPostConfigureOptions<OpenApiOptions>, ConfigureOpenApiOptions>() );
7172
services.TryAdd( Singleton<IOptionsFactory<VersionedOpenApiOptions>>( EM.GetRequiredService<VersionedOpenApiOptionsFactory> ) );
7273
services.AddTransient( sp => new XmlCommentsFile( assemblies, sp.GetRequiredService<IHostEnvironment>() ) );
7374
services.TryAddTransient( sp => new XmlCommentsTransformer( sp.GetRequiredService<XmlCommentsFile>() ) );
74-
75-
if ( GetJsonConfiguration() is { } descriptor )
76-
{
77-
services.TryAddEnumerable( descriptor );
78-
}
7975
}
8076

8177
// NOTE: The calling assembly must be captured at the call site that invokes AddOpenApi. In 99% of the cases that
@@ -94,53 +90,19 @@ private static Assembly[] GetAssemblies( Assembly callingAssembly )
9490
return [.. assemblies];
9591
}
9692

97-
// HACK: the json configuration is internal; this approach negates the use of reflection
98-
// REF: https://github.com/dotnet/aspnetcore/blob/08a9fc2c3864d99759ab3d71cfda868d852bfc4b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs#L121
99-
private static ServiceDescriptor? GetJsonConfiguration()
100-
{
101-
var services = new ServiceCollection();
102-
services.AddOpenApi( "*" );
103-
return services.SingleOrDefault( sd => sd.ServiceType == typeof( IConfigureOptions<JsonOptions> ) );
104-
}
105-
106-
private static object ResolveDocumentProvider( IServiceProvider provider ) =>
107-
provider.GetRequiredService<KeyedServiceContainer>().GetRequiredService( Type.IDocumentProvider );
108-
10993
[UnconditionalSuppressMessage( "ILLink", "IL3050" )]
110-
private static KeyedServiceContainer NewRequestServices( IServiceProvider services )
94+
private static AggregateKeyedServiceProvider NewRequestServices( IServiceProvider services, IServiceCollection parentServiceCollection )
11195
{
11296
var provider = services.GetRequiredService<IApiVersionDescriptionProvider>();
113-
var container = new KeyedServiceContainer( services );
114-
var type = typeof( IOpenApiDocumentProvider );
97+
var container = new AggregateKeyedServiceProvider( services );
11598
var descriptions = provider.ApiVersionDescriptions;
116-
var names = new List<string>( descriptions.Count );
11799

118100
for ( var i = 0; i < descriptions.Count; i++ )
119101
{
120102
var description = descriptions[i];
121-
122-
// REF: https://github.com/dotnet/aspnetcore/blob/319e87fd950a99f3baae2aa79db3d4fb68783d85/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs#L64
123-
#pragma warning disable CA1308 // Normalize strings to uppercase
124-
var key = description.GroupName.ToLowerInvariant();
125-
#pragma warning restore CA1308
126-
127-
names.Add( key );
128-
container.AddService( Type.OpenApiSchemaService, key, Class.OpenApiSchemaService.New );
129-
container.AddService( Type.OpenApiDocumentService, key, Class.OpenApiDocumentService.New );
130-
container.AddService( type, key, ( sp, k ) => sp.GetRequiredKeyedService( Type.OpenApiDocumentService, k ) );
131-
}
132-
133-
if ( names.Count > 0 )
134-
{
135-
var array = Array.CreateInstance( Type.NamedService, names.Count );
136-
137-
for ( var i = 0; i < names.Count; i++ )
138-
{
139-
array.SetValue( Class.NamedService.New( names[i] ), i );
140-
}
141-
142-
container.AddService( Type.IDocumentProvider, Class.OpenApiDocumentProvider.New );
143-
container.AddService( Type.IEnumerableOfNamedService, array );
103+
var serviceCollection = new ServiceCollection();
104+
serviceCollection.AddOpenApi( description.GroupName );
105+
container.Add( serviceCollection, parentServiceCollection );
144106
}
145107

146108
return container;

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/DependencyInjection/KeyedServiceContainer.cs

Lines changed: 0 additions & 63 deletions
This file was deleted.

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Reflection/Class.cs

Lines changed: 0 additions & 111 deletions
This file was deleted.

0 commit comments

Comments
 (0)