Skip to content

Commit 5f7df92

Browse files
Refactored the way API version conventions are mapped to controller actions, which also resolves a few flaky tests. Fixes #55. (#57)
1 parent 28e21a9 commit 5f7df92

File tree

14 files changed

+210
-90
lines changed

14 files changed

+210
-90
lines changed

ApiVersioning.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.OData.Vers
8787
EndProject
8888
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Conventions", "Conventions", "{B24995FB-AF48-4E5D-9327-377A599BDE2A}"
8989
ProjectSection(SolutionItems) = preProject
90+
src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderCollectionT.cs = src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderCollectionT.cs
9091
src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderT.cs = src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderT.cs
9192
src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderTExtensions.cs = src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderTExtensions.cs
9293
src\Common\Versioning\Conventions\ActionConventionBuilderTExtensions.cs = src\Common\Versioning\Conventions\ActionConventionBuilderTExtensions.cs

ApiVersioningWithSamples.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,19 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.WebApi.Acc
116116
EndProject
117117
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Mvc.Acceptance.Tests", "test\Microsoft.AspNetCore.Mvc.Acceptance.Tests\Microsoft.AspNetCore.Mvc.Acceptance.Tests.xproj", "{4EED304C-D1A6-4866-8D7F-450D084FD25D}"
118118
EndProject
119+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Conventions", "Conventions", "{2ABB1DE5-8E77-440D-9517-4A5E6877D1C5}"
120+
ProjectSection(SolutionItems) = preProject
121+
src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderCollectionT.cs = src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderCollectionT.cs
122+
src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderT.cs = src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderT.cs
123+
src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderTExtensions.cs = src\Common\Versioning\Conventions\ActionApiVersionConventionBuilderTExtensions.cs
124+
src\Common\Versioning\Conventions\ActionConventionBuilderTExtensions.cs = src\Common\Versioning\Conventions\ActionConventionBuilderTExtensions.cs
125+
src\Common\Versioning\Conventions\ControllerApiVersionConventionBuilderT.cs = src\Common\Versioning\Conventions\ControllerApiVersionConventionBuilderT.cs
126+
src\Common\Versioning\Conventions\ControllerApiVersionConventionBuilderTExtensions.cs = src\Common\Versioning\Conventions\ControllerApiVersionConventionBuilderTExtensions.cs
127+
src\Common\Versioning\Conventions\ExpressionExtensions.cs = src\Common\Versioning\Conventions\ExpressionExtensions.cs
128+
src\Common\Versioning\Conventions\IActionConventionBuilderT.cs = src\Common\Versioning\Conventions\IActionConventionBuilderT.cs
129+
src\Common\Versioning\Conventions\IApiVersionConventionT.cs = src\Common\Versioning\Conventions\IApiVersionConventionT.cs
130+
EndProjectSection
131+
EndProject
119132
Global
120133
GlobalSection(SolutionConfigurationPlatforms) = preSolution
121134
Debug|Any CPU = Debug|Any CPU
@@ -214,5 +227,6 @@ Global
214227
{1EFC221F-35CF-4B55-BD59-240D5B808E14} = {900DD210-8500-4D89-A05D-C9526935A719}
215228
{5C31964D-EA8B-420B-9297-5ADFEFE54962} = {0987757E-4D09-4523-B9C9-65B1E8832AA1}
216229
{4EED304C-D1A6-4866-8D7F-450D084FD25D} = {0987757E-4D09-4523-B9C9-65B1E8832AA1}
230+
{2ABB1DE5-8E77-440D-9517-4A5E6877D1C5} = {DE4EE45F-F8EA-4B32-B16F-441F946ACEF4}
217231
EndGlobalSection
218232
EndGlobal
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning.Conventions
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
5+
#endif
6+
{
7+
using System;
8+
using System.Collections;
9+
using System.Collections.Generic;
10+
using System.Diagnostics.Contracts;
11+
using System.Linq;
12+
using System.Reflection;
13+
14+
/// <summary>
15+
/// Represents a collection of controller action convention builders.
16+
/// </summary>
17+
public partial class ActionApiVersionConventionBuilderCollection<T> : IReadOnlyCollection<ActionApiVersionConventionBuilder<T>>
18+
{
19+
private readonly ControllerApiVersionConventionBuilder<T> controllerBuilder;
20+
private readonly IList<ActionBuilderMapping<T>> actionBuilderMappings = new List<ActionBuilderMapping<T>>();
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="ActionApiVersionConventionBuilderCollection{T}"/> class.
24+
/// </summary>
25+
/// <param name="controllerBuilder">The associated <see cref="ControllerApiVersionConventionBuilder{T}">controller convention builder</see>.</param>
26+
public ActionApiVersionConventionBuilderCollection( ControllerApiVersionConventionBuilder<T> controllerBuilder )
27+
{
28+
Arg.NotNull( controllerBuilder, nameof( controllerBuilder ) );
29+
this.controllerBuilder = controllerBuilder;
30+
}
31+
32+
/// <summary>
33+
/// Gets or adds a controller action convention builder for the specified method.
34+
/// </summary>
35+
/// <param name="actionMethod">The controller action method to get or add the convention builder for.</param>
36+
/// <returns>A new or existing <see cref="ActionApiVersionConventionBuilder{T}">controller action convention builder</see>.</returns>
37+
protected internal virtual ActionApiVersionConventionBuilder<T> GetOrAdd( MethodInfo actionMethod )
38+
{
39+
Arg.NotNull( actionMethod, nameof( actionMethod ) );
40+
41+
var mapping = actionBuilderMappings.FirstOrDefault( m => m.Method == actionMethod );
42+
43+
if ( mapping == null )
44+
{
45+
mapping = new ActionBuilderMapping<T>( actionMethod, new ActionApiVersionConventionBuilder<T>( controllerBuilder ) );
46+
actionBuilderMappings.Add( mapping );
47+
}
48+
49+
return mapping.Builder;
50+
}
51+
52+
/// <summary>
53+
/// Gets a count of the controller action convention builders in the collection.
54+
/// </summary>
55+
/// <value>The total number of controller action convention builders in the collection.</value>
56+
public virtual int Count => actionBuilderMappings.Count;
57+
58+
/// <summary>
59+
/// Attempts to retrieve the controller action convention builder for the specified method.
60+
/// </summary>
61+
/// <param name="actionMethod">The controller action method to get the convention builder for.</param>
62+
/// <param name="actionBuilder">The <see cref="ActionApiVersionConventionBuilder{T}">controller action convention builder</see> or <c>null</c>.</param>
63+
/// <returns>True if the <paramref name="actionBuilder">action builder</paramref> is successfully retrieved; otherwise, false.</returns>
64+
public virtual bool TryGetValue( MethodInfo actionMethod, out ActionApiVersionConventionBuilder<T> actionBuilder )
65+
{
66+
actionBuilder = null;
67+
68+
if ( actionMethod == null )
69+
{
70+
return false;
71+
}
72+
73+
var mapping = actionBuilderMappings.FirstOrDefault( m => m.Method == actionMethod );
74+
75+
return ( actionBuilder = mapping?.Builder ) != null;
76+
}
77+
78+
/// <summary>
79+
/// Returns an iterator that enumerates the controller action convention builders in the collection.
80+
/// </summary>
81+
/// <returns>An <see cref="IEnumerator{T}"/> object.</returns>
82+
public virtual IEnumerator<ActionApiVersionConventionBuilder<T>> GetEnumerator()
83+
{
84+
foreach ( var mapping in actionBuilderMappings )
85+
{
86+
yield return mapping.Builder;
87+
}
88+
}
89+
90+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
91+
92+
private sealed partial class ActionBuilderMapping<TModel>
93+
{
94+
internal ActionBuilderMapping( MethodInfo method, ActionApiVersionConventionBuilder<TModel> builder )
95+
{
96+
Contract.Requires( method != null );
97+
Contract.Requires( builder != null );
98+
99+
Method = method;
100+
Builder = builder;
101+
}
102+
103+
internal MethodInfo Method { get; }
104+
105+
internal ActionApiVersionConventionBuilder<TModel> Builder { get; }
106+
}
107+
}
108+
}

