Skip to content

Commit 93b6049

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Support API version using namespace convention. Closes #278.
1 parent da7710b commit 93b6049

27 files changed

+865
-183
lines changed

src/Common/Common.projitems

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ActionApiVersionConventionBuilderExtensions.cs" />
3939
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ActionApiVersionConventionBuilderTExtensions.cs" />
4040
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ActionConventionBuilderExtensions.cs" />
41+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ApiVersionConventionBuilder.cs" />
4142
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ControllerApiVersionConventionBuilderBase.cs" />
4243
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ControllerApiVersionConventionBuilder.cs" />
4344
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ControllerApiVersionConventionBuilder{T}.cs" />
@@ -47,6 +48,8 @@
4748
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IActionConventionBuilder.cs" />
4849
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IActionConventionBuilder{T}.cs" />
4950
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IApiVersionConvention{T}.cs" />
51+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IControllerConventionBuilder.cs" />
52+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\VersionByNamespaceConvention.cs" />
5053
<Compile Include="$(MSBuildThisFileDirectory)Versioning\CurrentImplementationApiVersionSelector.cs" />
5154
<Compile Include="$(MSBuildThisFileDirectory)Versioning\DefaultApiVersionReporter.cs" />
5255
<Compile Include="$(MSBuildThisFileDirectory)Versioning\DefaultApiVersionSelector.cs" />
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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.Generic;
9+
using System.Diagnostics.Contracts;
10+
#if WEBAPI
11+
using System.Web.Http.Controllers;
12+
using Model = System.Web.Http.Controllers.HttpControllerDescriptor;
13+
using TypeInfo = System.Type;
14+
#else
15+
using System.Reflection;
16+
using Model = Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerModel;
17+
#endif
18+
19+
/// <summary>
20+
/// Represents an object used to configure and create API version conventions for a controllers and their actions.
21+
/// </summary>
22+
public partial class ApiVersionConventionBuilder
23+
{
24+
/// <summary>
25+
/// Gets a collection of controller convention builders.
26+
/// </summary>
27+
/// <value>A <see cref="IDictionary{TKey, TValue}">collection</see> of <see cref="IControllerConventionBuilder">controller convention builders</see>.</value>
28+
protected IDictionary<TypeInfo, IControllerConventionBuilder> ControllerConventionBuilders { get; } = new Dictionary<TypeInfo, IControllerConventionBuilder>();
29+
30+
/// <summary>
31+
/// Gets a collection of controller conventions.
32+
/// </summary>
33+
/// <value>A <see cref="IList{T}">list</see> of <see cref="IControllerConvention">controller conventions</see>.</value>
34+
protected IList<IControllerConvention> ControllerConventions { get; } = new List<IControllerConvention>();
35+
36+
/// <summary>
37+
/// Gets the count of configured conventions.
38+
/// </summary>
39+
/// <value>The total count of configured conventions.</value>
40+
public virtual int Count => ControllerConventionBuilders.Count + ControllerConventions.Count;
41+
42+
/// <summary>
43+
/// Gets or creates the convention builder for the specified controller.
44+
/// </summary>
45+
/// <typeparam name="TController">The <see cref="Type">type</see> of controller to build conventions for.</typeparam>
46+
/// <returns>A new or existing <see cref="ControllerApiVersionConventionBuilder{T}"/>.</returns>
47+
public virtual ControllerApiVersionConventionBuilder<TController> Controller<TController>()
48+
#if WEBAPI
49+
where TController : IHttpController
50+
#endif
51+
{
52+
Contract.Ensures( Contract.Result<ControllerApiVersionConventionBuilder<TController>>() != null );
53+
54+
var key = GetKey( typeof( TController ) );
55+
56+
if ( !ControllerConventionBuilders.TryGetValue( key, out var builder ) )
57+
{
58+
var newBuilder = new ControllerApiVersionConventionBuilder<TController>();
59+
ControllerConventionBuilders[key] = newBuilder;
60+
return newBuilder;
61+
}
62+
63+
if ( builder is ControllerApiVersionConventionBuilder<TController> typedBuilder )
64+
{
65+
return typedBuilder;
66+
}
67+
68+
throw new InvalidOperationException( SR.ConventionStyleMismatch.FormatDefault( key.Name ) );
69+
}
70+
71+
/// <summary>
72+
/// Gets or creates the convention builder for the specified controller.
73+
/// </summary>
74+
/// <param name="controllerType">The <see cref="Type">type</see> of controller to build conventions for.</param>
75+
/// <returns>A new or existing <see cref="ControllerApiVersionConventionBuilder"/>.</returns>
76+
public virtual ControllerApiVersionConventionBuilder Controller( Type controllerType )
77+
{
78+
Arg.NotNull( controllerType, nameof( controllerType ) );
79+
Contract.Ensures( Contract.Result<ControllerApiVersionConventionBuilder>() != null );
80+
81+
var key = GetKey( controllerType );
82+
83+
if ( !ControllerConventionBuilders.TryGetValue( key, out var builder ) )
84+
{
85+
var newBuilder = new ControllerApiVersionConventionBuilder( controllerType );
86+
ControllerConventionBuilders[key] = newBuilder;
87+
return newBuilder;
88+
}
89+
90+
if ( builder is ControllerApiVersionConventionBuilder typedBuilder )
91+
{
92+
return typedBuilder;
93+
}
94+
95+
throw new InvalidOperationException( SR.ConventionStyleMismatch.FormatDefault( key.Name ) );
96+
}
97+
98+
/// <summary>
99+
/// Adds a new convention applied to all controllers.
100+
/// </summary>
101+
/// <param name="convention">The <see cref="IControllerConvention">convention</see> to be applied.</param>
102+
public virtual void Add( IControllerConvention convention )
103+
{
104+
Arg.NotNull( convention, nameof( convention ) );
105+
ControllerConventions.Add( convention );
106+
}
107+
108+
bool InternalApplyTo( Model model )
109+
{
110+
var key = model.ControllerType;
111+
var hasExplicitConventions = ControllerConventionBuilders.TryGetValue( key, out var builder );
112+
var applied = hasExplicitConventions;
113+
114+
if ( !hasExplicitConventions )
115+
{
116+
var hasNoImplicitConventions = ControllerConventions.Count == 0;
117+
118+
if ( hasNoImplicitConventions )
119+
{
120+
return false;
121+
}
122+
123+
builder = new ControllerApiVersionConventionBuilder( model.ControllerType );
124+
}
125+
126+
foreach ( var convention in ControllerConventions )
127+
{
128+
applied |= convention.Apply( builder, model );
129+
}
130+
131+
if ( applied )
132+
{
133+
builder.ApplyTo( model );
134+
}
135+
136+
return applied;
137+
}
138+
}
139+
}

