Skip to content

Commit ed37dd4

Browse files
Validate ApiVersioningOptions.DefaultApiVersion != ApiVersion.Neutral. Resolves #1011
1 parent f25b079 commit ed37dd4

File tree

9 files changed

+144
-3
lines changed

9 files changed

+144
-3
lines changed

src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.resx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@
150150
<data name="DirectRoute_AmbiguousController" xml:space="preserve">
151151
<value>Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.{1}{1}The request has found the following matching controller types: {0}</value>
152152
</data>
153+
<data name="InvalidDefaultApiVersion" xml:space="preserve">
154+
<value>{0}.{1} is an invalid value for {2}.{3}. Did you mean to apply {4} via attribute or convention instead?</value>
155+
<comment>0 = ApiVersion
156+
1 = Neutral
157+
2 = ApiVersioningOptions
158+
3 = DefaultApiVersion
159+
4 = IApiVersionNeutral</comment>
160+
</data>
153161
<data name="NoControllerSelected" xml:space="preserve">
154162
<value>A controller was not selected for request URI '{0}' and API version '{1}'.</value>
155163
</data>

src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace System.Web.Http;
55
using Asp.Versioning;
66
using Asp.Versioning.Controllers;
77
using Asp.Versioning.Dispatcher;
8+
using System.Globalization;
89
using System.Web.Http.Controllers;
910
using System.Web.Http.Dispatcher;
1011
using static Asp.Versioning.ApiVersionParameterLocation;
@@ -65,6 +66,7 @@ public static void AddApiVersioning( this HttpConfiguration configuration, Actio
6566
var options = new ApiVersioningOptions();
6667

6768
setupAction( options );
69+
ValidateApiVersioningOptions( options );
6870
configuration.AddApiVersioning( options );
6971
}
7072

@@ -97,6 +99,30 @@ private static void AddApiVersioning( this HttpConfiguration configuration, ApiV
9799
SunsetPolicyManager.Default = new SunsetPolicyManager( options );
98100
}
99101

102+
// ApiVersion.Neutral does not have the same meaning as IApiVersionNeutral. setting
103+
// ApiVersioningOptions.DefaultApiVersion this value will not make all APIs version-neutral
104+
// and will likely lead to many unexpected side effects. this is a best-effort, one-time
105+
// validation check to help prevent people from going off the rails. if someone bypasses
106+
// this validation by removing the check or updating the value later, then caveat emptor.
107+
//
108+
// REF: https://github.com/dotnet/aspnet-api-versioning/issues/1011
109+
private static void ValidateApiVersioningOptions( ApiVersioningOptions options )
110+
{
111+
if ( options.DefaultApiVersion == ApiVersion.Neutral )
112+
{
113+
var message = string.Format(
114+
CultureInfo.CurrentCulture,
115+
SR.InvalidDefaultApiVersion,
116+
nameof( ApiVersion ),
117+
nameof( ApiVersion.Neutral ),
118+
nameof( ApiVersioningOptions ),
119+
nameof( ApiVersioningOptions.DefaultApiVersion ),
120+
nameof( IApiVersionNeutral ) );
121+
122+
throw new InvalidOperationException( message );
123+
}
124+
}
125+
100126
internal static IReportApiVersions GetApiVersionReporter( this HttpConfiguration configuration )
101127
{
102128
var options = configuration.GetApiVersioningOptions();

src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,17 @@ public void add_api_versioning_should_report_api_versions_when_option_is_enabled
3737
configuration.Services.GetActionSelector().Should().BeOfType<ApiVersionActionSelector>();
3838
configuration.Filters.Single().Instance.Should().BeOfType<ReportApiVersionsAttribute>();
3939
}
40+
41+
[Fact]
42+
public void add_api_versioning_should_not_allow_default_neutral_api_version()
43+
{
44+
// arrange
45+
var configuration = new HttpConfiguration();
46+
47+
// act
48+
Action options = () => configuration.AddApiVersioning( options => options.DefaultApiVersion = ApiVersion.Neutral );
49+
50+
// assert
51+
options.Should().Throw<InvalidOperationException>();
52+
}
4053
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ private static void AddApiVersioningServices( IServiceCollection services )
8585
}
8686

