Skip to content

Commit 812f5bd

Browse files
Chris Martinezcommonsensesoftware
authored andcommitted
Final v1 changes for the ASP.NET Core implementation
1 parent 37ea90e commit 812f5bd

13 files changed

+558
-61
lines changed

src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiVersionDescription.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public ApiVersionDescription( ApiVersion apiVersion, string groupName, bool depr
2727
public ApiVersion ApiVersion { get; }
2828

2929
/// <summary>
30-
/// Gets the API version group name.
30+
/// Gets the API version group name.f
3131
/// </summary>
3232
/// <value>The group name for the API version.</value>
3333
public string GroupName { get; }

src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionDescriptionProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ protected virtual IReadOnlyList<ApiVersionDescription> EnumerateApiVersions( IAc
8585
AppendDescriptions( descriptions, supported, deprecated: false );
8686
AppendDescriptions( descriptions, deprecated, deprecated: true );
8787

88-
return descriptions.OrderBy( d => d.ApiVersion ).ThenBy( o => o.IsDeprecated ).ToArray();
88+
return descriptions.OrderBy( d => d.ApiVersion ).ToArray();
8989
}
9090

9191
static void BucketizeApiVersions( IReadOnlyList<ActionDescriptor> actions, ISet<ApiVersion> supported, ISet<ApiVersion> deprecated )

src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using Microsoft.AspNetCore.Mvc.ApplicationModels;
55
using Microsoft.AspNetCore.Mvc.Controllers;
66
using Microsoft.AspNetCore.Mvc.ModelBinding;
7+
using Microsoft.AspNetCore.Mvc.Routing;
78
using Microsoft.AspNetCore.Mvc.Versioning;
9+
using Microsoft.AspNetCore.Routing;
810
using System;
911
using System.Collections.Generic;
1012
using System.Diagnostics.Contracts;
@@ -22,16 +24,27 @@ public class VersionedApiDescriptionProvider : IApiDescriptionProvider
2224
/// Initializes a new instance of <see cref="VersionedApiDescriptionProvider"/> class.
2325
/// </summary>
2426
/// <param name="groupNameFormatter">The <see cref="IApiVersionGroupNameFormatter">formatter</see> used to get group names for API versions.</param>
27+
/// <param name="metadadataProvider">The <see cref="IModelMetadataProvider">provider</see> used to retrieve model metadata.</param>
2528
public VersionedApiDescriptionProvider( IApiVersionGroupNameFormatter groupNameFormatter, IModelMetadataProvider metadadataProvider )
2629
{
2730
Arg.NotNull( groupNameFormatter, nameof( groupNameFormatter ) );
28-
GroupNameFormatter = groupNameFormatter;
31+
Arg.NotNull( metadadataProvider, nameof( metadadataProvider ) );
2932

30-
this.metadadataProvider = metadadataProvider;
33+
GroupNameFormatter = groupNameFormatter;
34+
MetadadataProvider = metadadataProvider;
3135
}
3236

33-
readonly IModelMetadataProvider metadadataProvider;
37+
/// <summary>
38+
/// Gets the group name formatter associated with the API description provider.
39+
/// </summary>
40+
/// <value>The <see cref="IApiVersionGroupNameFormatter">group name formatter</see> used to format group names.</value>
41+
protected IApiVersionGroupNameFormatter GroupNameFormatter { get; }
3442

43+
/// <summary>
44+
/// Gets the model metadata provider associated with the API description provider.
45+
/// </summary>
46+
/// <value>The <see cref="IModelMetadataProvider">provider</see> used to retrieve model metadata.</value>
47+
protected IModelMetadataProvider MetadadataProvider { get; }
3548

3649
/// <summary>
3750
/// Gets the order prescendence of the current API description provider.
@@ -40,13 +53,7 @@ public VersionedApiDescriptionProvider( IApiVersionGroupNameFormatter groupNameF
4053
public virtual int Order => 0;
4154

4255
/// <summary>
43-
/// Gets the group name formatter associated with the provider.
44-
/// </summary>
45-
/// <value>The <see cref="IApiVersionGroupNameFormatter">group name formatter</see> used to format group names.</value>
46-
protected IApiVersionGroupNameFormatter GroupNameFormatter { get; }
47-
48-
/// <summary>
49-
/// Determines whether the specified action should be explored.
56+
/// Determines whether the specified action should be explored for the indicated API version.
5057
/// </summary>
5158
/// <param name="actionDescriptor">The <see cref="ActionDescriptor">action</see> to evaluate.</param>
5259
/// <param name="apiVersion">The <see cref="ApiVersion">API version</see> for action being explored.</param>
@@ -91,6 +98,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
9198
}
9299

93100
var groupResults = new List<ApiDescription>();
101+
var stringModelMetadata = new Lazy<ModelMetadata>( () => MetadadataProvider.GetMetadataForType( typeof( string ) ) );
94102

95103
foreach ( var version in FlattenApiVersions( results ) )
96104
{
@@ -102,21 +110,15 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
102110

103111
if ( ShouldExploreAction( action, version ) )
104112
{
105-
// BUG: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/315
106-
// BUG: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/355
107-
// HACK: this happens when the the ApiVersionRouteConstraint is used. it doesn't produce model metadata; not even string. can it be prevented beyond the Swagger/Swashuckle fix?
108-
foreach ( var param in result.ParameterDescriptions )
113+
foreach ( var parameter in result.ParameterDescriptions )
109114
{
110-
if ( param.ModelMetadata == null )
111-
{
112-
param.ModelMetadata = metadadataProvider.GetMetadataForType( typeof( string ) );
113-
}
115+
ApplyModelMetadataIfNecessary( parameter, stringModelMetadata );
114116
}
115117

116118
var groupResult = result.Clone();
117119

118120
groupResult.GroupName = groupName;
119-
groupResult.SetProperty( version );
121+
groupResult.SetApiVersion( version );
120122
groupResults.Add( groupResult );
121123
}
122124
}
@@ -134,6 +136,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
134136
/// Occurs when the providers are being executed.
135137
/// </summary>
136138
/// <param name="context">The current <see cref="ApiDescriptionProviderContext">execution context</see>.</param>
139+
/// <remarks>The default implementation performs no operation.</remarks>
137140
public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context ) { }
138141