src/Common/Versioning/Conventions/ControllerApiVersionConventionBuilder.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
1111
/// <summary>
1212
/// Represents a builder for API versions applied to a controller.
1313
/// </summary>
14-
public partial class ControllerApiVersionConventionBuilder : ControllerApiVersionConventionBuilderBase
14+
public partial class ControllerApiVersionConventionBuilder : ControllerApiVersionConventionBuilderBase, IControllerConventionBuilder
1515
{
1616
/// <summary>
1717
/// Initializes a new instance of the <see cref="ControllerApiVersionConventionBuilder"/> class.
@@ -123,5 +123,15 @@ public virtual ActionApiVersionConventionBuilder Action( MethodInfo actionMethod
123123
Contract.Ensures( Contract.Result<ActionApiVersionConventionBuilder>() != null );
124124
return ActionBuilders.GetOrAdd( actionMethod );
125125
}
126+
127+
void IControllerConventionBuilder.IsApiVersionNeutral() => IsApiVersionNeutral();
128+
129+
void IControllerConventionBuilder.HasApiVersion( ApiVersion apiVersion ) => HasApiVersion( apiVersion );
130+
131+
void IControllerConventionBuilder.HasDeprecatedApiVersion( ApiVersion apiVersion ) => HasDeprecatedApiVersion( apiVersion );
132+
133+
void IControllerConventionBuilder.AdvertisesApiVersion( ApiVersion apiVersion ) => AdvertisesApiVersion( apiVersion );
134+
135+
void IControllerConventionBuilder.AdvertisesDeprecatedApiVersion( ApiVersion apiVersion ) => AdvertisesDeprecatedApiVersion( apiVersion );
126136
}
127137
}

