Skip to content

Commit a65e096

Browse files
Support async API version selection. Resolves #1009
1 parent 770cf6a commit a65e096

File tree

6 files changed

+172
-38
lines changed

6 files changed

+172
-38
lines changed

src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/ODataBatchPathMapping.cs

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,56 @@ public bool TryGetHandler( HttpContext context, [NotNullWhen( true )] out ODataB
4040
return false;
4141
}
4242

43+
var routeData = new RouteValueDictionary();
44+
var candidates = new Dictionary<ApiVersion, int>( capacity: mappings.Length );
45+
46+
batchHandler = SelectExactMatch( context, routeData, candidates ) ??
47+
SelectBestCandidate( context, candidates, routeData );
48+
49+
return batchHandler is not null;
50+
}
51+
52+
public ValueTask<ODataBatchHandler?> TryGetHandlerAsync( HttpContext context, CancellationToken cancellationToken )
53+
{
54+
if ( count == 0 )
55+
{
56+
return ValueTask.FromResult( default( ODataBatchHandler ) );
57+
}
58+
59+
var routeData = new RouteValueDictionary();
60+
var candidates = new Dictionary<ApiVersion, int>( capacity: mappings.Length );
61+
62+
if ( SelectExactMatch( context, routeData, candidates ) is { } handler )
63+
{
64+
return ValueTask.FromResult<ODataBatchHandler?>( handler );
65+
}
66+
67+
return SelectBestCandidateAsync( context, candidates, routeData, cancellationToken );
68+
}
69+
70+
private static void MergeRouteData( HttpContext context, RouteValueDictionary routeData )
71+
{
72+
if ( routeData.Count == 0 )
73+
{
74+
return;
75+
}
76+
77+
var batchRouteData = context.ODataFeature().BatchRouteData;
78+
79+
foreach ( var (key, value) in routeData )
80+
{
81+
batchRouteData.Add( key, value );
82+
}
83+
}
84+
85+
private ODataBatchHandler? SelectExactMatch(
86+
HttpContext context,
87+
RouteValueDictionary routeData,
88+
Dictionary<ApiVersion, int> candidates )
89+
{
4390
var path = context.Request.Path;
4491
var feature = context.ApiVersioningFeature();
4592
var unspecified = feature.RawRequestedApiVersions.Count == 0;
46-
var routeData = new RouteValueDictionary();
47-
var candidates = new Dictionary<ApiVersion, int>( capacity: mappings.Length );
4893

4994
for ( var i = 0; i < count; i++ )
5095
{
@@ -73,32 +118,39 @@ public bool TryGetHandler( HttpContext context, [NotNullWhen( true )] out ODataB
73118
}
74119

75120
MergeRouteData( context, routeData );
76-
batchHandler = handler;
77-
return true;
121+
return handler;
78122
}
79123

80-
batchHandler = SelectBestCandidate( context, ref path, candidates, routeData );
81-
return batchHandler is not null;
124+
return default;
82125
}
83126

84-
private static void MergeRouteData( HttpContext context, RouteValueDictionary routeData )
127+
private ODataBatchHandler? SelectBestCandidate(
128+
HttpContext context,
129+
Dictionary<ApiVersion, int> candidates,
130+
RouteValueDictionary routeData,
131+
ApiVersion version )
85132
{
86-
if ( routeData.Count == 0 )
133+
if ( version is null || !candidates.TryGetValue( version, out var index ) )
87134
{
88-
return;
135+
return default;
89136
}
90137

91-
var batchRouteData = context.ODataFeature().BatchRouteData;
138+
ref readonly var mapping = ref mappings[index];
139+
var (matcher, handler, _) = mapping;
92140

93-
foreach ( var (key, value) in routeData )
94-
{
95-
batchRouteData.Add( key, value );
96-
}
141+
routeData.Clear();
142+
matcher.TryMatch( context.Request.Path, routeData );
143+
MergeRouteData( context, routeData );
144+
145+
// it's important that the resolved api version be set here to ensure the correct
146+
// ODataOptions are resolved by ODataBatchHandler when executed
147+
context.ApiVersioningFeature().RequestedApiVersion = version;
148+
149+
return handler;
97150
}
98151