src/Common/Versioning/Conventions/ControllerApiVersionConventionBuilderT.cs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ public partial class ControllerApiVersionConventionBuilder<T>
2121
private readonly HashSet<ApiVersion> advertisedVersions = new HashSet<ApiVersion>();
2222
private readonly HashSet<ApiVersion> deprecatedAdvertisedVersions = new HashSet<ApiVersion>();
2323

24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="ControllerApiVersionConventionBuilder{T}"/> class.
26+
/// </summary>
27+
public ControllerApiVersionConventionBuilder()
28+
{
29+
ActionBuilders = new ActionApiVersionConventionBuilderCollection<T>( this );
30+
}
31+
2432
/// <summary>
2533
/// Gets or sets a value indicating whether the current controller is API version-neutral.
2634
/// </summary>
@@ -54,10 +62,9 @@ public partial class ControllerApiVersionConventionBuilder<T>
5462
/// <summary>
5563
/// Gets a collection of controller action convention builders.
5664
/// </summary>
57-
/// <value>A <see cref="IDictionary{TKey, TValue}">collection</see> of
65+
/// <value>A <see cref="ActionApiVersionConventionBuilderCollection{T}">collection</see> of
5866
/// <see cref="ActionApiVersionConventionBuilder{T}">controller action convention builders</see>.</value>
59-
protected IDictionary<int, ActionApiVersionConventionBuilder<T>> ActionBuilders { get; } =
60-
new Dictionary<int, ActionApiVersionConventionBuilder<T>>();
67+
protected virtual ActionApiVersionConventionBuilderCollection<T> ActionBuilders { get; }
6168

