Skip to content

Commit 154c489

Browse files
User-defined and out-of-the-box OData controllers now return 400 instead of 404 when a route could match, but doesn't for a specific API version. Fixes #17 (#21)
1 parent 51b7148 commit 154c489

File tree

14 files changed

+362
-234
lines changed

14 files changed

+362
-234
lines changed

src/Microsoft.AspNet.OData.Versioning/System.Web.OData/HttpConfigurationExtensions.cs renamed to src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs

+89-15
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,27 @@
88
using Microsoft;
99
using Microsoft.OData.Edm;
1010
using Microsoft.Web.Http;
11+
using Microsoft.Web.Http.Routing;
1112
using Microsoft.Web.OData.Builder;
1213
using Microsoft.Web.OData.Routing;
1314
using OData.Batch;
1415
using OData.Extensions;
1516
using OData.Routing;
1617
using OData.Routing.Conventions;
18+
using Routing;
1719
using static Linq.Expressions.Expression;
20+
using static System.String;
21+
using static System.StringComparison;
1822

1923
/// <summary>
2024
/// Provides extension methods for the <see cref="HttpConfiguration"/> class.
2125
/// </summary>
2226
public static class HttpConfigurationExtensions
2327
{
2428
private const string ResolverSettingsKey = "System.Web.OData.ResolverSettingsKey";
29+
private const string UnversionedRouteSuffix = "-Unversioned";
30+
private const string ApiVersionConstraintName = "apiVersion";
31+
private const string ApiVersionConstraint = "{" + ApiVersionConstraintName + "}";
2532
private static readonly Lazy<Action<DefaultODataPathHandler, object>> setResolverSettings = new Lazy<Action<DefaultODataPathHandler, object>>( GetResolverSettingsMutator );
2633

2734
private static Action<DefaultODataPathHandler, object> GetResolverSettingsMutator()
@@ -192,46 +199,42 @@ public static IReadOnlyList<ODataRoute> MapVersionedODataRoutes(
192199
var routeConventions = EnsureConventions( routingConventions.ToList() );
193200
var routes = configuration.Routes;
194201

195-
if ( !string.IsNullOrEmpty( routePrefix ) )
202+
if ( !IsNullOrEmpty( routePrefix ) )
196203
{
197204
routePrefix = routePrefix.TrimEnd( '/' );
198205
}
199206

200207
if ( batchHandler != null )
201208
{
202-
var batchTemplate = string.IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch;
209+
var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch;
203210
routes.MapHttpBatchRoute( routeName + "Batch", batchTemplate, batchHandler );
204211
}
205212

206213
configuration.SetResolverSettings( pathHandler );
207214
routeConventions.Insert( 0, null );
208215

209216
var odataRoutes = new List<ODataRoute>();
217+
var unversionedConstraints = new List<IHttpRouteConstraint>();
210218

211219
foreach ( var model in models )
212220
{
213221
var versionedRouteName = routeName;
214-
var apiVersion = model.GetAnnotationValue<ApiVersionAnnotation>( model )?.ApiVersion;
215222
var routeConstraint = default( ODataPathRouteConstraint );
216223

217224
routeConventions[0] = new VersionedAttributeRoutingConvention( model, configuration );
218-
219-
if ( apiVersion == null )
220-
{
221-
routeConstraint = new ODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray() );
222-
}
223-
else
224-
{
225-
versionedRouteName += "-" + apiVersion.ToString();
226-
routeConstraint = new VersionedODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray(), apiVersion );
227-
}
225+
routeConstraint = new ODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray() );
226+
unversionedConstraints.Add( routeConstraint );
227+
routeConstraint = MakeVersionedODataRouteConstraint( routeConstraint, pathHandler, routeConventions, model, ref versionedRouteName );
228228

229229
var route = new ODataRoute( routePrefix, routeConstraint );
230230

231+
AddApiVersionConstraintIfNecessary( route );
231232
routes.Add( versionedRouteName, route );
232233
odataRoutes.Add( route );
233234
}
234235

236+
AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, routes, odataRoutes, unversionedConstraints );
237+
235238
return odataRoutes;
236239
}
237240