99152
private ODataBatchHandler? SelectBestCandidate(
100153
HttpContext context,
101-
ref PathString path,
102154
Dictionary<ApiVersion, int> candidates,
103155
RouteValueDictionary routeData )
104156
{
@@ -114,22 +166,27 @@ private static void MergeRouteData( HttpContext context, RouteValueDictionary ro
114166
var model = new ApiVersionModel( candidates.Keys, Enumerable.Empty<ApiVersion>() );
115167
var version = selector.SelectVersion( context.Request, model );
116168

117-
if ( version is null || !candidates.TryGetValue( version, out var index ) )
169+
return SelectBestCandidate( context, candidates, routeData, version );
170+
}
171+
172+
private async ValueTask<ODataBatchHandler?> SelectBestCandidateAsync(
173+
HttpContext context,
174+
Dictionary<ApiVersion, int> candidates,
175+
RouteValueDictionary routeData,
176+
CancellationToken cancellationToken )
177+
{
178+
if ( candidates.Count == 0 )
118179
{
119180
return default;
120181
}
121182

122-
ref readonly var mapping = ref mappings[index];
123-
var (matcher, handler, _) = mapping;
124-
125-
routeData.Clear();
126-
matcher.TryMatch( path, routeData );
127-
MergeRouteData( context, routeData );
128-
129-
// it's important that the resolved api version be set here to ensure the correct
130-
// ODataOptions are resolved by ODataBatchHandler when executed
131-
context.ApiVersioningFeature().RequestedApiVersion = version;
183+
// ~/$batch is always version-neutral so there is no need to check
184+
// ApiVersioningOptions.AllowDefaultVersionWhenUnspecified. use the
185+
// configured IApiVersionSelector to provide a chance to select the
186+
// most appropriate version.
187+
var model = new ApiVersionModel( candidates.Keys, Enumerable.Empty<ApiVersion>() );
188+
var version = await selector.SelectVersionAsync( context.Request, model, cancellationToken ).ConfigureAwait( false );
132189

133-
return handler;
190+
return SelectBestCandidate( context, candidates, routeData, version );
134191
}
135192
}