src/Common/Versioning/Conventions/ControllerApiVersionConventionBuilder{T}.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
1414
/// Represents a builder for API versions applied to a controller.
1515
/// </summary>
1616
#pragma warning disable SA1619 // Generic type parameters should be documented partial class; false positive
17-
public partial class ControllerApiVersionConventionBuilder<T> : ControllerApiVersionConventionBuilderBase
17+
public partial class ControllerApiVersionConventionBuilder<T> : ControllerApiVersionConventionBuilderBase, IControllerConventionBuilder
1818
#pragma warning restore SA1619
1919
{
2020
/// <summary>
@@ -108,5 +108,15 @@ public virtual ActionApiVersionConventionBuilder<T> Action( MethodInfo actionMet
108108
Contract.Ensures( Contract.Result<ActionApiVersionConventionBuilder<T>>() != null );
109109
return ActionBuilders.GetOrAdd( actionMethod );
110110
}
111+
112+
void IControllerConventionBuilder.IsApiVersionNeutral() => IsApiVersionNeutral();
113+
114+
void IControllerConventionBuilder.HasApiVersion( ApiVersion apiVersion ) => HasApiVersion( apiVersion );
115+
116+
void IControllerConventionBuilder.HasDeprecatedApiVersion( ApiVersion apiVersion ) => HasDeprecatedApiVersion( apiVersion );
117+
118+
void IControllerConventionBuilder.AdvertisesApiVersion( ApiVersion apiVersion ) => AdvertisesApiVersion( apiVersion );
119+
120+
void IControllerConventionBuilder.AdvertisesDeprecatedApiVersion( ApiVersion apiVersion ) => AdvertisesDeprecatedApiVersion( apiVersion );
111121
}
112122
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
9+
/// <summary>
10+
/// Defines the behavior of a convention builder for a controller.
11+
/// </summary>
12+
public partial interface IControllerConventionBuilder
13+
{
14+
/// <summary>
15+
/// Indicates that the controller is API version-neutral.
16+
/// </summary>
17+
void IsApiVersionNeutral();
18+
19+
/// <summary>
20+
/// Indicates that the specified API version is supported by the configured controller.
21+
/// </summary>
22+
/// <param name="apiVersion">The supported <see cref="ApiVersion">API version</see> implemented by the controller.</param>
23+
void HasApiVersion( ApiVersion apiVersion );
24+
25+
/// <summary>
26+
/// Indicates that the specified API version is deprecated by the configured controller.
27+
/// </summary>
28+
/// <param name="apiVersion">The deprecated <see cref="ApiVersion">API version</see> implemented by the controller.</param>
29+
void HasDeprecatedApiVersion( ApiVersion apiVersion );
30+
31+
/// <summary>
32+
/// Indicates that the specified API version is advertised by the configured controller.
33+
/// </summary>
34+
/// <param name="apiVersion">The advertised <see cref="ApiVersion">API version</see> not directly implemented by the controller.</param>
35+
void AdvertisesApiVersion( ApiVersion apiVersion );
36+
37+
/// <summary>
38+
/// Indicates that the specified API version is advertised and deprecated by the configured controller.
39+
/// </summary>
40+
/// <param name="apiVersion">The advertised, but deprecated <see cref="ApiVersion">API version</see> not directly implemented by the controller.</param>
41+
void AdvertisesDeprecatedApiVersion( ApiVersion apiVersion );
42+
}
43+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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.Generic;
9+
using System.Diagnostics.Contracts;
10+
using System.Text;
11+
using System.Text.RegularExpressions;
12+
using static System.Text.RegularExpressions.RegexOptions;
13+
14+
/// <summary>
15+
/// Represents a convention which applies an API to a controller by its defined namespace.
16+
/// </summary>
17+
public partial class VersionByNamespaceConvention
18+
{
19+
static string GetRawApiVersion( string @namespace )
20+
{
21+
Contract.Requires( !string.IsNullOrEmpty( @namespace ) );
22+
Contract.Ensures( Contract.Result<string>() != null );
23+
24+
// 'v' | 'V' : [<year> '-' <month> '-' <day>] : [<major[.minor]>] : [<status>]
25+
// ex: v2018_04_01_1_1_Beta
26+
const string Pattern = @"(?:^|\.)[vV](\d{4})?_?(\d{2})?_?(\d{2})?_?(\d+)?_?(\d*)_?([a-zA-Z][a-zA-Z0-9]*)?(?:$|\.)";
27+
28+
var match = Regex.Match( @namespace, Pattern, Singleline );
29+
var rawApiVersions = new List<string>();
30+
var text = new StringBuilder();
31+
32+
while ( match.Success )
33+
{
34+
ExtractDateParts( match, text );
35+
ExtractNumericParts( match, text );
36+
ExtractStatusPart( match, text );
37+
38+
if ( text.Length > 0 )
39+
{
40+
rawApiVersions.Add( text.ToString() );
41+
}
42+
43+
text.Clear();
44+
match = match.NextMatch();
45+
}
46+
47+
switch ( rawApiVersions.Count )
48+
{
49+
case 0:
50+
return default;
51+
case 1:
52+
return rawApiVersions[0];
53+
}
54+
55+
throw new InvalidOperationException( SR.MultipleApiVersionsInferredFromNamespaces.FormatInvariant( @namespace ) );
56+
}
57+
58+
static void ExtractDateParts( Match match, StringBuilder text )
59+
{
60+
Contract.Requires( match != null );
61+
Contract.Requires( text != null );
62+
63+
var year = match.Groups[1];
64+
var month = match.Groups[2];
65+
var day = match.Groups[3];
66+
67+
if ( !year.Success || !month.Success || !day.Success )
68+
{
69+
return;
70+
}
71+
72+
text.Append( year.Value );
73+
text.Append( '-' );
74+
text.Append( month.Value );
75+
text.Append( '-' );
76+
text.Append( day.Value );
77+
}
78+
79+
static void ExtractNumericParts( Match match, StringBuilder text )
80+
{
81+
Contract.Requires( match != null );
82+
Contract.Requires( text != null );
83+
84+
var major = match.Groups[4];
85+
86+
if ( !major.Success )
87+
{
88+
return;
89+
}
90+
91+
if ( text.Length > 0 )
92+
{
93+
text.Append( '.' );
94+
}
95+
96+
text.Append( major.Value );
97+
98+
var minor = match.Groups[5];
99+
100+
if ( !minor.Success )
101+
{
102+
return;
103+
}
104+
105+
text.Append( '.' );
106+
107+
if ( minor.Length > 0 )
108+
{
109+
text.Append( minor.Value );
110+
}
111+
else
112+
{
113+
text.Append( '0' );
114+
}
115+
}
116+
117+
static void ExtractStatusPart( Match match, StringBuilder text )
118+
{
119+
Contract.Requires( match != null );
120+
Contract.Requires( text != null );
121+
122+
var status = match.Groups[6];
123+
124+
if ( status.Success && text.Length > 0 )
125+
{
126+
text.Append( '-' );
127+
text.Append( status.Value );
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)