8787
services.TryAddSingleton<IApiVersionParser, ApiVersionParser>();
88-
services.Add( Singleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader ) );
89-
services.Add( Singleton( sp => (IApiVersionParameterSource) sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader ) );
90-
services.Add( Singleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionSelector ) );
88+
services.AddSingleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader );
89+
services.AddSingleton( sp => (IApiVersionParameterSource) sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader );
90+
services.AddSingleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionSelector );
9191
services.TryAddSingleton<IReportApiVersions, DefaultApiVersionReporter>();
9292
services.TryAddSingleton<ISunsetPolicyManager, SunsetPolicyManager>();
93+
services.TryAddEnumerable( Transient<IValidateOptions<ApiVersioningOptions>, ValidateApiVersioningOptions>() );
9394
services.TryAddEnumerable( Transient<IPostConfigureOptions<RouteOptions>, ApiVersioningRouteOptionsSetup>() );
9495
services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() );
9596
services.TryAddEnumerable( Singleton<IApiVersionMetadataCollationProvider, EndpointApiVersionMetadataCollationProvider>() );

src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@
129129
<data name="ConventionAddedAfterEndpointBuilt" xml:space="preserve">
130130
<value>Conventions cannot be added after building the endpoint.</value>
131131
</data>
132+
<data name="InvalidDefaultApiVersion" xml:space="preserve">
133+
<value>{0}.{1} is an invalid value for {2}.{3}. Did you mean to apply {4} via attribute or convention instead?</value>
134+
<comment>0 = ApiVersion
135+
1 = Neutral
136+
2 = ApiVersioningOptions
137+
3 = DefaultApiVersion
138+
4 = IApiVersionNeutral</comment>
139+
</data>
132140
<data name="MultipleVersionSets" xml:space="preserve">
133141
<value>An endpoint cannot apply multiple API version sets.</value>
134142
</data>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning;
4+
5+
using Microsoft.Extensions.Options;
6+
using System.Globalization;
7+
8+
// ApiVersion.Neutral does not have the same meaning as IApiVersionNeutral. setting
9+
// ApiVersioningOptions.DefaultApiVersion this value will not make all APIs version-neutral
10+
// and will likely lead to many unexpected side effects. this is a best-effort, one-time
11+
// validation check to help prevent people from going off the rails. if someone bypasses
12+
// this validation by removing the check or updating the value later, then caveat emptor.
13+
//
14+
// REF: https://github.com/dotnet/aspnet-api-versioning/issues/1011
15+
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
16+
internal sealed class ValidateApiVersioningOptions : IValidateOptions<ApiVersioningOptions>
17+
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
18+
{
19+
public ValidateOptionsResult Validate( string? name, ApiVersioningOptions options )
20+
{
21+
if ( name is not null && name != Options.DefaultName )
22+
{
23+
return ValidateOptionsResult.Skip;
24+
}
25+
26+
if ( options.DefaultApiVersion == ApiVersion.Neutral )
27+
{
28+
var message = string.Format(
29+
CultureInfo.CurrentCulture,
30+
SR.InvalidDefaultApiVersion,
31+
nameof( ApiVersion ),
32+
nameof( ApiVersion.Neutral ),
33+
nameof( ApiVersioningOptions ),
34+
nameof( ApiVersioningOptions.DefaultApiVersion ),
35+
nameof( IApiVersionNeutral ) );
36+
return ValidateOptionsResult.Fail( message );
37+
}
38+
39+
return ValidateOptionsResult.Success;
40+
}
41+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Microsoft.Extensions.DependencyInjection;
4+
5+
using Asp.Versioning;
6+
using Microsoft.Extensions.Options;
7+
8+
public class IServiceCollectionExtensionsTest
9+
{
10+
[Fact]
11+
public void add_api_versioning_should_not_allow_default_neutral_api_version()
12+
{
13+
// arrange
14+
var services = new ServiceCollection();
15+
16+
services.AddApiVersioning( options => options.DefaultApiVersion = ApiVersion.Neutral );
17+
18+
var provider = services.BuildServiceProvider();
19+
20+
// act
21+
Func<ApiVersioningOptions> options = () => provider.GetRequiredService<IOptions<ApiVersioningOptions>>().Value;
22+
23+
// assert
24+
options.Should().Throw<OptionsValidationException>();
25+
}
26+
}

0 commit comments

Comments
 (0)