Skip to content

Commit ac5dd2c

Browse files
Chris Martinezcommonsensesoftware
authored andcommitted
Fix handling OData route prefix in URL building, template parsing, and URL segment substitution. Fixes #365
1 parent ecc9ba6 commit ac5dd2c

File tree

14 files changed

+375
-193
lines changed

14 files changed

+375
-193
lines changed

src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ void AppendRoutePrefix( IList<string> segments )
8080
return;
8181
}
8282

83-
prefix = UpdateRoutePrefixAndRemoveApiVersionParameterIfNecessary( prefix );
83+
prefix = RemoveRouteConstraints( prefix );
8484
segments.Add( prefix );
8585
}
8686

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,9 @@
11
namespace Microsoft.AspNet.OData.Routing
22
{
3-
using Microsoft.Web.Http.Description;
4-
using System.Diagnostics.Contracts;
5-
using System.Linq;
6-
using static System.Globalization.CultureInfo;
3+
using System;
74

85
partial class ODataRouteBuilder
96
{
10-
string UpdateRoutePrefixAndRemoveApiVersionParameterIfNecessary( string routePrefix )
11-
{
12-
Contract.Requires( !string.IsNullOrEmpty( routePrefix ) );
13-
Contract.Ensures( !string.IsNullOrEmpty( Contract.Result<string>() ) );
14-
15-
var parameters = Context.ParameterDescriptions;
16-
var parameter = parameters.FirstOrDefault( p => p.ParameterDescriptor is ApiVersionParameterDescriptor pd && pd.FromPath );
17-
18-
if ( parameter == null )
19-
{
20-
return routePrefix;
21-
}
22-
23-
var apiVersionFormat = Context.Options.SubstitutionFormat;
24-
var token = string.Concat( '{', parameter.Name, '}' );
25-
var value = Context.ApiVersion.ToString( apiVersionFormat, InvariantCulture );
26-
var newRoutePrefix = routePrefix.Replace( token, value );
27-
28-
if ( routePrefix == newRoutePrefix )
29-
{
30-
return routePrefix;
31-
}
32-
33-
parameters.Remove( parameter );
34-
return newRoutePrefix;
35-
}
7+
static string RemoveRouteConstraints( string routePrefix ) => routePrefix;
368
}
379
}

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Microsoft.Web.Http.Description
22
{
33
using Microsoft.Web.Http.Versioning;
4+
using System;
45
using System.Collections.Generic;
56
using System.Diagnostics.Contracts;
67
using System.Linq;
@@ -18,6 +19,7 @@
1819
public class ApiVersionParameterDescriptionContext : IApiVersionParameterDescriptionContext
1920
{
2021
readonly List<ApiParameterDescription> parameters = new List<ApiParameterDescription>( 1 );
22+
readonly Lazy<bool> versionNeutral;
2123
bool optional;
2224

2325
/// <summary>
@@ -39,6 +41,7 @@ public ApiVersionParameterDescriptionContext(
3941
ApiVersion = apiVersion;
4042
Options = options;
4143
optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion;
44+
versionNeutral = new Lazy<bool>( TestIfApiVersionNeutral );
4245
}
4346

4447
/// <summary>
@@ -53,6 +56,12 @@ public ApiVersionParameterDescriptionContext(
5356
/// <value>The associated <see cref="ApiVersion">API version</see>.</value>
5457
protected ApiVersion ApiVersion { get; }
5558

59+
/// <summary>
60+
/// Gets a value indicating whether the current API is version-neutral.
61+
/// </summary>
62+
/// <value>True if the current API is version-neutral; otherwise, false.</value>
63+
protected bool IsApiVersionNeutral => versionNeutral.Value;
64+
5665
/// <summary>
5766
/// Gets the options associated with the API explorer.
5867
/// </summary>
@@ -78,20 +87,29 @@ bool HasPathParameter
7887
/// <param name="location">One of the <see cref="ApiVersionParameterLocation"/> values.</param>
7988
public virtual void AddParameter( string name, ApiVersionParameterLocation location )
8089
{
90+
var add = default( Action<string> );
91+
8192
switch ( location )
8293
{
8394
case Query:
84-
AddQueryString( name );
95+
add = AddQueryString;
8596
break;
8697
case Header:
87-
AddHeader( name );
98+
add = AddHeader;
8899
break;
89100
case Path:
90101
UpdateUrlSegment();
91-
break;
102+
return;
92103
case MediaTypeParameter:
93-
AddMediaTypeParameter( name );
104+
add = AddMediaTypeParameter;
94105
break;
106+
default:
107+
return;
108+
}
109+
110+
if ( Options.AddApiVersionParametersWhenVersionNeutral || !IsApiVersionNeutral )
111+
{
112+
add( name );
95113
}
96114
}
97115

@@ -188,6 +206,14 @@ ApiParameterDescription NewApiVersionParameter( string name, ApiParameterSource
188206
return parameter;
189207
}
190208

209+
bool TestIfApiVersionNeutral()
210+
{
211+
var action = ApiDescription.ActionDescriptor;
212+
var model = action.GetApiVersionModel();
213+
214+
return model.IsApiVersionNeutral || ( model.DeclaredApiVersions.Count == 0 && action.ControllerDescriptor.IsApiVersionNeutral() );
215+
}
216+
191217
void RemoveAllParametersExcept( ApiParameterDescription parameter )
192218
{
193219
// note: in a scenario where multiple api version parameters are allowed, we can remove all other parameters because

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

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions()
271271

272272
if ( Options.SubstituteApiVersionInUrl )
273273
{
274-
UpdateRelativePathAndRemoveApiVersionParameterIfNecessary( apiDescriptionGroup, Options.SubstitutionFormat );
274+
foreach ( var apiDescription in apiDescriptionGroup.ApiDescriptions )
275+
{
276+
apiDescription.TryUpdateRelativePathAndRemoveApiVersionParameter( Options );
277+
}
275278
}
276279
}
277280

@@ -394,32 +397,6 @@ protected virtual bool TryExpandUriParameters( IHttpRoute route, IParsedRoute pa
394397
return true;
395398
}
396399

397-
static void UpdateRelativePathAndRemoveApiVersionParameterIfNecessary( ApiDescriptionGroup apiDescriptionGroup, string apiVersionFormat )
398-
{
399-
Contract.Requires( apiDescriptionGroup != null );
400-
401-
foreach ( var apiDescription in apiDescriptionGroup.ApiDescriptions )
402-
{
403-
var parameter = apiDescription.ParameterDescriptions.FirstOrDefault( p => p.ParameterDescriptor is ApiVersionParameterDescriptor pd && pd.FromPath );
404-
405-
if ( parameter == null )
406-
{
407-
continue;
408-
}
409-
410-
var relativePath = apiDescription.RelativePath;
411-
var token = '{' + parameter.ParameterDescriptor.ParameterName + '}';
412-
var value = apiDescription.ApiVersion.ToString( apiVersionFormat, InvariantCulture );
413-
var newRelativePath = relativePath.Replace( token, value );
414-
415-
if ( relativePath != newRelativePath )
416-
{
417-
apiDescription.RelativePath = newRelativePath;
418-
apiDescription.ParameterDescriptions.Remove( parameter );
419-
}
420-
}
421-
}
422-
423400
static IEnumerable<IHttpRoute> FlattenRoutes( IEnumerable<IHttpRoute> routes )
424401
{
425402
Contract.Requires( routes != null );
@@ -691,14 +668,6 @@ protected virtual void PopulateApiVersionParameters( ApiDescription apiDescripti
691668
Arg.NotNull( apiDescription, nameof( apiDescription ) );
692669
Arg.NotNull( apiVersion, nameof( apiVersion ) );
693670

694-
var action = apiDescription.ActionDescriptor;
695-
var model = action.GetApiVersionModel();
696-
697-
if ( model.IsApiVersionNeutral || ( model.DeclaredApiVersions.Count == 0 && action.ControllerDescriptor.IsApiVersionNeutral() ) )
698-
{
699-
return;
700-
}
701-
702671
var parameterSource = Options.ApiVersionParameterSource;
703672
var context = new ApiVersionParameterDescriptionContext( apiDescription, apiVersion, Options );
704673

src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using Microsoft;
44
using Microsoft.Web.Http.Description;
55
using System.Diagnostics.Contracts;
6+
using System.Linq;
7+
using static System.Globalization.CultureInfo;
68

79
/// <summary>
810
/// Provides extension methods for the <see cref="ApiDescription"/> class.
@@ -49,6 +51,44 @@ public static string GetUniqueID( this ApiDescription apiDescription )
4951
return apiDescription.ID;
5052
}
5153

54+
/// <summary>
55+
/// Attempts to update the relate path of the specified API description and remove the corresponding parameter according to the specified options.
56+
/// </summary>
57+
/// <param name="apiDescription">The <see cref="ApiDescription">API description</see> to attempt to update.</param>
58+
/// <param name="options">The current <see cref="ApiExplorerOptions">API Explorer options</see>.</param>
59+
/// <returns>True if the <paramref name="apiDescription">API description</paramref> was updated; otherwise, false.</returns>
60+
public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDescription apiDescription, ApiExplorerOptions options )
61+
{
62+
Arg.NotNull( apiDescription, nameof( apiDescription ) );
63+
Arg.NotNull( options, nameof( options ) );
64+
65+
if ( !options.SubstituteApiVersionInUrl || !( apiDescription is VersionedApiDescription versionedApiDescription ) )
66+
{
67+
return false;
68+
}
69+
70+
var parameter = versionedApiDescription.ParameterDescriptions.FirstOrDefault( p => p.ParameterDescriptor is ApiVersionParameterDescriptor pd && pd.FromPath );
71+
72+
if ( parameter == null )
73+
{
74+
return false;
75+
}
76+
77+
var relativePath = apiDescription.RelativePath;
78+
var token = '{' + parameter.ParameterDescriptor.ParameterName + '}';
79+
var value = versionedApiDescription.ApiVersion.ToString( options.SubstitutionFormat, InvariantCulture );
80+
var newRelativePath = relativePath.Replace( token, value );
81+
82+
if ( relativePath == newRelativePath )
83+
{
84+
return false;
85+
}
86+
87+
apiDescription.RelativePath = newRelativePath;
88+
apiDescription.ParameterDescriptions.Remove( parameter );
89+
return true;
90+
}
91+
5292
/// <summary>
5393
/// Gets a property of the specified type from the API description.
5494
/// </summary>
@@ -59,12 +99,12 @@ public static T GetProperty<T>( this VersionedApiDescription apiDescription )
5999
{
60100
Arg.NotNull( apiDescription, nameof( apiDescription ) );
61101

62-
if ( apiDescription.Properties.TryGetValue( typeof( T ), out object value ) )
102+
if ( apiDescription.Properties.TryGetValue( typeof( T ), out var value ) )
63103
{
64104
return (T) value;
65105
}
66106

67-
return default( T );
107+
return default;
68108
}
69109

70110
/// <summary>

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
using System;
44
using System.ComponentModel;
55
using System.Diagnostics.Contracts;
6+
using System.Linq;
7+
using static Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource;
68
using static System.ComponentModel.EditorBrowsableState;
9+
using static System.Globalization.CultureInfo;
10+
using static System.Linq.Enumerable;
711

812
/// <summary>
913
/// Provides extension methods for the <see cref="ApiDescription"/> class.
@@ -26,6 +30,44 @@ public static class ApiDescriptionExtensions
2630
[EditorBrowsable( Never )]
2731
public static void SetApiVersion( this ApiDescription apiDescription, ApiVersion apiVersion ) => apiDescription.SetProperty( apiVersion );
2832

33+
/// <summary>
34+
/// Attempts to update the relate path of the specified API description and remove the corresponding parameter according to the specified options.
35+
/// </summary>
36+
/// <param name="apiDescription">The <see cref="ApiDescription">API description</see> to attempt to update.</param>
37+
/// <param name="options">The current <see cref="ApiExplorerOptions">API Explorer options</see>.</param>
38+
/// <returns>True if the <paramref name="apiDescription">API description</paramref> was updated; otherwise, false.</returns>
39+
public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDescription apiDescription, ApiExplorerOptions options )
40+
{
41+
Arg.NotNull( apiDescription, nameof( apiDescription ) );
42+
Arg.NotNull( options, nameof( options ) );
43+
44+
if ( !options.SubstituteApiVersionInUrl )
45+
{
46+
return false;
47+
}
48+
49+
var parameter = apiDescription.ParameterDescriptions.FirstOrDefault( pd => pd.Source == Path && pd.ModelMetadata?.DataTypeName == nameof( ApiVersion ) );
50+
51+
if ( parameter == null )
52+
{
53+
return false;
54+
}
55+
56+
var relativePath = apiDescription.RelativePath;
57+
var token = '{' + parameter.Name + '}';
58+
var value = apiDescription.GetApiVersion().ToString( options.SubstitutionFormat, InvariantCulture );
59+
var newRelativePath = relativePath.Replace( token, value );
60+
61+
if ( relativePath == newRelativePath )
62+
{
63+
return false;
64+
}
65+
66+
apiDescription.RelativePath = newRelativePath;
67+
apiDescription.ParameterDescriptions.Remove( parameter );
68+
return true;
69+
}
70+
2971
/// <summary>
3072
/// Creates a shallow copy of the current API description.
3173
/// </summary>

0 commit comments

Comments
 (0)