Skip to content

Commit 7478f65

Browse files
Add option to define the controller naming convention. Fixes #734
1 parent bfa7eb3 commit 7478f65

21 files changed

+557
-86
lines changed

src/Common/Common.projitems

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,20 @@
4343
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ControllerApiVersionConventionBuilder.cs" />
4444
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ControllerApiVersionConventionBuilder{T}.cs" />
4545
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ControllerConventionBuilderExtensions.cs" />
46+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ControllerNameConvention.cs" />
4647
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ExpressionExtensions.cs" />
48+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\GroupedControllerNameConvention.cs" />
4749
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IActionConventionBuilder.cs" />
4850
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IActionConventionBuilder{T}.cs" />
4951
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\ApiVersionConventionBuilderExtensions.cs" />
5052
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IApiVersionConventionBuilder.cs" />
5153
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IApiVersionConvention{T}.cs" />
54+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IControllerNameConvention.cs" />
5255
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IDeclareApiVersionConventionBuilder.cs" />
5356
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IControllerConventionBuilder.cs" />
5457
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IControllerConventionBuilder{T}.cs" />
5558
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\IMapToApiVersionConventionBuilder.cs" />
59+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\OriginalControllerNameConvention.cs" />
5660
<Compile Include="$(MSBuildThisFileDirectory)Versioning\Conventions\VersionByNamespaceConvention.cs" />
5761
<Compile Include="$(MSBuildThisFileDirectory)Versioning\CurrentImplementationApiVersionSelector.cs" />
5862
<Compile Include="$(MSBuildThisFileDirectory)Versioning\DefaultApiVersionReporter.cs" />

src/Common/Versioning/ApiVersioningOptions.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
1414
using System;
1515
#if WEBAPI
1616
using static Microsoft.Web.Http.Versioning.ApiVersionReader;
17+
using NamingConvention = Microsoft.Web.Http.Versioning.Conventions.ControllerNameConvention;
1718
#else
1819
using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionReader;
20+
using NamingConvention = Microsoft.AspNetCore.Mvc.Versioning.Conventions.ControllerNameConvention;
1921
#endif
2022