@@ -329,14 +332,14 @@ public static ODataRoute MapVersionedODataRoute(
329332
var routeConventions = EnsureConventions( routingConventions.ToList() );
330333
var routes = configuration.Routes;
331334

332-
if ( !string.IsNullOrEmpty( routePrefix ) )
335+
if ( !IsNullOrEmpty( routePrefix ) )
333336
{
334337
routePrefix = routePrefix.TrimEnd( '/' );
335338
}
336339

337340
if ( batchHandler != null )
338341
{
339-
var batchTemplate = string.IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch;
342+
var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch;
340343
routes.MapHttpBatchRoute( routeName + "Batch", batchTemplate, batchHandler );
341344
}
342345

@@ -347,9 +350,80 @@ public static ODataRoute MapVersionedODataRoute(
347350
var routeConstraint = new VersionedODataPathRouteConstraint( pathHandler, model, routeName, routeConventions.ToArray(), apiVersion );
348351
var route = new ODataRoute( routePrefix, routeConstraint );
349352

353+
AddApiVersionConstraintIfNecessary( route );
350354
routes.Add( routeName, route );
351355

356+
var unversionedRouteConstraint = new ODataPathRouteConstraint( pathHandler, model, routeName, routeConventions.ToArray() );
357+
var unversionedRoute = new ODataRoute( routePrefix, new UnversionedODataPathRouteConstraint( unversionedRouteConstraint, apiVersion ) );
358+
359+
AddApiVersionConstraintIfNecessary( unversionedRoute );
360+
routes.Add( routeName + UnversionedRouteSuffix, unversionedRoute );
361+
352362
return route;
353363
}
364+
365+
private static ODataPathRouteConstraint MakeVersionedODataRouteConstraint(
366+
ODataPathRouteConstraint routeConstraint,
367+
IODataPathHandler pathHandler,
368+
IList<IODataRoutingConvention> routeConventions,
369+
IEdmModel model,
370+
ref string versionedRouteName )
371+
{
372+
Contract.Requires( routeConstraint != null );
373+
Contract.Requires( pathHandler != null );
374+
Contract.Requires( routeConventions != null );
375+
Contract.Requires( model != null );
376+
Contract.Requires( !IsNullOrEmpty( versionedRouteName ) );
377+
Contract.Ensures( Contract.Result<ODataPathRouteConstraint>() != null );
378+
379+
var apiVersion = model.GetAnnotationValue<ApiVersionAnnotation>( model )?.ApiVersion;
380+
381+
if ( apiVersion == null )
382+
{
383+
return routeConstraint;
384+
}
385+
386+
versionedRouteName += "-" + apiVersion.ToString();
387+
return new VersionedODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray(), apiVersion );
388+
}
389+
390+
private static void AddApiVersionConstraintIfNecessary( ODataRoute route )
391+
{
392+
Contract.Requires( route != null );
393+
394+
var routePrefix = route.RoutePrefix;
395+
396+
if ( routePrefix == null || routePrefix.IndexOf( ApiVersionConstraint, Ordinal ) < 0 || route.Constraints.ContainsKey( ApiVersionConstraintName ) )
397+
{
398+
return;
399+
}
400+
401+
// note: even though the constraints are a dictionary, it's important to rebuild the entire collection
402+
// to make sure the api version constraint is evaluated first; otherwise, the current api version will
403+
// not be resolved when the odata versioning constraint is evaluated
404+
var originalConstraints = new Dictionary<string, object>( route.Constraints );
405+
406+
route.Constraints.Clear();
407+
route.Constraints.Add( ApiVersionConstraintName, new ApiVersionRouteConstraint() );
408+
409+
foreach ( var constraint in originalConstraints )
410+
{
411+
route.Constraints.Add( constraint.Key, constraint.Value );
412+
}
413+
}
414+
415+
private static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( string routeName, string routePrefix, HttpRouteCollection routes, List<ODataRoute> odataRoutes, List<IHttpRouteConstraint> unversionedConstraints )
416+
{
417+
Contract.Requires( !IsNullOrEmpty( routeName ) );
418+
Contract.Requires( routes != null );
419+
Contract.Requires( odataRoutes != null );
420+
Contract.Requires( unversionedConstraints != null );
421+
422+
var unversionedRoute = new ODataRoute( routePrefix, new UnversionedODataPathRouteConstraint( unversionedConstraints ) );
423+
424+
AddApiVersionConstraintIfNecessary( unversionedRoute );
425+
routes.Add( routeName + UnversionedRouteSuffix, unversionedRoute );
426+
odataRoutes.Add( unversionedRoute );
427+
}
354428
}
355429
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace System.Web.Http
2+
{
3+
using Diagnostics.Contracts;
4+
using Microsoft.OData.Core;
5+
using Microsoft.Web.Http;
6+
using Microsoft.Web.Http.Versioning;
7+
using System.Net.Http;
8+
using static System.Net.HttpStatusCode;
9+
10+
internal static class HttpRequestMessageExtensions
11+
{
12+
internal static ApiVersion GetRequestedApiVersionOrReturnBadRequest( this HttpRequestMessage request )
13+
{
14+
Contract.Requires( request != null );
15+
16+
try
17+
{
18+
return request.GetRequestedApiVersion();
19+
}
20+
catch ( AmbiguousApiVersionException ex )
21+
{
22+
var error = new ODataError() { ErrorCode = "AmbiguousApiVersion", Message = ex.Message };
23+
throw new HttpResponseException( request.CreateResponse( BadRequest, error ) );
24+
}
25+
}
26+
}
27+
}