src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataOptions.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public IReadOnlyDictionary<ApiVersion, ODataOptions> Mapping
8585
/// <param name="context">The current <see cref="HttpContext">HTTP context</see>.</param>
8686
/// <param name="handler">The retrieved <see cref="ODataBatchHandler">OData batch handler</see> or <c>null</c>.</param>
8787
/// <returns>True if the <paramref name="handler"/> was successfully retrieved; otherwise, false.</returns>
88+
/// <remarks>Prefer the asynchronous version of this method
89+
/// <see cref="TryGetBatchHandlerAsync(HttpContext, CancellationToken)"/>.</remarks>
8890
public virtual bool TryGetBatchHandler( HttpContext context, [NotNullWhen( true )] out ODataBatchHandler? handler )
8991
{
9092
ArgumentNullException.ThrowIfNull( context );
@@ -98,12 +100,33 @@ public virtual bool TryGetBatchHandler( HttpContext context, [NotNullWhen( true
98100
return batchMapping.TryGetHandler( context, out handler );
99101
}
100102

103+
/// <summary>
104+
/// Attempts to retrieve the configured batch handler for the current context.
105+
/// </summary>
106+
/// <param name="context">The current <see cref="HttpContext">HTTP context</see>.</param>
107+
/// <param name="cancellationToken">The token that can be used to cancel the operation.</param>
108+
/// <returns>A <see cref="ValueTask{TResult}">task</see> containing the matched <see cref="ODataBatchHandler"/>
109+
/// or <c>null</c> if the no match was found.</returns>
110+
public virtual ValueTask<ODataBatchHandler?> TryGetBatchHandlerAsync( HttpContext context, CancellationToken cancellationToken )
111+
{
112+
ArgumentNullException.ThrowIfNull( context );
113+
114+
if ( batchMapping is null )
115+
{
116+
return ValueTask.FromResult( default( ODataBatchHandler? ) );
117+
}
118+
119+
return batchMapping.TryGetHandlerAsync( context, cancellationToken );
120+
}
121+
101122
/// <summary>
102123
/// Attempts to get the current OData options.
103124
/// </summary>
104125
/// <param name="context">The current <see cref="HttpContext">HTTP context</see>.</param>
105126
/// <param name="options">The resolved <see cref="ODataOptions">OData options</see> or <c>null</c>.</param>
106127
/// <returns>True if the current OData were successfully resolved; otherwise, false.</returns>
128+
/// <remarks>Prefer the asynchronous version of this method
129+
/// <see cref="TryGetValueAsync(HttpContext?, CancellationToken)"/>.</remarks>
107130
public virtual bool TryGetValue( HttpContext? context, [NotNullWhen( true )] out ODataOptions? options )
108131
{
109132
if ( context == null || mapping == null || mapping.Count == 0 )
@@ -129,6 +152,36 @@ public virtual bool TryGetValue( HttpContext? context, [NotNullWhen( true )] out
129152
return mapping.TryGetValue( apiVersion, out options );
130153
}
131154

155+
/// <summary>
156+
/// Attempts to get the current OData options.
157+
/// </summary>
158+
/// <param name="context">The current <see cref="HttpContext">HTTP context</see>.</param>
159+
/// <param name="cancellationToken">The token that can be used to cancel the operation.</param>
160+
/// <returns>A <see cref="ValueTask{TResult}">task</see> containing the matched <see cref="ODataOptions"/>
161+
/// or <c>null</c> if the no match was found.</returns>
162+
public virtual async ValueTask<ODataOptions?> TryGetValueAsync( HttpContext? context, CancellationToken cancellationToken )
163+
{
164+
if ( context == null || mapping == null || mapping.Count == 0 )
165+
{
166+
return default;
167+
}
168+
169+
var apiVersion = context.GetRequestedApiVersion();
170+
171+
if ( apiVersion == null )
172+
{
173+
var model = new ApiVersionModel( mapping.Keys, Array.Empty<ApiVersion>() );
174+
apiVersion = await ApiVersionSelector.SelectVersionAsync( context.Request, model, cancellationToken ).ConfigureAwait( false );
175+
176+
if ( apiVersion == null )
177+
{
178+
return default;
179+
}
180+
}
181+
182+
return mapping.TryGetValue( apiVersion, out var options ) ? options : default;
183+
}
184+
132185
/// <summary>
133186
/// Attempts to resolve the current OData options.
134187
/// </summary>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning;
4+
5+
using Microsoft.AspNetCore.Http;
6+
7+
/// <content>
8+
/// content>
9+
/// Provides additional implementation specific to ASP.NET Core.
10+
/// </content>
11+
[CLSCompliant( false )]
12+
public partial interface IApiVersionSelector
13+
{
14+
/// <summary>
15+
/// Selects an API version given the specified HTTP request and API version information.
16+
/// </summary>
17+
/// <param name="request">The current <see cref="HttpRequest">HTTP request</see> to select the version for.</param>
18+
/// <param name="model">The <see cref="ApiVersionModel">model</see> to select the version from.</param>
19+
/// <param name="cancellationToken">The token that can be used to cancel the operation.</param>
20+
/// <returns>A <see cref="ValueTask{TResult}">task</see> containing the selected <see cref="ApiVersion">API version</see>.</returns>
21+
ValueTask<ApiVersion> SelectVersionAsync(
22+
HttpRequest request,
23+
ApiVersionModel model,
24+
CancellationToken cancellationToken ) =>
25+
ValueTask.FromResult( SelectVersion( request, model ) );
26+
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public bool AppliesToEndpoints( IReadOnlyList<Endpoint> endpoints )
7777
}
7878

7979
/// <inheritdoc />
80-
public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
80+
public async Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
8181
{
8282
ArgumentNullException.ThrowIfNull( httpContext );
8383
ArgumentNullException.ThrowIfNull( candidates );
@@ -87,7 +87,7 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
8787

8888
if ( apiVersion == null && Options.AssumeDefaultVersionWhenUnspecified )
8989
{
90-
apiVersion = TrySelectApiVersion( httpContext, candidates );
90+
apiVersion = await TrySelectApiVersionAsync( httpContext, candidates ).ConfigureAwait( false );
9191
feature.RequestedApiVersion = apiVersion;
9292
}
9393

@@ -98,8 +98,6 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
9898
var builder = new ClientErrorEndpointBuilder( feature, candidates, Options, logger );
9999
httpContext.SetEndpoint( builder.Build() );
100100
}
101-
102-
return Task.CompletedTask;
103101
}
104102

105103
/// <inheritdoc />
@@ -453,7 +451,7 @@ private static (bool Matched, bool HasCandidates) MatchApiVersion( CandidateSet
453451
return (matched, hasCandidates);
454452
}
455453

456-
private ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet candidates )
454+
private ValueTask<ApiVersion> TrySelectApiVersionAsync( HttpContext httpContext, CandidateSet candidates )
457455
{
458456
var models = new List<ApiVersionModel>( capacity: candidates.Count );
459457

@@ -473,7 +471,10 @@ private ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet ca
473471
}
474472
}
475473

476-
return ApiVersionSelector.SelectVersion( httpContext.Request, models.Aggregate() );
474+
return ApiVersionSelector.SelectVersionAsync(
475+
httpContext.Request,
476+
models.Aggregate(),
477+
httpContext.RequestAborted );
477478
}
478479

479480
bool INodeBuilderPolicy.AppliesToEndpoints( IReadOnlyList<Endpoint> endpoints ) =>

src/Common/src/Common/ApiVersioningOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public partial class ApiVersioningOptions
4747
/// <value>True if the a default API version should be assumed when a client does not
4848
/// provide an API version; otherwise, false. The default value is <c>false</c>.</value>
4949
/// <remarks>When a default API version is assumed, the version used is based up the
50-
/// result of the <see cref="IApiVersionSelector.SelectVersion"/> method.</remarks>
50+
/// result from <see cref="IApiVersionSelector"/>.</remarks>
5151
public bool AssumeDefaultVersionWhenUnspecified { get; set; }
5252

5353
/// <summary>

src/Common/src/Common/IApiVersionSelector.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ namespace Asp.Versioning;
1111
/// <summary>
1212
/// Defines the behavior of an API version selector.
1313
/// </summary>
14-
#if !NETFRAMEWORK
15-
[CLSCompliant( false )]
16-
#endif
17-
public interface IApiVersionSelector
14+
public partial interface IApiVersionSelector
1815
{
1916
/// <summary>
2017
/// Selects an API version given the specified HTTP request and API version information.

0 commit comments

Comments
 (0)