Skip to content

Commit 4c969b1

Browse files
Explicit API description should supersede implicit one. Relates to #1025
1 parent ee996f2 commit 4c969b1

File tree

2 files changed

+133
-1
lines changed

2 files changed

+133
-1
lines changed

src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
172172

173173
groupResult.SetApiVersion( version );
174174
PopulateApiVersionParameters( groupResult, version );
175-
groupResults.Add( groupResult );
175+
AddOrUpdateResult( groupResults, groupResult, metadata, version );
176176
}
177177
}
178178

@@ -245,6 +245,47 @@ private static void TryUpdateControllerRouteValueForMinimalApi( ApiDescription d
245245
}
246246
}
247247

248+
private static void AddOrUpdateResult(
249+
List<ApiDescription> results,
250+
ApiDescription result,
251+
ApiVersionMetadata metadata,
252+
ApiVersion version )
253+
{
254+
var comparer = StringComparer.OrdinalIgnoreCase;
255+
256+
for ( var i = results.Count - 1; i >= 0; i-- )
257+
{
258+
var other = results[i];
259+
260+
if ( comparer.Equals( result.GroupName, other.GroupName ) &&
261+
comparer.Equals( result.RelativePath, other.RelativePath ) &&
262+
comparer.Equals( result.HttpMethod, other.HttpMethod ) )
263+
{
264+
var mapping = other.ActionDescriptor.GetApiVersionMetadata().MappingTo( version );
265+
266+
switch ( metadata.MappingTo( version ) )
267+
{
268+
case Explicit:
269+
if ( mapping == Implicit )
270+
{
271+
results.RemoveAt( i );
272+
}
273+
274+
break;
275+
case Implicit:
276+
if ( mapping == Explicit )
277+
{
278+
return;
279+
}
280+
281+
break;
282+
}
283+
}
284+
}
285+
286+
results.Add( result );
287+
}
288+
248289
private IEnumerable<ApiVersion> FlattenApiVersions( IList<ApiDescription> descriptions )
249290
{
250291
var versions = default( SortedSet<ApiVersion> );

src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,97 @@ public void versioned_api_explorer_should_use_custom_group_name()
149149
context.Results.Single().GroupName.Should().Be( "Test-1.0" );
150150
}
151151

152+
[Fact]
153+
public void versioned_api_explorer_should_prefer_explicit_over_implicit_action_match()
154+
{
155+
// arrange
156+
var @implicit = new ActionDescriptor()
157+
{
158+
DisplayName = "Implicit GET ~/test?api-version=[1.0,2.0]",
159+
EndpointMetadata = new[]
160+
{
161+
new ApiVersionMetadata(
162+
new ApiVersionModel(
163+
new ApiVersion[] { new( 1.0 ), new( 2.0 ) },
164+
new ApiVersion[] { new( 1.0 ), new( 2.0 ) },
165+
Array.Empty<ApiVersion>(),
166+
Array.Empty<ApiVersion>(),
167+
Array.Empty<ApiVersion>() ),
168+
new ApiVersionModel(
169+
Array.Empty<ApiVersion>(),
170+
new ApiVersion[] { new( 1.0 ), new( 2.0 ) },
171+
Array.Empty<ApiVersion>(),
172+
Array.Empty<ApiVersion>(),
173+
Array.Empty<ApiVersion>() ) ),
174+
},
175+
};
176+
var @explicit = new ActionDescriptor()
177+
{
178+
DisplayName = "Explicit GET ~/test?api-version=2.0",
179+
EndpointMetadata = new[]
180+
{
181+
new ApiVersionMetadata(
182+
new ApiVersionModel(
183+
new ApiVersion[] { new( 1.0 ), new( 2.0 ) },
184+
new ApiVersion[] { new( 1.0 ), new( 2.0 ) },
185+
Array.Empty<ApiVersion>(),
186+
Array.Empty<ApiVersion>(),
187+
Array.Empty<ApiVersion>() ),
188+
new ApiVersionModel(
189+
new ApiVersion[] { new( 2.0 ) },
190+
new ApiVersion[] { new( 1.0 ), new( 2.0 ) },
191+
Array.Empty<ApiVersion>(),
192+
Array.Empty<ApiVersion>(),
193+
Array.Empty<ApiVersion>() ) ),
194+
},
195+
};
196+
var actionProvider = new TestActionDescriptorCollectionProvider( @implicit, @explicit );
197+
var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items );
198+
199+
context.Results.Add(
200+
new()
201+
{
202+
HttpMethod = "GET",
203+
RelativePath = "test",
204+
ActionDescriptor = context.Actions[0],
205+
} );
206+
207+
context.Results.Add(
208+
new()
209+
{
210+
HttpMethod = "GET",
211+
RelativePath = "test",
212+
ActionDescriptor = context.Actions[1],
213+
} );
214+
215+
var apiExplorer = new VersionedApiDescriptionProvider(
216+
Mock.Of<ISunsetPolicyManager>(),
217+
NewModelMetadataProvider(),
218+
Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) );
219+
220+
// act
221+
apiExplorer.OnProvidersExecuted( context );
222+
223+
// assert
224+
context.Results.Should().BeEquivalentTo(
225+
new[]
226+
{
227+
new
228+
{
229+
GroupName = "v1",
230+
ActionDescriptor = @implicit,
231+
Properties = new Dictionary<object, object>() { [typeof( ApiVersion )] = new ApiVersion( 1.0 ) },
232+
},
233+
new
234+
{
235+
GroupName = "v2",
236+
ActionDescriptor = @explicit,
237+
Properties = new Dictionary<object, object>() { [typeof( ApiVersion )] = new ApiVersion( 2.0 ) },
238+
},
239+
},
240+
options => options.ExcludingMissingMembers() );
241+
}
242+
152243
private static IModelMetadataProvider NewModelMetadataProvider()
153244
{
154245
var provider = new Mock<IModelMetadataProvider>();

0 commit comments

Comments
 (0)