Skip to content

Commit 8a7e238

Browse files
Remove EnableApiVersionBinding extension
1 parent 7c495d3 commit 8a7e238

3 files changed

Lines changed: 69 additions & 52 deletions

File tree

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/EndpointBuilderFinalizer.cs

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ namespace Asp.Versioning.Builder;
1010
using Microsoft.Extensions.DependencyInjection;
1111
using Microsoft.Extensions.Options;
1212
using System.Globalization;
13-
using System.Runtime.CompilerServices;
1413
using static Asp.Versioning.ApiVersionParameterLocation;
1514
using static Asp.Versioning.ApiVersionProviderOptions;
1615

@@ -47,16 +46,20 @@ private static void Finialize( EndpointBuilder endpointBuilder, ApiVersionSet? v
4746

4847
endpointBuilder.Metadata.Add( metadata );
4948

50-
var requestDelegate = default( RequestDelegate );
49+
var requestDelegate =
50+
endpointBuilder.RequestDelegate
51+
?? throw new InvalidOperationException(
52+
string.Format(
53+
CultureInfo.CurrentCulture,
54+
Format.UnsetRequestDelegate,
55+
nameof( RequestDelegate ),
56+
nameof( RouteEndpoint ) ) );
5157

5258
if ( reportApiVersions )
5359
{
54-
requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate );
55-
5660
var reporter = services.GetRequiredService<IReportApiVersions>();
5761

5862
requestDelegate = new ReportApiVersionsDecorator( requestDelegate, reporter, metadata );
59-
endpointBuilder.RequestDelegate = requestDelegate;
6063
}
6164

6265
var parameterSource = services.GetRequiredService<IApiVersionParameterSource>();
@@ -67,11 +70,15 @@ private static void Finialize( EndpointBuilder endpointBuilder, ApiVersionSet? v
6770

6871
if ( !string.IsNullOrEmpty( parameterName ) )
6972
{
70-
requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate );
7173
requestDelegate = new ContentTypeApiVersionDecorator( requestDelegate, parameterName );
72-
endpointBuilder.RequestDelegate = requestDelegate;
7374
}
7475
}
76+
77+
endpointBuilder.RequestDelegate = context =>
78+
{
79+
context.RequestServices = new InjectApiVersion( context );
80+
return requestDelegate( context );
81+
};
7582
}
7683

7784
private static bool IsApiVersionNeutral( IList<object> metadata )
@@ -261,16 +268,6 @@ private static ApiVersionMetadata Build( IList<object> metadata, ApiVersionSet v
261268
return new( apiModel, endpointModel, name );
262269
}
263270