2123
/// <summary>
@@ -24,11 +26,10 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
2426
public partial class ApiVersioningOptions
2527
{
2628
IApiVersionReader? apiVersionReader;
27-
28-
/// <summary>
29-
/// Initializes a new instance of the <see cref="ApiVersioningOptions"/> class.
30-
/// </summary>
31-
public ApiVersioningOptions() => ApiVersionSelector = new DefaultApiVersionSelector( this );
29+
IApiVersionSelector? apiVersionSelector;
30+
IApiVersionConventionBuilder? conventions;
31+
IErrorResponseProvider? errorResponses;
32+
IControllerNameConvention? controllerNameConvention;
3233

3334
/// <summary>
3435
/// Gets or sets the name associated with the API version route constraint.
@@ -105,7 +106,11 @@ public IApiVersionReader ApiVersionReader
105106
#if !WEBAPI
106107
[CLSCompliant( false )]
107108
#endif
108-
public IApiVersionSelector ApiVersionSelector { get; set; }
109+
public IApiVersionSelector ApiVersionSelector
110+
{
111+
get => apiVersionSelector ??= new DefaultApiVersionSelector( this );
112+
set => apiVersionSelector = value;
113+
}
109114

110115
/// <summary>
111116
/// Gets or sets the builder used to define API version conventions.
@@ -114,7 +119,11 @@ public IApiVersionReader ApiVersionReader
114119
#if !WEBAPI
115120
[CLSCompliant( false )]
116121
#endif
117-
public IApiVersionConventionBuilder Conventions { get; set; } = new ApiVersionConventionBuilder();
122+
public IApiVersionConventionBuilder Conventions
123+
{
124+
get => conventions ??= new ApiVersionConventionBuilder();
125+
set => conventions = value;
126+
}
118127

119128
/// <summary>
120129
/// Gets or sets the object used to generate HTTP error responses related to API versioning.
@@ -124,6 +133,21 @@ public IApiVersionReader ApiVersionReader
124133
#if !WEBAPI
125134
[CLSCompliant( false )]
126135
#endif
127-
public IErrorResponseProvider ErrorResponses { get; set; } = new DefaultErrorResponseProvider();
136+
public IErrorResponseProvider ErrorResponses
137+
{
138+
get => errorResponses ??= new DefaultErrorResponseProvider();
139+
set => errorResponses = value;
140+
}
141+
142+
/// <summary>
143+
/// Gets or sets the naming convention applied to controllers.
144+
/// </summary>
145+
/// <value>The <see cref="IControllerNameConvention">naming convention</see> applied to controllers. The default
146+
/// value is <see cref="NamingConvention.Default"/>.</value>
147+
public IControllerNameConvention ControllerNameConvention
148+
{
149+
get => controllerNameConvention ??= NamingConvention.Default;
150+
set => controllerNameConvention = value;
151+
}
128152
}
129153
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning.Conventions
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions
5+
#endif
6+
{
7+
/// <summary>
8+
/// Represents the base implementation for a <see cref="IControllerNameConvention">controller name convention</see>.
9+
/// </summary>
10+
public abstract class ControllerNameConvention : IControllerNameConvention
11+
{
12+
static IControllerNameConvention? @default;
13+
static IControllerNameConvention? original;
14+
static IControllerNameConvention? grouped;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ControllerNameConvention"/> class.
18+
/// </summary>
19+
protected ControllerNameConvention() { }
20+
21+
/// <inheritdoc />
22+
public abstract string NormalizeName( string controllerName );
23+
24+
/// <inheritdoc />
25+
public abstract string GroupName( string controllerName );
26+
27+
/// <summary>
28+
/// Gets the default controller name convention.
29+
/// </summary>
30+
/// <value>The default <see cref="IControllerNameConvention">controller name convention</see>.</value>
31+
/// <remarks>This convention will strip the <b>Controller</b> suffix as well as any trailing numeric values.</remarks>
32+
public static IControllerNameConvention Default => @default ??= new DefaultControllerNameConvention();
33+
34+
/// <summary>
35+
/// Gets the original controller name convention.
36+
/// </summary>
37+
/// <value>The original <see cref="IControllerNameConvention">controller name convention</see>.</value>
38+
/// <remarks>This convention will apply the original convention which only strips the <b>Controller</b> suffix.</remarks>
39+
public static IControllerNameConvention Original => original ??= new OriginalControllerNameConvention();
40+
41+
/// <summary>
42+
/// Gets the grouped controller name convention.
43+
/// </summary>
44+
/// <value>The grouped <see cref="IControllerNameConvention">controller name convention</see>.</value>
45+
/// <remarks>This convention will apply the original convention which strips the <b>Controller</b> suffix from the
46+
/// controller name. Any trailing numbers will also be stripped from controller name, but only for the purposes
47+
/// of grouping.</remarks>
48+
public static IControllerNameConvention Grouped => grouped ??= new GroupedControllerNameConvention();
49+
50+
/// <summary>
51+
/// Trims any trailing numeric characters from the specified name.
52+
/// </summary>
53+
/// <param name="name">The name to trim any trailing numbers from.</param>
54+
/// <returns>The <paramref name="name"/> with any trailing numbers from its suffix.</returns>
55+
public static string TrimTrailingNumbers( string name )
56+
{
57+
if ( string.IsNullOrEmpty( name ) )
58+
{
59+
return string.Empty;
60+
}
61+
62+
var last = name.Length - 1;
63+
64+
for ( var i = last; i >= 0; i-- )
65+
{
66+
if ( !char.IsNumber( name[i] ) )
67+
{
68+
if ( i < last )
69+
{
70+
return name.Substring( 0, i + 1 );
71+
}
72+
73+
return name;
74+
}
75+
}
76+
77+
return name;
78+
}
79+
}
80+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 static ControllerNameConvention;
9+
10+
/// <summary>
11+
/// Represents the grouped <see cref="IControllerNameConvention">controller name convention</see>.
12+
/// </summary>
13+
/// <remarks>This convention will apply the original convention which strips the <b>Controller</b> suffix from the
14+
/// controller name. Any trailing numbers will also be stripped from controller name, but only for the purposes
15+
/// of grouping.</remarks>
16+
public class GroupedControllerNameConvention : OriginalControllerNameConvention
17+
{
18+
/// <inheritdoc />
19+
public override string GroupName( string controllerName ) => TrimTrailingNumbers( controllerName );
20+
}
21+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 for controller names.
11+
/// </summary>
12+
public interface IControllerNameConvention
13+
{
14+
/// <summary>
15+
/// Normalizes the specified controller name.
16+
/// </summary>
17+
/// <param name="controllerName">The name of the controller.</param>
18+
/// <returns>The normalized name of the specified controller.</returns>
19+
string NormalizeName( string controllerName );
20+
21+
/// <summary>
22+
/// Gets the name used for grouping the specified controller.
23+
/// </summary>
24+
/// <param name="controllerName">The name of the controller.</param>
25+
/// <returns>The group name of the specified controller.</returns>
26+
string GroupName( string controllerName );
27+
}
28+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
/// Represents the original <see cref="IControllerNameConvention">controller name convention</see>.
11+
/// </summary>
12+
/// <remarks>This convention will apply the original convention which only strips the <b>Controller</b> suffix.</remarks>
13+
public partial class OriginalControllerNameConvention : IControllerNameConvention
14+
{
15+
/// <inheritdoc />
16+
public virtual string GroupName( string controllerName ) => controllerName;
17+
}
18+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
/// </summary>
2727
public class VersionedApiExplorer : IApiExplorer
2828
{
29-
static readonly Regex actionVariableRegex = new Regex( $"{{{RouteValueKeys.Action}}}", Compiled | IgnoreCase | CultureInvariant );
30-
static readonly Regex controllerVariableRegex = new Regex( $"{{{RouteValueKeys.Controller}}}", Compiled | IgnoreCase | CultureInvariant );
29+
static readonly Regex actionVariableRegex = new( $"{{{RouteValueKeys.Action}}}", Compiled | IgnoreCase | CultureInvariant );
30+
static readonly Regex controllerVariableRegex = new( $"{{{RouteValueKeys.Controller}}}", Compiled | IgnoreCase | CultureInvariant );
3131
readonly Lazy<ApiDescriptionGroupCollection> apiDescriptions;
3232
IDocumentationProvider? documentationProvider;
3333

src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpControllerTypeCache.cs

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
namespace Microsoft.Web.Http.Dispatcher
22
{
3+
using Microsoft.Web.Http.Versioning.Conventions;
34
using System;
45
using System.Collections.Generic;
56
using System.Linq;
67
using System.Reflection;
78
using System.Web.Http;
8-
using static System.Web.Http.Dispatcher.DefaultHttpControllerSelector;
99

1010
sealed class HttpControllerTypeCache
1111
{
@@ -18,62 +18,25 @@ internal HttpControllerTypeCache( HttpConfiguration configuration )
1818
cache = new Lazy<Dictionary<string, ILookup<string, Type>>>( InitializeCache );
1919
}
2020

21-
static string GetControllerName( Type type )
21+
static string GetControllerName( Type type, IControllerNameConvention convention )
2222
{
23-
// allow authors to specify a controller name via an attribute. this is required for controllers that
24-
// do not use attribute-based routing, but support versioning. in the pure Convention-Over-Configuration
25-
// model, this is not otherwise possible because each controller type maps to a different route
26-
var attribute = type.GetCustomAttributes<ControllerNameAttribute>( false ).SingleOrDefault();
23+
var name = type.GetCustomAttribute<ControllerNameAttribute>( false ) is ControllerNameAttribute attribute ?
24+
attribute.Name :
25+
type.Name;
2726

28-
if ( attribute != null )
29-
{
30-
return attribute.Name;
31-
}
32-
33-
// use standard convention for the controller name (ex: ValuesController -> Values)
34-
var name = type.Name;
35-
var suffixLength = ControllerSuffix.Length;
36-
37-
name = name.Substring( 0, name.Length - suffixLength );
38-
39-
// trim trailing numbers to enable grouping by convention (ex: Values1Controller -> Values, Values2Controller -> Values)
40-
return TrimTrailingNumbers( name );
41-
}
42-
43-
static string TrimTrailingNumbers( string name )
44-
{
45-
if ( string.IsNullOrEmpty( name ) )
46-
{
47-
return name;
48-
}
49-
50-
var last = name.Length - 1;
51-
52-
for ( var i = last; i >= 0; i-- )
53-
{
54-
if ( !char.IsNumber( name[i] ) )
55-
{
56-
if ( i < last )
57-
{
58-
return name.Substring( 0, i + 1 );
59-
}
60-
61-
return name;
62-
}
63-
}
64-
65-
return name;
27+
return convention.GroupName( convention.NormalizeName( name ) );
6628
}
6729

6830
Dictionary<string, ILookup<string, Type>> InitializeCache()
6931
{
7032
var services = configuration.Services;
7133
var assembliesResolver = services.GetAssembliesResolver();
7234
var typeResolver = services.GetHttpControllerTypeResolver();
35+
var convention = configuration.GetApiVersioningOptions().ControllerNameConvention;
7336
var comparer = StringComparer.OrdinalIgnoreCase;
7437

7538
return typeResolver.GetControllerTypes( assembliesResolver )
76-
.GroupBy( GetControllerName, comparer )
39+
.GroupBy( type => GetControllerName( type, convention ), comparer )
7740
.ToDictionary( g => g.Key, g => g.ToLookup( t => t.Namespace ?? string.Empty, comparer ), comparer );
7841
}
7942

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Microsoft.Web.Http.Versioning.Conventions
2+
{
3+
using System;
4+
using static ControllerNameConvention;
5+
6+
/// <summary>
7+
/// Represents the default <see cref="IControllerNameConvention">controller name convention</see>.
8+
/// </summary>
9+
/// <remarks>This convention will strip the <b>Controller</b> suffix as well as any trailing numeric values.</remarks>
10+
public class DefaultControllerNameConvention : OriginalControllerNameConvention
11+
{
12+
/// <inheritdoc />
13+
public override string NormalizeName( string controllerName )
14+
{
15+
if ( string.IsNullOrEmpty( controllerName ) )
16+
{
17+
return string.Empty;
18+
}
19+
20+
var name = base.NormalizeName( controllerName );
21+
22+
if ( name.Length == controllerName.Length )
23+
{
24+
return controllerName;
25+
}
26+
27+
return TrimTrailingNumbers( name );
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)