Skip to content

Commit 8a4a325

Browse files
Added support for detecting and handling ambiguously requested API versions. Resolves #12 (#14)
1 parent 9d52f61 commit 8a4a325

33 files changed

+819
-215
lines changed

ApiVersioning.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{261B77
3939
EndProject
4040
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Versioning", "Versioning", "{DE4EE45F-F8EA-4B32-B16F-441F946ACEF4}"
4141
ProjectSection(SolutionItems) = preProject
42+
src\Common\Versioning\AmbiguousApiVersionException.cs = src\Common\Versioning\AmbiguousApiVersionException.cs
4243
src\Common\Versioning\ApiVersioningOptions.cs = src\Common\Versioning\ApiVersioningOptions.cs
4344
src\Common\Versioning\ApiVersionModel.cs = src\Common\Versioning\ApiVersionModel.cs
4445
src\Common\Versioning\ApiVersionModelDebugView.cs = src\Common\Versioning\ApiVersionModelDebugView.cs

samples/webapi/BasicODataWebApiSample/BasicODataWebApiSample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
<AutoAssignPort>True</AutoAssignPort>
157157
<DevelopmentServerPort>4129</DevelopmentServerPort>
158158
<DevelopmentServerVPath>/</DevelopmentServerVPath>
159-
<IISUrl>http://localhost:25282/</IISUrl>
159+
<IISUrl>http://localhost:4129/</IISUrl>
160160
<NTLMAuthentication>False</NTLMAuthentication>
161161
<UseCustomServer>False</UseCustomServer>
162162
<CustomServerUrl>

src/Common/CollectionExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ namespace Microsoft.AspNetCore.Mvc
88
using System.Collections.Generic;
99
using System.Diagnostics.Contracts;
1010
using System.Linq;
11+
using Versioning;
12+
using static System.Globalization.CultureInfo;
13+
using static System.String;
1114

1215
internal static partial class CollectionExtensions
1316
{
@@ -58,5 +61,20 @@ internal static void AddRange<T>( this ICollection<T> collection, IEnumerable<T>
5861
collection.Add( item );
5962
}
6063
}
64+
65+
internal static string EnsureZeroOrOneApiVersions( this ICollection<string> apiVersions )
66+
{
67+
Contract.Requires( apiVersions != null );
68+
69+
if ( apiVersions.Count < 2 )
70+
{
71+
return apiVersions.SingleOrDefault();
72+
}
73+
74+
var requestedVersions = Join( ", ", apiVersions.OrderBy( v => v ) );
75+
var message = Format( InvariantCulture, SR.MultipleDifferentApiVersionsRequested, requestedVersions );
76+
77+
throw new AmbiguousApiVersionException( message, apiVersions.OrderBy( v => v ) );
78+
}
6179
}
6280
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#if WEBAPI
2+
namespace Microsoft.Web.Http.Versioning
3+
#else
4+
namespace Microsoft.AspNetCore.Mvc.Versioning
5+
#endif
6+
{
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
#if NET451 || WEBAPI
11+
using System.Runtime.Serialization;
12+
#endif
13+
14+
/// <summary>
15+
/// Represents the exception thrown when multiple, different API versions specified in a single request.
16+
/// </summary>
17+
#if NET451 || WEBAPI
18+
[Serializable]
19+
#endif
20+
public class AmbiguousApiVersionException : Exception
21+
{
22+
private readonly string[] apiVersions;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="AmbiguousApiVersionException"/> class.
26+
/// </summary>
27+
/// <param name="message">The associated error message.</param>
28+
/// <param name="apiVersions">The <see cref="IEnumerable{T}">sequence</see> of ambiguous API versions.</param>
29+
public AmbiguousApiVersionException( string message, IEnumerable<string> apiVersions )
30+
: base( message )
31+
{
32+
Arg.NotNull( apiVersions, nameof( apiVersions ) );
33+
this.apiVersions = apiVersions.ToArray();
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="AmbiguousApiVersionException"/> class.
38+
/// </summary>
39+
/// <param name="message">The associated error message.</param>
40+
/// <param name="apiVersions">The <see cref="IEnumerable{T}">sequence</see> of ambiguous API versions.</param>
41+
/// <param name="innerException">The inner <see cref="Exception">exception</see> that caused the current exception, if any.</param>
42+
public AmbiguousApiVersionException( string message, IEnumerable<string> apiVersions, Exception innerException )
43+
: base( message, innerException )
44+
{
45+
Arg.NotNull( apiVersions, nameof( apiVersions ) );
46+
this.apiVersions = apiVersions.ToArray();
47+
}
48+
49+
/// <summary>
50+
/// Gets a read-only list of the ambiguous API versions.
51+
/// </summary>
52+
/// <value>A <see cref="IReadOnlyList{T}">read-only list</see> of unparsed, ambiguous API versions.</value>
53+
public IReadOnlyList<string> ApiVersions => apiVersions;
54+
#if NET451 || WEBAPI
55+
/// <summary>
56+
/// Initializes a new instance of the <see cref="AmbiguousApiVersionException"/> class.
57+
/// </summary>
58+
/// <param name="info">The <see cref="SerializationInfo">serialization info</see> the exception is being deserialized with.</param>
59+
/// <param name="context">The <see cref="StreamingContext">streaming context</see> the exception is being deserialized from.</param>
60+
protected AmbiguousApiVersionException( SerializationInfo info, StreamingContext context )
61+
: base( info, context )
62+
{
63+
apiVersions = (string[]) info.GetValue( nameof( apiVersions ), typeof( string[] ) );
64+
}
65+
66+
/// <summary>
67+
/// Gets information about the exception being serialized.
68+
/// </summary>
69+
/// <param name="info">The <see cref="SerializationInfo">serialization info</see> the exception is being serialized with.</param>
70+
/// <param name="context">The <see cref="StreamingContext">streaming context</see> the exception is being serialized in.</param>
71+
public override void GetObjectData( SerializationInfo info, StreamingContext context )
72+
{
73+
base.GetObjectData( info, context );
74+
info.AddValue( nameof( apiVersions ), apiVersions );
75+
}
76+
#endif
77+
}
78+
}

src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedODataPathRouteConstraint.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
namespace Microsoft.Web.OData.Routing
22
{
33
using Http;
4+
using Http.Versioning;
5+
using Microsoft.OData.Core;
46
using Microsoft.OData.Edm;
57
using System.Collections.Generic;
68
using System.Diagnostics.CodeAnalysis;
@@ -11,6 +13,7 @@
1113
using System.Web.OData.Routing;
1214
using System.Web.OData.Routing.Conventions;
1315
using static Http.ApiVersion;
16+
using static System.Net.HttpStatusCode;
1417
using static System.StringSplitOptions;
1518
using static System.Web.Http.Routing.HttpRouteDirection;
1619

@@ -85,6 +88,22 @@ private static bool TryExtractApiVersionFromSegment( string segment, out ApiVers
8588
return TryParse( text, out apiVersion );
8689
}
8790

91+
private static ApiVersion ResolveApiVersion( HttpRequestMessage request, IHttpRoute route )
92+
{
93+
Contract.Requires( request != null );
94+
Contract.Requires( route != null );
95+
96+
try
97+
{
98+
return request.GetRequestedApiVersion() ?? GetApiVersionFromRoutePrefix( request, route );
99+
}
100+
catch ( AmbiguousApiVersionException ex )
101+
{
102+
var error = new ODataError() { ErrorCode = "AmbiguousApiVersion", Message = ex.Message };
103+
throw new HttpResponseException( request.CreateResponse( BadRequest, error ) );
104+
}
105+
}
106+
88107
/// <summary>
89108
/// Gets the API version matched by the current OData path route constraint.
90109
/// </summary>
@@ -111,7 +130,7 @@ public override bool Match( HttpRequestMessage request, IHttpRoute route, string
111130
return false;
112131
}
113132

114-
var requestedVersion = request.GetRequestedApiVersion() ?? GetApiVersionFromRoutePrefix( request, route );
133+
var requestedVersion = ResolveApiVersion( request, route );
115134

116135
if ( requestedVersion != null )
117136
{

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Web.Http.Dispatcher;
1414
using Versioning;
1515
using static Controllers.HttpControllerDescriptorComparer;
16+
using static System.Net.HttpStatusCode;
1617
using static System.StringComparer;
1718

1819
/// <summary>
@@ -68,6 +69,8 @@ public virtual HttpControllerDescriptor SelectController( HttpRequestMessage req
6869
Arg.NotNull( request, nameof( request ) );
6970
Contract.Ensures( Contract.Result<HttpControllerDescriptor>() != null );
7071

72+
EnsureRequestHasValidApiVersion( request );
73+
7174
var aggregator = new ApiVersionControllerAggregator( request, GetControllerName, controllerInfoCache );
7275
var conventionRouteSelector = new ConventionRouteControllerSelector( options, controllerTypeCache );
7376
var conventionRouteResult = default( ControllerSelectionResult );
@@ -119,10 +122,10 @@ public virtual string GetControllerName( HttpRequestMessage request )
119122
return null;
120123
}
121124

122-
string controller;
125+
object controller;
123126
routeData.Values.TryGetValue( RouteDataTokenKeys.Controller, out controller );
124127

125-
return controller;
128+
return (string) controller;
126129
}
127130

128131
private ConcurrentDictionary<string, HttpControllerDescriptorGroup> InitializeControllerInfoCache()
@@ -151,5 +154,20 @@ private ConcurrentDictionary<string, HttpControllerDescriptorGroup> InitializeCo
151154

152155
return mapping;
153156
}
157+
158+
private static void EnsureRequestHasValidApiVersion( HttpRequestMessage request )
159+
{
160+
Contract.Requires( request != null );
161+
162+
try
163+
{
164+
var apiVersion = request.GetRequestedApiVersion();
165+
}
166+
catch ( AmbiguousApiVersionException ex )
167+
{
168+
var error = new HttpError( ex.Message ) { ["Code"] = "AmbiguousApiVersion" };
169+
throw new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
170+
}
171+
}
154172
}
155173
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ private HttpResponseException CreateBadRequestForUnsupportedApiVersion( Controll
4949

5050
var message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
5151
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );
52+
var error = new HttpError() { Message = message, MessageDetail = messageDetail };
5253

54+
error["Code"] = "UnsupportedApiVersion";
5355
traceWriter.Info( request, ControllerSelectorCategory, message );
5456

55-
return new HttpResponseException( request.CreateErrorResponse( BadRequest, message, messageDetail ) );
57+
return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
5658
}
5759

5860
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
@@ -68,10 +70,12 @@ private HttpResponseException CreateBadRequestForInvalidApiVersion()
6870

6971
var message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
7072
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );
73+
var error = new HttpError() { Message = message, MessageDetail = messageDetail };
7174

75+
error["Code"] = "InvalidApiVersion";
7276
traceWriter.Info( request, ControllerSelectorCategory, message );
7377

74-
return new HttpResponseException( request.CreateErrorResponse( BadRequest, message, messageDetail ) );
78+
return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
7579
}
7680

7781
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]

0 commit comments

Comments
 (0)