139142
static IEnumerable<ApiVersion> FlattenApiVersions( IEnumerable<ApiDescription> descriptions )
@@ -157,5 +160,28 @@ static IEnumerable<ApiVersion> FlattenApiVersions( IEnumerable<ApiDescription> d
157160

158161
return versions.OrderBy( v => v );
159162
}
163+
164+
static void ApplyModelMetadataIfNecessary( ApiParameterDescription parameter, Lazy<ModelMetadata> stringModelMetadata )
165+
{
166+
if ( parameter.ModelMetadata != null )
167+
{
168+
return;
169+
}
170+
171+
var constraints = parameter?.RouteInfo.Constraints ?? Empty<IRouteConstraint>();
172+
173+
// versioning by URL path segment is the only method that the built-in api explorer will detect as a parameter.
174+
// since the route parameter likely has no counterpart in model binding, fill in what the model metadata "should"
175+
// be. this is only required when the ApiVersionRouteConstraint is found. all other methods such as versioning
176+
// by query string, header, or media type will require service authors to add the corresponding parameter in
177+
// tools such as Swagger. treat the api version as a string for the purposes of api exploration.
178+
if ( constraints.OfType<ApiVersionRouteConstraint>().Any() )
179+
{
180+
var modelMetadata = stringModelMetadata.Value;
181+
182+
parameter.ModelMetadata = modelMetadata;
183+
parameter.Type = modelMetadata.ModelType;
184+
}
185+
}
160186
}
161187
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
2+
{
3+
using FluentAssertions;
4+
using Microsoft.AspNetCore.Mvc.Abstractions;
5+
using Microsoft.AspNetCore.Mvc.Formatters;
6+
using Moq;
7+
using Xunit;
8+
9+
public class ApiDescriptionExtensionsTest
10+
{
11+
[Fact]
12+
public void get_api_version_should_associated_value()
13+
{
14+
// arrange
15+
var version = new ApiVersion( 42, 0 );
16+
var description = new ApiDescription();
17+
18+
description.Properties[typeof( ApiVersion )] = version;
19+
20+
// act
21+
var value = description.GetApiVersion();
22+
23+
// assert
24+
value.Should().Be( version );
25+
}
26+
27+
[Fact]
28+
public void set_api_version_should_associate_value()
29+
{
30+
// arrange
31+
var version = new ApiVersion( 42, 0 );
32+
var description = new ApiDescription();
33+
34+
description.SetApiVersion( version );
35+
36+
// act
37+
var value = (ApiVersion) description.Properties[typeof( ApiVersion )];
38+
39+
// assert
40+
value.Should().Be( version );
41+
}
42+
43+
[Fact]
44+
public void clone_api_description_should_create_a_shallow_copy()
45+
{
46+
// arrange
47+
var original = new ApiDescription()
48+
{
49+
GroupName = "Test",
50+
HttpMethod = "GET",
51+
RelativePath = "test",
52+
ActionDescriptor = new ActionDescriptor(),
53+
Properties = { ["key"] = new object() },
54+
ParameterDescriptions = { new ApiParameterDescription() },
55+
SupportedRequestFormats =
56+
{
57+
new ApiRequestFormat()
58+
{
59+
Formatter = new Mock<IInputFormatter>().Object,
60+
MediaType = "application/json"
61+
}
62+
},
63+
SupportedResponseTypes =
64+
{
65+
new ApiResponseType()
66+
{
67+
ApiResponseFormats =
68+
{
69+
new ApiResponseFormat()
70+
{
71+
Formatter = new Mock<IOutputFormatter>().Object,
72+
MediaType = "application/json"
73+
}
74+
},
75+
StatusCode = 200,
76+
Type = typeof( object )
77+
}
78+
}
79+
};
80+
81+
// act
82+
var clone = original.Clone();
83+
84+
// assert
85+
clone.ShouldBeEquivalentTo( original );
86+
}
87+
}
88+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
2+
{
3+
using FluentAssertions;
4+
using Microsoft.AspNetCore.Mvc.Abstractions;
5+
using Microsoft.AspNetCore.Mvc.ApplicationModels;
6+
using Microsoft.AspNetCore.Mvc.Infrastructure;
7+
using Microsoft.AspNetCore.Mvc.Versioning;
8+
using Moq;
9+
using System.Reflection;
10+
using Xunit;
11+
using static System.Linq.Enumerable;
12+
13+
public class DefaultApiVersionDescriptionProviderTest
14+
{
15+
[Fact]
16+
public void api_version_descriptions_should_collate_expected_versions()
17+
{
18+
// arrange
19+
var actionProvider = new TestActionDescriptorCollectionProvider();
20+
var groupNameFormatter = new DefaultApiVersionGroupNameFormatter();
21+
var descriptionProvider = new DefaultApiVersionDescriptionProvider( actionProvider, groupNameFormatter );
22+
23+
// act
24+
var descriptions = descriptionProvider.ApiVersionDescriptions;
25+
26+
// assert
27+
descriptions.ShouldBeEquivalentTo(
28+
new[]
29+
{
30+
new ApiVersionDescription( new ApiVersion( 0, 9 ), "v0.9", true ),
31+
new ApiVersionDescription( new ApiVersion( 1, 0 ), "v1", false ),
32+
new ApiVersionDescription( new ApiVersion( 2, 0 ), "v2", false ),
33+
new ApiVersionDescription( new ApiVersion( 3, 0 ), "v3", false )
34+
} );
35+
}
36+
37+
[Fact]
38+
public void is_deprecated_should_return_false_without_api_vesioning()
39+
{
40+
// arrange
41+
var provider = new DefaultApiVersionDescriptionProvider(
42+
new Mock<IActionDescriptorCollectionProvider>().Object,
43+
new Mock<IApiVersionGroupNameFormatter>().Object );
44+
var action = new ActionDescriptor();
45+
46+
// act
47+
var result = provider.IsDeprecated( action, new ApiVersion( 1, 0 ) );
48+
49+
// assert
50+
result.Should().BeFalse();
51+
}
52+
53+
[Fact]
54+
public void is_deprecated_should_return_false_when_controller_is_versionX2Dneutral()
55+
{
56+
// arrange
57+
var provider = new DefaultApiVersionDescriptionProvider(
58+
new Mock<IActionDescriptorCollectionProvider>().Object,
59+
new Mock<IApiVersionGroupNameFormatter>().Object );
60+
var action = new ActionDescriptor();
61+
var controller = new ControllerModel( typeof( Controller ).GetTypeInfo(), new object[0] );
62+
63+
controller.SetProperty( ApiVersionModel.Neutral );
64+
action.SetProperty( controller );
65+
66+
// act
67+
var result = provider.IsDeprecated( action, new ApiVersion( 1, 0 ) );
68+
69+
// assert
70+
result.Should().BeFalse();
71+
}
72+
73+
[Theory]
74+
[InlineData( 1, true )]
75+
[InlineData( 2, false )]
76+
public void is_deprecated_should_return_expected_result_for_deprecated_version( int majorVersion, bool expected )
77+
{
78+
// arrange
79+
var provider = new DefaultApiVersionDescriptionProvider(
80+
new Mock<IActionDescriptorCollectionProvider>().Object,
81+
new Mock<IApiVersionGroupNameFormatter>().Object );
82+
var action = new ActionDescriptor();
83+
var controller = new ControllerModel( typeof( Controller ).GetTypeInfo(), new object[0] );
84+
var model = new ApiVersionModel(
85+
declaredVersions: new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ) },
86+
supportedVersions: new[] { new ApiVersion( 2, 0 ) },
87+
deprecatedVersions: new[] { new ApiVersion( 1, 0 ) },
88+
advertisedVersions: Empty<ApiVersion>(),
89+
deprecatedAdvertisedVersions: Empty<ApiVersion>() );
90+
91+
controller.SetProperty( model );
92+
action.SetProperty( controller );
93+
94+
// act
95+
var result = provider.IsDeprecated( action, new ApiVersion( majorVersion, 0 ) );
96+
97+
// assert
98+
result.Should().Be( expected );
99+
}
100+
}
101+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
2+
{
3+
using FluentAssertions;
4+
using System;
5+
using System.Collections.Generic;
6+
using Xunit;
7+
8+
public class DefaultApiVersionGroupNameFormatterTest
9+
{
10+
[Theory]
11+
[MemberData( nameof( GroupNameData ) )]
12+
public void get_group_name_should_return_expected_text( ApiVersion version, string groupName )
13+
{
14+
// arrange
15+
var formatter = new DefaultApiVersionGroupNameFormatter();
16+
17+
// act
18+
var result = formatter.GetGroupName( version );
19+
20+
// assert
21+
result.Should().Be( groupName );
22+
}
23+
24+
public static IEnumerable<object[]> GroupNameData
25+
{
26+
get
27+
{
28+
yield return new object[] { new ApiVersion( 1, 0 ), "v1" };
29+
yield return new object[] { new ApiVersion( 1, 0, "RC" ), "v1-RC" };
30+
yield return new object[] { new ApiVersion( 1, 1 ), "v1.1" };
31+
yield return new object[] { new ApiVersion( 1, 1, "Beta" ), "v1.1-Beta" };
32+
yield return new object[] { new ApiVersion( 0, 1 ), "v0.1" };
33+
yield return new object[] { new ApiVersion( 0, 1, "Alpha" ), "v0.1-Alpha" };
34+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ) ), "2017-04-01" };
35+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), "Beta" ), "2017-04-01-Beta" };
36+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 0 ), "2017-04-01-1" };
37+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 0, "RC" ), "2017-04-01-1-RC" };
38+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 1 ), "2017-04-01-1.1" };
39+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 1, "Beta" ), "2017-04-01-1.1-Beta" };
40+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 0, 1 ), "2017-04-01-0.1" };
41+
yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 0, 1, "Alpha" ), "2017-04-01-0.1-Alpha" };
42+
}
43+
}
44+
}
45+
}

test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/IApiDescriptionProviderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
1+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
22
{
33
using Microsoft.AspNetCore.Mvc.Abstractions;
44
using Microsoft.AspNetCore.Mvc.ApiExplorer;

0 commit comments

Comments
 (0)