6269
/// <summary>
6370
/// Indicates that the controller is API version-neutral.
@@ -136,16 +143,7 @@ public virtual ActionApiVersionConventionBuilder<T> Action( MethodInfo actionMet
136143
{
137144
Arg.NotNull( actionMethod, nameof( actionMethod ) );
138145
Contract.Ensures( Contract.Result<ActionApiVersionConventionBuilder<T>>() != null );
139-
140-
var key = actionMethod.GetHashCode();
141-
var actionBuilder = default( ActionApiVersionConventionBuilder<T> );
142-
143-
if ( !ActionBuilders.TryGetValue( key, out actionBuilder ) )
144-
{
145-
ActionBuilders[key] = actionBuilder = new ActionApiVersionConventionBuilder<T>( this );
146-
}
147-
148-
return actionBuilder;
146+
return ActionBuilders.GetOrAdd( actionMethod );
149147
}
150148
}
151149
}

src/Microsoft.AspNet.OData.Versioning/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
"dependencies": {
1010
"Microsoft.AspNet.OData": "[5.9.1,6.0.0)",
11-
"Microsoft.AspNet.WebApi.Versioning": "2.0.1-*"
11+
"Microsoft.AspNet.WebApi.Versioning": "2.0.2-*"
1212
},
1313

1414
"frameworks": {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Microsoft.Web.Http.Versioning.Conventions
2+
{
3+
using System;
4+
using System.Web.Http.Controllers;
5+
6+
/// <content>
7+
/// Provides additional implementation specific to Microsoft ASP.NET Web API.
8+
/// </content>
9+
/// <typeparam name="T">The <see cref="Type">type</see> of <see cref="IHttpController">controller</see>.</typeparam>
10+
public partial class ActionApiVersionConventionBuilderCollection<T> where T : IHttpController
11+
{
12+
private sealed partial class ActionBuilderMapping<TModel> where TModel : IHttpController
13+
{
14+
}
15+
}
16+
}

src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderT.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private void ApplyActionConventions( HttpControllerDescriptor controllerDescript
8484

8585
foreach ( var actionDescriptor in actionDescriptors )
8686
{
87-
var key = actionDescriptor.MethodInfo.GetHashCode();
87+
var key = actionDescriptor.MethodInfo;
8888
var actionBuilder = default( ActionApiVersionConventionBuilder<T> );
8989

9090
if ( ActionBuilders.TryGetValue( key, out actionBuilder ) )

src/Microsoft.AspNet.WebApi.Versioning/project.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "2.0.1-*",
2+
"version": "2.0.2-*",
33
"authors": [ "Microsoft" ],
44
"title": "Microsoft ASP.NET Web API Versioning",
55
"copyright": "Copyright \u00A9 2016. Microsoft Corporation. All rights reserved.",
@@ -26,7 +26,7 @@
2626
"summary": "Provides API versioning for RESTful services created using ASP.NET Web API",
2727
"tags": [ "Microsoft", "AspNet", "AspNetWebAPI", "Versioning" ],
2828
"owners": [ "Microsoft" ],
29-
"releaseNotes": "\u2022 Fixed InvalidCastException in attribute-routing (Issue #44)",
29+
"releaseNotes": "\u2022 Fixed mapping of API-versioned actions by convention (Issue #55)",
3030
"iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288890",
3131
"licenseUrl": "https://raw.githubusercontent.com/Microsoft/aspnet-api-versioning/master/LICENSE",
3232
"requireLicenseAcceptance": true,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
2+
{
3+
using ApplicationModels;
4+
using System;
5+
6+
/// <content>
7+
/// Provides additional implementation specific to Microsoft ASP.NET Core.
8+
/// </content>
9+
/// <typeparam name="T">The <see cref="Type">type</see> of <see cref="ICommonModel">model</see>.</typeparam>
10+
[CLSCompliant( false )]
11+
public partial class ActionApiVersionConventionBuilderCollection<T>
12+
{
13+
}
14+
}

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderT.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private void ApplyActionConventions( ControllerModel controller, ControllerVersi
8989

9090
foreach ( var action in controller.Actions )
9191
{
92-
var key = action.ActionMethod.GetHashCode();
92+
var key = action.ActionMethod;
9393
var actionBuilder = default( ActionApiVersionConventionBuilder<T> );
9494

9595
action.SetProperty( controller );

0 commit comments

Comments
 (0)