src/Microsoft.AspNet.OData.Versioning/Web.OData/Builder/VersionedODataModelBuilder.cs

+44-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
namespace Microsoft.Web.OData.Builder
22
{
3+
using Controllers;
34
using Http;
5+
using Http.Versioning.Conventions;
46
using Microsoft.OData.Edm;
57
using System;
68
using System.Collections.Generic;
@@ -102,21 +104,31 @@ public virtual IEnumerable<IEdmModel> GetEdmModels()
102104
var typeResolver = services.GetHttpControllerTypeResolver();
103105
var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ).Where( c => c.IsODataController() );
104106
var options = configuration.GetApiVersioningOptions();
105-
var apiVersions = new HashSet<ApiVersion>();
107+
var supported = new HashSet<ApiVersion>();
108+
var deprecated = new HashSet<ApiVersion>();
106109

107110
foreach ( var controllerType in controllerTypes )
108111
{
109112
var descriptor = new HttpControllerDescriptor( configuration, string.Empty, controllerType );
110113

111114
options.Conventions.ApplyTo( descriptor );
112115

113-
foreach ( var apiVersion in descriptor.GetImplementedApiVersions() )
116+
var model = descriptor.GetApiVersionModel();
117+
118+
foreach ( var apiVersion in model.SupportedApiVersions )
114119
{
115-
apiVersions.Add( apiVersion );
120+
supported.Add( apiVersion );
121+
}
122+
123+
foreach ( var apiVersion in model.DeprecatedApiVersions )
124+
{
125+
deprecated.Add( apiVersion );
116126
}
117127
}
118128

119-
foreach ( var apiVersion in apiVersions )
129+
deprecated.ExceptWith( supported );
130+
131+
foreach ( var apiVersion in supported.Union( deprecated ) )
120132
{
121133
var builder = ModelBuilderFactory();
122134

@@ -132,7 +144,35 @@ public virtual IEnumerable<IEdmModel> GetEdmModels()
132144
models.Add( model );
133145
}
134146

147+
ConfigureMetadataController( configuration, supported, deprecated );
148+
135149
return models;
136150
}
151+
152+
/// <summary>
153+
/// Configures the metadata controller using the specified configuration and API versions.
154+
/// </summary>
155+
/// <param name="configuration">The current <see cref="HttpConfiguration">configuration</see>.</param>
156+
/// <param name="supportedApiVersions">The discovered <see cref="IEnumerable{T}">sequence</see> of
157+
/// supported OData controller <see cref="ApiVersion">API versions</see>.</param>
158+
/// <param name="deprecatedApiVersions">The discovered <see cref="IEnumerable{T}">sequence</see> of
159+
/// deprecated OData controller <see cref="ApiVersion">API versions</see>.</param>
160+
protected virtual void ConfigureMetadataController( HttpConfiguration configuration, IEnumerable<ApiVersion> supportedApiVersions, IEnumerable<ApiVersion> deprecatedApiVersions )
161+
{
162+
var controllerMapping = configuration.Services.GetHttpControllerSelector().GetControllerMapping();
163+
var controllerDescriptor = default( HttpControllerDescriptor );
164+
165+
if ( !controllerMapping.TryGetValue( "VersionedMetadata", out controllerDescriptor ))
166+
{
167+
return;
168+
}
169+
170+
var options = configuration.GetApiVersioningOptions();
171+
var controllerBuilder = options.Conventions.Controller<VersionedMetadataController>()
172+
.HasApiVersions( supportedApiVersions )
173+
.HasDeprecatedApiVersions( deprecatedApiVersions );
174+
175+
controllerBuilder.ApplyTo( controllerDescriptor );
176+
}
137177
}
138178
}

0 commit comments

Comments
 (0)