264-
[MethodImpl( MethodImplOptions.AggressiveInlining )]
265-
private static RequestDelegate EnsureRequestDelegate( RequestDelegate? current, RequestDelegate? original ) =>
266-
( current ?? original ) ??
267-
throw new InvalidOperationException(
268-
string.Format(
269-
CultureInfo.CurrentCulture,
270-
Format.UnsetRequestDelegate,
271-
nameof( RequestDelegate ),
272-
nameof( RouteEndpoint ) ) );
273-
274271
private record struct ApiVersionBuckets(
275272
IReadOnlyList<ApiVersion> Mapped,
276273
IReadOnlyList<ApiVersion> Supported,
@@ -284,4 +281,28 @@ private record struct ApiVersionBuckets(
284281
&& Advertised.Count == 0
285282
&& AdvertisedDeprecated.Count == 0;
286283
}
284+
285+
private sealed class InjectApiVersion : IServiceProvider
286+
{
287+
private static readonly Type ApiVersionType = typeof( ApiVersion );
288+
private readonly IServiceProvider provider;
289+
private readonly HttpContext context;
290+
291+
public InjectApiVersion( HttpContext context )
292+
{
293+
this.context = context;
294+
provider = context.RequestServices;
295+
context.RequestServices = this;
296+
}
297+
298+
public object? GetService( Type serviceType )
299+
{
300+
if ( serviceType.IsAssignableFrom( ApiVersionType ) )
301+
{
302+
return context.RequestedApiVersion;
303+
}
304+
305+
return provider.GetService( serviceType );
306+
}
307+
}
287308
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilder.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ public virtual IApplicationBuilder CreateApplicationBuilder() =>
5555
public virtual ICollection<EndpointDataSource> DataSources => dataSources;
5656

5757
/// <inheritdoc />
58-
public virtual void Add( Action<EndpointBuilder> convention ) =>
59-
conventionBuilder.Add( convention );
58+
public virtual void Add( Action<EndpointBuilder> convention ) => conventionBuilder.Add( convention );
6059

6160
private sealed class ServiceProviderDecorator(
6261
IServiceProvider decorated,

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

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -96,48 +96,45 @@ public IApiVersioningBuilder AddApiVersioning( Action<ApiVersioningOptions> setu
9696
}
9797
}
9898

99-
/// <param name="builder">The extended <see cref="IApiVersioningBuilder">builder</see>.</param>
100-
/// <returns>The original <paramref name="builder"/>.</returns>
101-
extension( IApiVersioningBuilder builder )
99+
private static void DefaultErrorObjectJsonConfig( JsonOptions options ) =>
100+
options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default );
101+
102+
// HACK: convince DI that ApiVersion can be resolved as a service. this enables ApiVersion to be used as a
103+
// a parameter without explicitly specifying [FromServices]. DI is not actually expected to resolve ApiVersion
104+
// because it requires the current HttpContext. an interceptor is inserted in EndpointBuilderFinalizer.Finalize.
105+
// resolving ApiVersion from DI allows it to be resolved from nearly any context, which is not intended. by the time
106+
// an endpoint action is invoked, the ApiVersion will be available in the current HttpContext unless the API is
107+
// version-neutral. in those situations, the parameter can be declared as ApiVersion? instead. this function makes
108+
// a best effort to be honor DI by resolving the ApiVersion through IHttpContextAccessor if it's available.
109+
//
110+
// ultimately, this is required because there is no other hook. if/when a better parameter binding mechanism becomes
111+
// available, this is expected to go away.
112+
//
113+
// 1. TryParse does not work because:
114+
// a. Parsing is delegated to IApiVersionParser.TryParse
115+
// b. The result can come from multiple locations
116+
// c. There can be multiple results
117+
// 2. BindAsync does not work because:
118+
// a. It is static and must be on the ApiVersion type
119+
// b. It requires HttpContext, which is specific to ASP.NET Core
120+
//
121+
// REF: https://github.com/dotnet/aspnetcore/issues/35489
122+
// REF: https://github.com/dotnet/aspnetcore/issues/50672
123+
private static ApiVersion ApiVersionAsService( IServiceProvider provider )
102124
{
103-
/// <summary>
104-
/// Enables binding the <see cref="ApiVersion"/> type in Minimal API parameters..
105-
/// </summary>
106-
public IApiVersioningBuilder EnableApiVersionBinding()
125+
if ( provider.GetService<IHttpContextAccessor>() is { } accessor && accessor.HttpContext is { } context )
107126
{
108-
ArgumentNullException.ThrowIfNull( builder );
109-
110-
// currently required because there is no other hook.
111-
// 1. TryParse does not work because:
112-
// a. Parsing is delegated to IApiVersionParser.TryParse
113-
// b. The result can come from multiple locations
114-
// c. There can be multiple results
115-
// 2. BindAsync does not work because:
116-
// a. It is static and must be on the ApiVersion type
117-
// b. It is specific to ASP.NET Core
118-
builder.Services.AddHttpContextAccessor();
119-
120-
// this registration is 'truthy'. it is possible for the requested API version to be null; however, but the time this is
121-
// resolved for a request delegate it can only be null if the API is version-neutral and no API version was requested. this
122-
// should be a rare and nonsensical scenario. declaring the parameter as ApiVersion? should be expect and solve the issue
123-
//
124-
// it should also be noted that this registration allows resolving the requested API version from virtually any context.
125-
// that is not intended, which is why this extension is not named something more general such as AddApiVersionAsService.
126-
// if/when a better parameter binding mechanism becomes available, this method is expected to become obsolete, no-op, and
127-
// eventually go away.
128-
builder.Services.AddTransient( sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.RequestedApiVersion! );
129-
130-
return builder;
127+
return context.RequestedApiVersion!;
131128
}
132-
}
133129

134-
private static void DefaultErrorObjectJsonConfig( JsonOptions options ) =>
135-
options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default );
130+
return default!;
131+
}
136132

137133
private static void AddApiVersioningServices( IServiceCollection services )
138134
{
139135
ArgumentNullException.ThrowIfNull( services );
140136

137+
services.AddTransient( ApiVersionAsService );
141138
services.TryAddSingleton<IApiVersionParser, ApiVersionParser>();
142139
services.AddSingleton( static sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader );
143140
services.AddSingleton( static sp => (IApiVersionParameterSource) sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader );

0 commit comments

Comments
 (0)