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}
0 commit comments