Skip to content

Commit a8d358d

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

10 files changed

Lines changed: 258 additions & 286 deletions

File tree

examples/AspNetCore/WebApi/MinimalOpenApiExample/MinimalOpenApiExample.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<TargetFramework>net10.0</TargetFramework>
55
<AssemblyTitle>Example API</AssemblyTitle>
66
<GenerateDocumentationFile>true</GenerateDocumentationFile>
7+
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated</InterceptorsNamespaces>
8+
<OpenApiGenerateDocuments>false</OpenApiGenerateDocuments>
9+
<OpenApiGenerateDocumentsOnBuild>false</OpenApiGenerateDocumentsOnBuild>
710
</PropertyGroup>
811

912
<ItemGroup>

src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/ApiVersioningBuilder.cs

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,184 @@
55
namespace Microsoft.Extensions.DependencyInjection;
66

77
using Asp.Versioning;
8+
using System.Collections;
9+
using System.Diagnostics;
810

911
internal sealed class ApiVersioningBuilder : IApiVersioningBuilder
1012
{
11-
public ApiVersioningBuilder( IServiceCollection services ) => Services = services;
13+
public ApiVersioningBuilder( IServiceCollection services ) => Services = new MutableServiceCollection(services);
1214

1315
public IServiceCollection Services { get; }
16+
17+
private sealed class MutableServiceCollection : IServiceCollection
18+
{
19+
private readonly List<ServiceDescriptor> descriptors = new List<ServiceDescriptor>();
20+
private readonly IServiceCollection inner;
21+
22+
public MutableServiceCollection(IServiceCollection inner)
23+
=> this.inner = inner;
24+
25+
private bool IndexIsInOurList( int index, out int mappedIndex )
26+
{
27+
mappedIndex = index - inner.Count;
28+
return mappedIndex >= 0 && mappedIndex < descriptors.Count;
29+
}
30+
31+
public ServiceDescriptor this[int index]
32+
{
33+
get => IndexIsInOurList( index, out int mappedIndex ) ? descriptors[mappedIndex] : inner[index];
34+
set
35+
{
36+
if ( IndexIsInOurList( index, out int mappedIndex ) )
37+
{
38+
descriptors[mappedIndex] = value;
39+
}
40+
else
41+
{
42+
inner[index] = value;
43+
}
44+
}
45+
}
46+
47+
public int Count => inner.Count + descriptors.Count;
48+
49+
// while current instance isn't in theory read only.
50+
// fake it to limit exposure.
51+
// Mutating the services after build is done is extremely bad.
52+
// It's mutated only by OpenAPI implementation so far which won't check for IsReadOnly.
53+
public bool IsReadOnly => inner.IsReadOnly;
54+
55+
public void Add( ServiceDescriptor item )
56+
{
57+
if ( !inner.IsReadOnly )
58+
{
59+
inner.Add( item );
60+
}
61+
else
62+
{
63+
descriptors.Add( item );
64+
}
65+
}
66+
67+
public void Clear()
68+
{
69+
inner.Clear();
70+
descriptors.Clear();
71+
}
72+
73+
public bool Contains( ServiceDescriptor item )
74+
=> inner.Contains( item ) || descriptors.Contains( item );
75+
76+
public void CopyTo( ServiceDescriptor[] array, int arrayIndex )
77+
{
78+
throw new NotImplementedException();
79+
}
80+
81+
public IEnumerator<ServiceDescriptor> GetEnumerator()
82+
=> new CombinedEnumerator( inner, descriptors );
83+
84+
public int IndexOf( ServiceDescriptor item )
85+
{
86+
var indexInInner = inner.IndexOf( item );
87+
if ( indexInInner >= 0 )
88+
{
89+
return indexInInner;
90+
}
91+
92+
var indexInCurrent = descriptors.IndexOf( item );
93+
if ( indexInCurrent >= 0 )
94+
{
95+
return indexInCurrent + inner.Count;
96+
}
97+
98+
return -1;
99+
}
100+
101+
public void Insert( int index, ServiceDescriptor item )
102+
{
103+
throw new NotImplementedException();
104+
}
105+
106+
public bool Remove( ServiceDescriptor item )
107+
=> inner.Remove( item ) || descriptors.Remove( item );
108+
109+
public void RemoveAt( int index )
110+
{
111+
if ( IndexIsInOurList( index, out var mappedIndex ) )
112+
{
113+
descriptors.RemoveAt( mappedIndex );
114+
}
115+
else
116+
{
117+
inner.RemoveAt( index );
118+
}
119+
}
120+
121+
IEnumerator IEnumerable.GetEnumerator()
122+
{
123+
return GetEnumerator();
124+
}
125+
}
126+
127+
private sealed class CombinedEnumerator : IEnumerator<ServiceDescriptor>
128+
{
129+
private readonly IServiceCollection inner;
130+
private readonly IList<ServiceDescriptor> descriptors;
131+
132+
private IEnumerator<ServiceDescriptor>? enumerator1;
133+
private IEnumerator<ServiceDescriptor>? enumerator2;
134+
135+
public CombinedEnumerator(IServiceCollection inner, IList<ServiceDescriptor> descriptors)
136+
{
137+
this.inner = inner;
138+
this.descriptors = descriptors;
139+
}
140+
141+
public ServiceDescriptor Current => enumerator2 is not null ? enumerator2.Current : enumerator1!.Current;
142+
143+
object IEnumerator.Current => Current;
144+
145+
public void Dispose()
146+
{
147+
enumerator1?.Dispose();
148+
enumerator2?.Dispose();
149+
}
150+
151+
public bool MoveNext()
152+
{
153+
if ( enumerator1 is null && enumerator2 is null )
154+
{
155+
enumerator1 = inner.GetEnumerator();
156+
}
157+
158+
if ( enumerator2 is not null )
159+
{
160+
return enumerator2.MoveNext();
161+
}
162+
else if ( enumerator1 is not null )
163+
{
164+
if ( enumerator1.MoveNext() )
165+
{
166+
return true;
167+
}
168+
else
169+
{
170+
enumerator2 = descriptors.GetEnumerator();
171+
return enumerator2.MoveNext();
172+
}
173+
}
174+
else
175+
{
176+
throw new UnreachableException();
177+
}
178+
}
179+
180+
public void Reset()
181+
{
182+
enumerator1?.Dispose();
183+
enumerator2?.Dispose();
184+
enumerator1 = null;
185+
enumerator2 = null;
186+
}
187+
}
14188
}

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: 6 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,7 @@ 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 );
52+
SetDocumentName( options, versionedOptions.Description.GroupName );
5253
options.AddDocumentTransformer( apiExplorer );
5354
options.AddSchemaTransformer( apiExplorer );
5455
options.AddOperationTransformer( apiExplorer );
@@ -59,4 +60,7 @@ private static void Configure( VersionedOpenApiOptions versionedOptions, XmlComm
5960
options.AddOperationTransformer( xmlComments );
6061
}
6162
}
63+
64+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_DocumentName")]
65+
private static extern void SetDocumentName( OpenApiOptions handler, string value );
6266
}
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+
}

0 commit comments

Comments
 (0)