Skip to content

Commit 0a77175

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Refactors and pre-aggregates API version models in Web API. Fixes #322
1 parent 7925cbd commit 0a77175

File tree

32 files changed

+1005
-578
lines changed

32 files changed

+1005
-578
lines changed

src/Common/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
1212
/// </summary>
1313
public partial class ActionApiVersionConventionBuilderBase
1414
{
15-
readonly HashSet<ApiVersion> mappedVersions = new HashSet<ApiVersion>();
16-
1715
/// <summary>
1816
/// Initializes a new instance of the <see cref="ActionApiVersionConventionBuilderBase"/> class.
1917
/// </summary>
@@ -23,6 +21,6 @@ protected ActionApiVersionConventionBuilderBase() { }
2321
/// Gets the collection of API versions mapped to the current action.
2422
/// </summary>
2523
/// <value>A <see cref="ICollection{T}">collection</see> of mapped <see cref="ApiVersion">API versions</see>.</value>
26-
protected ICollection<ApiVersion> MappedVersions => mappedVersions;
24+
protected ICollection<ApiVersion> MappedVersions { get; } = new HashSet<ApiVersion>();
2725
}
2826
}

src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,8 @@
55

66
static class HttpControllerDescriptorExtensions
77
{
8-
const string RelatedControllerCandidatesKey = "MS_RelatedControllerCandidates";
9-
108
internal static IEnumerable<HttpControllerDescriptor> AsEnumerable( this HttpControllerDescriptor controllerDescriptor )
119
{
12-
if ( controllerDescriptor.Properties.TryGetValue( RelatedControllerCandidatesKey, out object value ) )
13-
{
14-
if ( value is IEnumerable<HttpControllerDescriptor> relatedCandidates )
15-
{
16-
using ( var relatedControllerDescriptors = relatedCandidates.GetEnumerator() )
17-
{
18-
if ( relatedControllerDescriptors.MoveNext() )
19-
{
20-
yield return controllerDescriptor;
21-
22-
do
23-
{
24-
if ( relatedControllerDescriptors.Current != controllerDescriptor )
25-
{
26-
yield return relatedControllerDescriptors.Current;
27-
}
28-
}
29-
while ( relatedControllerDescriptors.MoveNext() );
30-
31-
yield break;
32-
}
33-
}
34-
}
35-
}
36-
3710
if ( controllerDescriptor is IEnumerable<HttpControllerDescriptor> groupedControllerDescriptors )
3811
{
3912
foreach ( var groupedControllerDescriptor in groupedControllerDescriptors )
Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
namespace System.Web.Http
22
{
3-
using System.Collections.Generic;
43
using System.Diagnostics.Contracts;
5-
using System.Reflection;
64
using System.Web.Http.Routing;
7-
using static System.Reflection.BindingFlags;
85

96
static class HttpRouteCollectionExtensions
107
{
@@ -13,9 +10,7 @@ internal static string GetRouteName( this HttpRouteCollection routes, IHttpRoute
1310
Contract.Requires( routes != null );
1411
Contract.Requires( route != null );
1512

16-
var items = CopyRouteEntries( routes );
17-
18-
foreach ( var item in items )
13+
foreach ( var item in routes.ToDictionary() )
1914
{
2015
if ( Equals( item.Value, route ) )
2116
{
@@ -25,61 +20,5 @@ internal static string GetRouteName( this HttpRouteCollection routes, IHttpRoute
2520

2621
return null;
2722
}
28-
29-
static KeyValuePair<string, IHttpRoute>[] CopyRouteEntries( HttpRouteCollection routes )
30-
{
31-
Contract.Requires( routes != null );
32-
Contract.Ensures( Contract.Result<KeyValuePair<string, IHttpRoute>[]>() != null );
33-
34-
var items = new KeyValuePair<string, IHttpRoute>[routes.Count];
35-
36-
try
37-
{
38-
routes.CopyTo( items, 0 );
39-
}
40-
catch ( NotSupportedException ) when ( routes.GetType().FullName == "System.Web.Http.WebHost.Routing.HostedHttpRouteCollection" )
41-
{
42-
var keys = GetRouteKeys( routes );
43-
44-
for ( var i = 0; i < keys.Count; i++ )
45-
{
46-
var key = keys[i];
47-
var route = routes[key];
48-
49-
items[i] = new KeyValuePair<string, IHttpRoute>( key, route );
50-
}
51-
}
52-
53-
return items;
54-
}
55-
56-
static IReadOnlyList<string> GetRouteKeys( HttpRouteCollection routes )
57-
{
58-
Contract.Requires( routes != null );
59-
Contract.Ensures( Contract.Result<IReadOnlyList<string>>() != null );
60-
61-
var collection = GetKeys( routes );
62-
var keys = new string[collection.Count];
63-
64-
collection.CopyTo( keys, 0 );
65-
66-
return keys;
67-
}
68-
69-
static ICollection<string> GetKeys( HttpRouteCollection routes )
70-
{
71-
Contract.Requires( routes != null );
72-
Contract.Ensures( Contract.Result<ICollection<string>>() != null );
73-
74-
// HACK: System.Web.Routing.RouteCollection doesn't expose the names associated with registered routes. The
75-
// HostedHttpRouteCollection could have provided an adapter to support it, but didn't. Instead, it always throws
76-
// NotSupportedException for the HttpRouteCollection.CopyTo method. This only happens when hosted on IIS. The
77-
// only way to get the keys is use reflection to poke at the underlying dictionary.
78-
var routeCollection = routes.GetType().GetField( "_routeCollection", Instance | NonPublic ).GetValue( routes );
79-
var dictionary = routeCollection.GetType().GetField( "_namedMap", Instance | NonPublic ).GetValue( routeCollection );
80-
var keys = (ICollection<string>) dictionary.GetType().GetRuntimeProperty( "Keys" ).GetValue( dictionary, null );
81-
82-
return keys;
83-
}
8423
}
8524
}

src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
using Microsoft.Web.Http;
44
using Microsoft.Web.Http.Versioning;
55
using Microsoft.Web.Http.Versioning.Conventions;
6+
using System;
67
using System.Collections.Generic;
7-
using System.Diagnostics.CodeAnalysis;
88
using System.Diagnostics.Contracts;
99
using System.Linq;
1010
using System.Web.Http;
@@ -23,7 +23,6 @@ public partial class VersionedODataModelBuilder
2323
/// <remarks>This constructor resolves the current <see cref="IHttpControllerSelector"/> from the
2424
/// <see cref="ServicesExtensions.GetHttpControllerSelector(ServicesContainer)"/> extension method via the
2525
/// <see cref="HttpConfiguration.Services"/>.</remarks>
26-
[SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract." )]
2726
public VersionedODataModelBuilder( HttpConfiguration configuration )
2827
{
2928
Arg.NotNull( configuration, nameof( configuration ) );
@@ -55,15 +54,18 @@ protected virtual IReadOnlyList<ApiVersion> GetApiVersions()
5554
var assembliesResolver = services.GetAssembliesResolver();
5655
var typeResolver = services.GetHttpControllerTypeResolver();
5756
var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ).Where( TypeExtensions.IsODataController );
58-
var conventions = Options.Conventions;
57+
var controllerDescriptors = services.GetHttpControllerSelector().GetControllerMapping().Values;
5958
var supported = new HashSet<ApiVersion>();
6059
var deprecated = new HashSet<ApiVersion>();
6160

6261
foreach ( var controllerType in controllerTypes )
6362
{
64-
var descriptor = new HttpControllerDescriptor( Configuration, string.Empty, controllerType );
63+
var descriptor = FindControllerDescriptor( controllerDescriptors, controllerType );
6564

66-
conventions.ApplyTo( descriptor );
65+
if ( descriptor == null )
66+
{
67+
continue;
68+
}
6769

6870
var model = descriptor.GetApiVersionModel();
6971
var versions = model.SupportedApiVersions;
@@ -116,5 +118,31 @@ protected virtual void ConfigureMetadataController( IEnumerable<ApiVersion> supp
116118

117119
controllerBuilder.ApplyTo( controllerDescriptor );
118120
}
121+
122+
static HttpControllerDescriptor FindControllerDescriptor( IEnumerable<HttpControllerDescriptor> controllerDescriptors, Type controllerType )
123+
{
124+
Contract.Requires( controllerDescriptors != null );
125+
Contract.Requires( controllerType != null );
126+
127+
foreach ( var controllerDescriptor in controllerDescriptors )
128+
{
129+
if ( controllerDescriptor is IEnumerable<HttpControllerDescriptor> groupedControllerDescriptors )
130+
{
131+
foreach ( var groupedControllerDescriptor in groupedControllerDescriptors )
132+
{
133+
if ( controllerType.Equals( groupedControllerDescriptor.ControllerType ) )
134+
{
135+
return groupedControllerDescriptor;
136+
}
137+
}
138+
}
139+
else if ( controllerType.Equals( controllerDescriptor.ControllerType ) )
140+
{
141+
return controllerDescriptor;
142+
}
143+
}
144+
145+
return default;
146+
}
119147
}
120148
}

src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -610,12 +610,9 @@ public static IEdmModel GetEdmModel( this HttpConfiguration configuration, ApiVe
610610
Arg.NotNull( configuration, nameof( configuration ) );
611611
Arg.NotNull( apiVersion, nameof( apiVersion ) );
612612

613-
var allRoutes = configuration.Routes;
614-
var routes = new KeyValuePair<string, IHttpRoute>[allRoutes.Count];
613+
var routes = configuration.Routes.ToDictionary();
615614
var containers = configuration.GetRootContainerMappings();
616615

617-
allRoutes.CopyTo( routes, 0 );
618-
619616
foreach ( var route in routes )
620617
{
621618
if ( !( route.Value is ODataRoute odataRoute ) )

src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions()
218218

219219
var routes = FlattenRoutes( Configuration.Routes ).ToArray();
220220

221-
foreach ( var apiVersion in FlattenApiVersions() )
221+
foreach ( var apiVersion in FlattenApiVersions( controllerMappings ) )
222222
{
223223
foreach ( var route in routes )
224224
{
@@ -441,15 +441,16 @@ static IEnumerable<IHttpRoute> FlattenRoutes( IEnumerable<IHttpRoute> routes )
441441
}
442442
}
443443

444-
IEnumerable<ApiVersion> FlattenApiVersions()
444+
IEnumerable<ApiVersion> FlattenApiVersions( IDictionary<string, HttpControllerDescriptor> controllerMapping )
445445
{
446+
Contract.Requires( controllerMapping != null );
446447
Contract.Ensures( Contract.Result<IEnumerable<ApiVersion>>() != null );
447448

448449
var services = Configuration.Services;
449450
var assembliesResolver = services.GetAssembliesResolver();
450451
var typeResolver = services.GetHttpControllerTypeResolver();
451452
var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver );
452-
var options = Configuration.GetApiVersioningOptions();
453+
var controllerDescriptors = controllerMapping.Values;
453454
var declared = new HashSet<ApiVersion>();
454455
var supported = new HashSet<ApiVersion>();
455456
var deprecated = new HashSet<ApiVersion>();
@@ -458,13 +459,12 @@ IEnumerable<ApiVersion> FlattenApiVersions()
458459

459460
foreach ( var controllerType in controllerTypes )
460461
{
461-
var descriptor = new HttpControllerDescriptor()
462-
{
463-
Configuration = Configuration,
464-
ControllerType = controllerType,
465-
};
462+
var descriptor = FindControllerDescriptor( controllerDescriptors, controllerType );
466463

467-
options.Conventions.ApplyTo( descriptor );
464+
if ( descriptor == null )
465+
{
466+
continue;
467+
}
468468

469469
var model = descriptor.GetApiVersionModel();
470470

@@ -494,13 +494,39 @@ IEnumerable<ApiVersion> FlattenApiVersions()
494494

495495
if ( supported.Count == 0 )
496496
{
497-
supported.Add( options.DefaultApiVersion );
497+
supported.Add( Configuration.GetApiVersioningOptions().DefaultApiVersion );
498498
return supported;
499499
}
500500

501501
return supported.OrderBy( v => v );
502502
}
503503

504+
static HttpControllerDescriptor FindControllerDescriptor( IEnumerable<HttpControllerDescriptor> controllerDescriptors, Type controllerType )
505+
{
506+
Contract.Requires( controllerDescriptors != null );
507+
Contract.Requires( controllerType != null );
508+
509+
foreach ( var controllerDescriptor in controllerDescriptors )
510+
{
511+
if ( controllerDescriptor is IEnumerable<HttpControllerDescriptor> groupedControllerDescriptors )
512+
{
513+
foreach ( var groupedControllerDescriptor in groupedControllerDescriptors )
514+
{
515+
if ( controllerType.Equals( groupedControllerDescriptor.ControllerType ) )
516+
{
517+
return groupedControllerDescriptor;
518+
}
519+
}
520+
}
521+
else if ( controllerType.Equals( controllerDescriptor.ControllerType ) )
522+
{
523+
return controllerDescriptor;
524+
}
525+
}
526+
527+
return default;
528+
}
529+
504530
static HttpControllerDescriptor GetDirectRouteController( CandidateAction[] directRouteCandidates, ApiVersion apiVersion )
505531
{
506532
Contract.Requires( apiVersion != null );
@@ -1050,9 +1076,7 @@ static bool MatchRegexConstraint( IHttpRoute route, string parameterName, string
10501076
}
10511077

10521078
// note that we don't support custom constraint (IHttpRouteConstraint) because it might rely on the request and some runtime states
1053-
var constraintsRule = constraint as string;
1054-
1055-
if ( constraintsRule == null )
1079+
if ( !( constraint is string constraintsRule ) )
10561080
{
10571081
return true;
10581082
}

0 commit comments

Comments
 (0)