diff --git a/src/Microsoft.AspNet.OData.Versioning/System.Web.OData/HttpConfigurationExtensions.cs b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs similarity index 81% rename from src/Microsoft.AspNet.OData.Versioning/System.Web.OData/HttpConfigurationExtensions.cs rename to src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs index 56164413..eb7e98ce 100644 --- a/src/Microsoft.AspNet.OData.Versioning/System.Web.OData/HttpConfigurationExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs @@ -8,13 +8,17 @@ using Microsoft; using Microsoft.OData.Edm; using Microsoft.Web.Http; + using Microsoft.Web.Http.Routing; using Microsoft.Web.OData.Builder; using Microsoft.Web.OData.Routing; using OData.Batch; using OData.Extensions; using OData.Routing; using OData.Routing.Conventions; + using Routing; using static Linq.Expressions.Expression; + using static System.String; + using static System.StringComparison; /// /// Provides extension methods for the class. @@ -22,6 +26,9 @@ public static class HttpConfigurationExtensions { private const string ResolverSettingsKey = "System.Web.OData.ResolverSettingsKey"; + private const string UnversionedRouteSuffix = "-Unversioned"; + private const string ApiVersionConstraintName = "apiVersion"; + private const string ApiVersionConstraint = "{" + ApiVersionConstraintName + "}"; private static readonly Lazy> setResolverSettings = new Lazy>( GetResolverSettingsMutator ); private static Action GetResolverSettingsMutator() @@ -192,14 +199,14 @@ public static IReadOnlyList MapVersionedODataRoutes( var routeConventions = EnsureConventions( routingConventions.ToList() ); var routes = configuration.Routes; - if ( !string.IsNullOrEmpty( routePrefix ) ) + if ( !IsNullOrEmpty( routePrefix ) ) { routePrefix = routePrefix.TrimEnd( '/' ); } if ( batchHandler != null ) { - var batchTemplate = string.IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; + var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; routes.MapHttpBatchRoute( routeName + "Batch", batchTemplate, batchHandler ); } @@ -207,31 +214,27 @@ public static IReadOnlyList MapVersionedODataRoutes( routeConventions.Insert( 0, null ); var odataRoutes = new List(); + var unversionedConstraints = new List(); foreach ( var model in models ) { var versionedRouteName = routeName; - var apiVersion = model.GetAnnotationValue( model )?.ApiVersion; var routeConstraint = default( ODataPathRouteConstraint ); routeConventions[0] = new VersionedAttributeRoutingConvention( model, configuration ); - - if ( apiVersion == null ) - { - routeConstraint = new ODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray() ); - } - else - { - versionedRouteName += "-" + apiVersion.ToString(); - routeConstraint = new VersionedODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray(), apiVersion ); - } + routeConstraint = new ODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray() ); + unversionedConstraints.Add( routeConstraint ); + routeConstraint = MakeVersionedODataRouteConstraint( routeConstraint, pathHandler, routeConventions, model, ref versionedRouteName ); var route = new ODataRoute( routePrefix, routeConstraint ); + AddApiVersionConstraintIfNecessary( route ); routes.Add( versionedRouteName, route ); odataRoutes.Add( route ); } + AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, routes, odataRoutes, unversionedConstraints ); + return odataRoutes; } @@ -329,14 +332,14 @@ public static ODataRoute MapVersionedODataRoute( var routeConventions = EnsureConventions( routingConventions.ToList() ); var routes = configuration.Routes; - if ( !string.IsNullOrEmpty( routePrefix ) ) + if ( !IsNullOrEmpty( routePrefix ) ) { routePrefix = routePrefix.TrimEnd( '/' ); } if ( batchHandler != null ) { - var batchTemplate = string.IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; + var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; routes.MapHttpBatchRoute( routeName + "Batch", batchTemplate, batchHandler ); } @@ -347,9 +350,80 @@ public static ODataRoute MapVersionedODataRoute( var routeConstraint = new VersionedODataPathRouteConstraint( pathHandler, model, routeName, routeConventions.ToArray(), apiVersion ); var route = new ODataRoute( routePrefix, routeConstraint ); + AddApiVersionConstraintIfNecessary( route ); routes.Add( routeName, route ); + var unversionedRouteConstraint = new ODataPathRouteConstraint( pathHandler, model, routeName, routeConventions.ToArray() ); + var unversionedRoute = new ODataRoute( routePrefix, new UnversionedODataPathRouteConstraint( unversionedRouteConstraint, apiVersion ) ); + + AddApiVersionConstraintIfNecessary( unversionedRoute ); + routes.Add( routeName + UnversionedRouteSuffix, unversionedRoute ); + return route; } + + private static ODataPathRouteConstraint MakeVersionedODataRouteConstraint( + ODataPathRouteConstraint routeConstraint, + IODataPathHandler pathHandler, + IList routeConventions, + IEdmModel model, + ref string versionedRouteName ) + { + Contract.Requires( routeConstraint != null ); + Contract.Requires( pathHandler != null ); + Contract.Requires( routeConventions != null ); + Contract.Requires( model != null ); + Contract.Requires( !IsNullOrEmpty( versionedRouteName ) ); + Contract.Ensures( Contract.Result() != null ); + + var apiVersion = model.GetAnnotationValue( model )?.ApiVersion; + + if ( apiVersion == null ) + { + return routeConstraint; + } + + versionedRouteName += "-" + apiVersion.ToString(); + return new VersionedODataPathRouteConstraint( pathHandler, model, versionedRouteName, routeConventions.ToArray(), apiVersion ); + } + + private static void AddApiVersionConstraintIfNecessary( ODataRoute route ) + { + Contract.Requires( route != null ); + + var routePrefix = route.RoutePrefix; + + if ( routePrefix == null || routePrefix.IndexOf( ApiVersionConstraint, Ordinal ) < 0 || route.Constraints.ContainsKey( ApiVersionConstraintName ) ) + { + return; + } + + // note: even though the constraints are a dictionary, it's important to rebuild the entire collection + // to make sure the api version constraint is evaluated first; otherwise, the current api version will + // not be resolved when the odata versioning constraint is evaluated + var originalConstraints = new Dictionary( route.Constraints ); + + route.Constraints.Clear(); + route.Constraints.Add( ApiVersionConstraintName, new ApiVersionRouteConstraint() ); + + foreach ( var constraint in originalConstraints ) + { + route.Constraints.Add( constraint.Key, constraint.Value ); + } + } + + private static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( string routeName, string routePrefix, HttpRouteCollection routes, List odataRoutes, List unversionedConstraints ) + { + Contract.Requires( !IsNullOrEmpty( routeName ) ); + Contract.Requires( routes != null ); + Contract.Requires( odataRoutes != null ); + Contract.Requires( unversionedConstraints != null ); + + var unversionedRoute = new ODataRoute( routePrefix, new UnversionedODataPathRouteConstraint( unversionedConstraints ) ); + + AddApiVersionConstraintIfNecessary( unversionedRoute ); + routes.Add( routeName + UnversionedRouteSuffix, unversionedRoute ); + odataRoutes.Add( unversionedRoute ); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs new file mode 100644 index 00000000..ef712da8 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs @@ -0,0 +1,27 @@ +namespace System.Web.Http +{ + using Diagnostics.Contracts; + using Microsoft.OData.Core; + using Microsoft.Web.Http; + using Microsoft.Web.Http.Versioning; + using System.Net.Http; + using static System.Net.HttpStatusCode; + + internal static class HttpRequestMessageExtensions + { + internal static ApiVersion GetRequestedApiVersionOrReturnBadRequest( this HttpRequestMessage request ) + { + Contract.Requires( request != null ); + + try + { + return request.GetRequestedApiVersion(); + } + catch ( AmbiguousApiVersionException ex ) + { + var error = new ODataError() { ErrorCode = "AmbiguousApiVersion", Message = ex.Message }; + throw new HttpResponseException( request.CreateResponse( BadRequest, error ) ); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Builder/VersionedODataModelBuilder.cs b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Builder/VersionedODataModelBuilder.cs index 25a0e58f..9a907058 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Builder/VersionedODataModelBuilder.cs +++ b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Builder/VersionedODataModelBuilder.cs @@ -1,6 +1,8 @@ namespace Microsoft.Web.OData.Builder { + using Controllers; using Http; + using Http.Versioning.Conventions; using Microsoft.OData.Edm; using System; using System.Collections.Generic; @@ -102,7 +104,8 @@ public virtual IEnumerable GetEdmModels() var typeResolver = services.GetHttpControllerTypeResolver(); var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ).Where( c => c.IsODataController() ); var options = configuration.GetApiVersioningOptions(); - var apiVersions = new HashSet(); + var supported = new HashSet(); + var deprecated = new HashSet(); foreach ( var controllerType in controllerTypes ) { @@ -110,13 +113,22 @@ public virtual IEnumerable GetEdmModels() options.Conventions.ApplyTo( descriptor ); - foreach ( var apiVersion in descriptor.GetImplementedApiVersions() ) + var model = descriptor.GetApiVersionModel(); + + foreach ( var apiVersion in model.SupportedApiVersions ) { - apiVersions.Add( apiVersion ); + supported.Add( apiVersion ); + } + + foreach ( var apiVersion in model.DeprecatedApiVersions ) + { + deprecated.Add( apiVersion ); } } - foreach ( var apiVersion in apiVersions ) + deprecated.ExceptWith( supported ); + + foreach ( var apiVersion in supported.Union( deprecated ) ) { var builder = ModelBuilderFactory(); @@ -132,7 +144,35 @@ public virtual IEnumerable GetEdmModels() models.Add( model ); } + ConfigureMetadataController( configuration, supported, deprecated ); + return models; } + + /// + /// Configures the metadata controller using the specified configuration and API versions. + /// + /// The current configuration. + /// The discovered sequence of + /// supported OData controller API versions. + /// The discovered sequence of + /// deprecated OData controller API versions. + protected virtual void ConfigureMetadataController( HttpConfiguration configuration, IEnumerable supportedApiVersions, IEnumerable deprecatedApiVersions ) + { + var controllerMapping = configuration.Services.GetHttpControllerSelector().GetControllerMapping(); + var controllerDescriptor = default( HttpControllerDescriptor ); + + if ( !controllerMapping.TryGetValue( "VersionedMetadata", out controllerDescriptor )) + { + return; + } + + var options = configuration.GetApiVersioningOptions(); + var controllerBuilder = options.Conventions.Controller() + .HasApiVersions( supportedApiVersions ) + .HasDeprecatedApiVersions( deprecatedApiVersions ); + + controllerBuilder.ApplyTo( controllerDescriptor ); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Controllers/VersionedMetadataController.cs b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Controllers/VersionedMetadataController.cs index 7b3da454..c59621ca 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Controllers/VersionedMetadataController.cs +++ b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Controllers/VersionedMetadataController.cs @@ -1,15 +1,8 @@ namespace Microsoft.Web.OData.Controllers { using Http; - using Http.Versioning; - using System; - using System.Collections.Generic; - using System.Diagnostics.Contracts; - using System.Linq; using System.Net.Http; - using System.Net.Http.Headers; using System.Web.Http; - using System.Web.Http.Controllers; using System.Web.OData; using static Microsoft.OData.Core.ODataConstants; using static Microsoft.OData.Core.ODataUtils; @@ -20,63 +13,9 @@ /// /// Represents a controller for generating versioned OData service and metadata documents. /// - /// This controller is, itself, API version-neutral. - [ApiVersionNeutral] + [ReportApiVersions] public class VersionedMetadataController : MetadataController { - private sealed class DiscoveredApiVersions - { - private const string ValueSeparator = ", "; - - internal DiscoveredApiVersions( IEnumerable models ) - { - Contract.Requires( models != null ); - - var supported = new HashSet(); - var deprecated = new HashSet(); - - foreach ( var model in models ) - { - foreach ( var version in model.SupportedApiVersions ) - { - supported.Add( version ); - } - - foreach ( var version in model.DeprecatedApiVersions ) - { - deprecated.Add( version ); - } - } - - if ( supported.Count > 0 ) - { - deprecated.ExceptWith( supported ); - SupportedApiVersions = Join( ValueSeparator, supported.OrderBy( v => v ).Select( v => v.ToString() ) ); - } - - if ( deprecated.Count > 0 ) - { - DeprecatedApiVersions = Join( ValueSeparator, deprecated.OrderBy( v => v ).Select( v => v.ToString() ) ); - } - } - - public string SupportedApiVersions { get; } - - public string DeprecatedApiVersions { get; } - } - - private const string ApiSupportedVersions = "api-supported-versions"; - private const string ApiDeprecatedVersions = "api-deprecated-versions"; - private readonly Lazy discovered; - - /// - /// Initializes a new instance of the class. - /// - public VersionedMetadataController() - { - discovered = new Lazy( () => new DiscoveredApiVersions( DiscoverODataApiVersions() ) ); - } - /// /// Handles a request for the HTTP OPTIONS method. /// @@ -112,45 +51,8 @@ public virtual IHttpActionResult GetOptions() response.Content.Headers.Add( "Allow", new[] { "GET", "OPTIONS" } ); response.Content.Headers.ContentType = null; headers.Add( ODataVersionHeader, ODataVersionToString( V4 ) ); - ReportApiVersions( headers ); return ResponseMessage( response ); } - - private DiscoveredApiVersions Discovered => discovered.Value; - - private void ReportApiVersions( HttpHeaders headers ) - { - Contract.Requires( headers != null ); - - var value = Discovered.SupportedApiVersions; - - if ( value != null ) - { - headers.Add( ApiSupportedVersions, value ); - } - - value = Discovered.DeprecatedApiVersions; - - if ( value != null ) - { - headers.Add( ApiDeprecatedVersions, value ); - } - } - - private IEnumerable DiscoverODataApiVersions() - { - Contract.Ensures( Contract.Result>() != null ); - - var services = Configuration.Services; - var assembliesResolver = services.GetAssembliesResolver(); - var typeResolver = services.GetHttpControllerTypeResolver(); - var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ); - - return from controllerType in controllerTypes - where controllerType.IsODataController() - let descriptor = new HttpControllerDescriptor( Configuration, string.Empty, controllerType ) - select descriptor.GetApiVersionModel(); - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/UnversionedODataPathRouteConstraint.cs b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/UnversionedODataPathRouteConstraint.cs new file mode 100644 index 00000000..803103fb --- /dev/null +++ b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/UnversionedODataPathRouteConstraint.cs @@ -0,0 +1,49 @@ +namespace Microsoft.Web.OData.Routing +{ + using Http; + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Net.Http; + using System.Web.Http; + using System.Web.Http.Routing; + using static System.Web.Http.Routing.HttpRouteDirection; + + internal sealed class UnversionedODataPathRouteConstraint : IHttpRouteConstraint + { + private readonly ApiVersion apiVersion; + private readonly IEnumerable innerConstraints; + + internal UnversionedODataPathRouteConstraint( IEnumerable innerConstraints ) + { + Contract.Requires( innerConstraints != null ); + this.innerConstraints = innerConstraints; + } + + internal UnversionedODataPathRouteConstraint( IHttpRouteConstraint innerConstraint, ApiVersion apiVersion ) + { + Contract.Requires( innerConstraint != null ); + + innerConstraints = new[] { innerConstraint }; + this.apiVersion = apiVersion; + } + + private bool MatchAnyVersion => apiVersion == null; + + public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection ) + { + if ( routeDirection == UriGeneration ) + { + return true; + } + + if ( !MatchAnyVersion && apiVersion != request.GetRequestedApiVersion() ) + { + return false; + } + + return innerConstraints.Any( c => c.Match( request, route, parameterName, values, routeDirection ) ); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedODataPathRouteConstraint.cs b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedODataPathRouteConstraint.cs index e1070c75..d37d1563 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedODataPathRouteConstraint.cs +++ b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedODataPathRouteConstraint.cs @@ -1,8 +1,6 @@ namespace Microsoft.Web.OData.Routing { using Http; - using Http.Versioning; - using Microsoft.OData.Core; using Microsoft.OData.Edm; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -12,9 +10,6 @@ using System.Web.Http.Routing; using System.Web.OData.Routing; using System.Web.OData.Routing.Conventions; - using static Http.ApiVersion; - using static System.Net.HttpStatusCode; - using static System.StringSplitOptions; using static System.Web.Http.Routing.HttpRouteDirection; /// @@ -49,61 +44,6 @@ private static bool IsServiceDocumentOrMetadataRoute( IDictionary /// Gets the API version matched by the current OData path route constraint. /// @@ -125,16 +65,22 @@ public override bool Match( HttpRequestMessage request, IHttpRoute route, string Arg.NotNull( request, nameof( request ) ); Arg.NotNull( values, nameof( values ) ); - if ( routeDirection != UriResolution ) + if ( routeDirection == UriGeneration ) { - return false; + return true; } - var requestedVersion = ResolveApiVersion( request, route ); + var requestedVersion = request.GetRequestedApiVersionOrReturnBadRequest(); if ( requestedVersion != null ) { - return ApiVersion == requestedVersion && base.Match( request, route, parameterName, values, routeDirection ); + if ( ApiVersion == requestedVersion && base.Match( request, route, parameterName, values, routeDirection ) ) + { + DecorateUrlHelperWithApiVersionRouteValueIfNecessary( request, values ); + return true; + } + + return false; } var options = request.GetApiVersioningOptions(); @@ -152,5 +98,21 @@ public override bool Match( HttpRequestMessage request, IHttpRoute route, string return false; } + + private static void DecorateUrlHelperWithApiVersionRouteValueIfNecessary( HttpRequestMessage request, IDictionary values ) + { + Contract.Requires( request != null ); + Contract.Requires( values != null ); + + var apiVersion = default( object ); + + if ( !values.TryGetValue( nameof( apiVersion ), out apiVersion ) ) + { + return; + } + + var requestContext = request.GetRequestContext(); + requestContext.Url = new VersionedUrlHelperDecorator( requestContext.Url, apiVersion ); + } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedUrlHelperDecorator.cs b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedUrlHelperDecorator.cs new file mode 100644 index 00000000..0423bc97 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedUrlHelperDecorator.cs @@ -0,0 +1,47 @@ +namespace Microsoft.Web.OData.Routing +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Web.Http.Routing; + + internal sealed class VersionedUrlHelperDecorator : UrlHelper + { + private readonly UrlHelper decorated; + private readonly object apiVersion; + + internal VersionedUrlHelperDecorator( UrlHelper decorated, object apiVersion ) + { + Contract.Requires( decorated != null ); + Contract.Requires( apiVersion != null ); + + this.decorated = decorated; + this.apiVersion = apiVersion; + + if ( decorated.Request != null ) + { + Request = decorated.Request; + } + } + + private void EnsureApiVersionRouteValue( IDictionary routeValues ) => routeValues[nameof( apiVersion )] = apiVersion; + + public override string Content( string path ) => decorated.Content( path ); + + public override string Link( string routeName, object routeValues ) => decorated.Link( routeName, routeValues ); + + public override string Link( string routeName, IDictionary routeValues ) + { + EnsureApiVersionRouteValue( routeValues ); + return decorated.Link( routeName, routeValues ); + } + + public override string Route( string routeName, object routeValues ) => decorated.Route( routeName, routeValues ); + + public override string Route( string routeName, IDictionary routeValues ) + { + EnsureApiVersionRouteValue( routeValues ); + return decorated.Route( routeName, routeValues ); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpResponseExceptionFactory.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpResponseExceptionFactory.cs index e4b08d4e..85ce1dbd 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpResponseExceptionFactory.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpResponseExceptionFactory.cs @@ -13,16 +13,16 @@ internal sealed class HttpResponseExceptionFactory { private static readonly string ControllerSelectorCategory = typeof( IHttpControllerSelector ).FullName; private readonly HttpRequestMessage request; - private readonly ITraceWriter traceWriter; internal HttpResponseExceptionFactory( HttpRequestMessage request ) { Contract.Requires( request != null ); this.request = request; - traceWriter = request.GetConfiguration().Services.GetTraceWriter() ?? NullTraceWriter.Instance; } + private ITraceWriter TraceWriter => request.GetConfiguration().Services.GetTraceWriter() ?? NullTraceWriter.Instance; + [SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )] internal HttpResponseException NewNotFoundOrBadRequestException( ControllerSelectionResult conventionRouteResult, ControllerSelectionResult directRouteResult ) => CreateBadRequestForUnsupportedApiVersion( conventionRouteResult, directRouteResult ) ?? CreateBadRequestForInvalidApiVersion() ?? CreateNotFound( conventionRouteResult ); @@ -52,7 +52,7 @@ private HttpResponseException CreateBadRequestForUnsupportedApiVersion( Controll var error = new HttpError() { Message = message, MessageDetail = messageDetail }; error["Code"] = "UnsupportedApiVersion"; - traceWriter.Info( request, ControllerSelectorCategory, message ); + TraceWriter.Info( request, ControllerSelectorCategory, message ); return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) ); } @@ -73,7 +73,7 @@ private HttpResponseException CreateBadRequestForInvalidApiVersion() var error = new HttpError() { Message = message, MessageDetail = messageDetail }; error["Code"] = "InvalidApiVersion"; - traceWriter.Info( request, ControllerSelectorCategory, message ); + TraceWriter.Info( request, ControllerSelectorCategory, message ); return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) ); } @@ -95,9 +95,9 @@ private HttpResponseException CreateNotFound( ControllerSelectionResult conventi messageDetail = SR.DefaultControllerFactory_ControllerNameNotFound.FormatDefault( conventionRouteResult.ControllerName ); } - traceWriter.Info( request, ControllerSelectorCategory, message ); + TraceWriter.Info( request, ControllerSelectorCategory, message ); return new HttpResponseException( request.CreateErrorResponse( NotFound, message, messageDetail ) ); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs b/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs index 99126208..0e01df77 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs @@ -2,13 +2,12 @@ { using System; using System.Collections.Generic; - using System.Diagnostics.Contracts; - using System.Linq; using System.Net.Http; using System.Web.Http; using System.Web.Http.Routing; - using static System.Web.Http.Routing.HttpRouteDirection; using static ApiVersion; + using static System.String; + using static System.Web.Http.Routing.HttpRouteDirection; /// /// Represents a route constraint for API versions. @@ -26,12 +25,13 @@ public sealed class ApiVersionRouteConstraint : IHttpRouteConstraint /// True if the route constraint is matched; otherwise, false. public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection ) { - if ( routeDirection != UriResolution ) + var value = default( string ); + + if ( routeDirection == UriGeneration ) { - return false; + return !IsNullOrEmpty( parameterName ) && values.TryGetValue( parameterName, out value ) && !IsNullOrEmpty( value ); } - var value = default( string ); var requestedVersion = default( ApiVersion ); if ( !values.TryGetValue( parameterName, out value ) || !TryParse( value, out requestedVersion ) ) diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/System.Web.OData/HttpConfigurationExtensionsTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/System.Web.OData/HttpConfigurationExtensionsTest.cs index 51a9bd08..f3aa330a 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/System.Web.OData/HttpConfigurationExtensionsTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/System.Web.OData/HttpConfigurationExtensionsTest.cs @@ -85,7 +85,13 @@ public void map_versioned_odata_routes_should_return_expected_results() // assert foreach ( var route in routes ) { - var constraint = (VersionedODataPathRouteConstraint) route.PathRouteConstraint; + var constraint = route.PathRouteConstraint as VersionedODataPathRouteConstraint; + + if ( constraint == null ) + { + continue; + } + var apiVersion = constraint.EdmModel.GetAnnotationValue( constraint.EdmModel ).ApiVersion; var versionedRouteName = routeName + "-" + apiVersion.ToString(); @@ -100,4 +106,4 @@ public void map_versioned_odata_routes_should_return_expected_results() batchRoute.RouteTemplate.Should().Be( "api/$batch" ); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Controllers/VersionedMetadataControllerTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Controllers/VersionedMetadataControllerTest.cs index f6822f4d..bc546819 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Controllers/VersionedMetadataControllerTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Controllers/VersionedMetadataControllerTest.cs @@ -1,11 +1,13 @@ namespace Microsoft.Web.OData.Controllers { + using Builder; using FluentAssertions; using Http; using Moq; using System; using System.Collections.Generic; using System.Linq; + using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web.Http; @@ -33,16 +35,27 @@ public async Task options_should_return_expected_headers() { // arrange var configuration = new HttpConfiguration(); + var builder = new VersionedODataModelBuilder( configuration ); + var metadata = new VersionedMetadataController() { Configuration = configuration }; var controllerTypeResolver = new Mock(); - var controllerTypes = new List() { typeof( Controller1 ), typeof( Controller2 ) }; + var controllerTypes = new List() { typeof( Controller1 ), typeof( Controller2 ), typeof( VersionedMetadataController ) }; controllerTypeResolver.Setup( ctr => ctr.GetControllerTypes( It.IsAny() ) ).Returns( controllerTypes ); configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object ); + configuration.AddApiVersioning(); - var metadata = new VersionedMetadataController() { Configuration = configuration }; + var models = builder.GetEdmModels(); + var request = new HttpRequestMessage( new HttpMethod( "OPTIONS" ), "http://localhost/$metadata" ); + var response = default( HttpResponseMessage ); + + configuration.MapVersionedODataRoutes( "odata", null, models ); - // act - var response = await metadata.GetOptions().ExecuteAsync( CancellationToken.None ); + using ( var server = new HttpServer( configuration ) ) + using ( var client = new HttpClient( server ) ) + { + // act + response = ( await client.SendAsync( request ) ).EnsureSuccessStatusCode(); + } // assert response.Headers.GetValues( "OData-Version" ).Single().Should().Be( "4.0" ); diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Routing/VersionedODataPathRouteConstraintTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Routing/VersionedODataPathRouteConstraintTest.cs index b5ef3ba8..f75ea1a3 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Routing/VersionedODataPathRouteConstraintTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Routing/VersionedODataPathRouteConstraintTest.cs @@ -55,7 +55,7 @@ private static VersionedODataPathRouteConstraint NewVersionedODataPathRouteConst } [Fact] - public void match_should_only_evaluate_uri_resolution() + public void match_should_always_return_true_for_uri_resolution() { // arrange var request = new HttpRequestMessage(); @@ -72,7 +72,7 @@ public void match_should_only_evaluate_uri_resolution() var result = constraint.Match( request, route, parameterName, values, routeDirection ); // assert - result.Should().BeFalse(); + result.Should().BeTrue(); } [Theory] @@ -140,27 +140,6 @@ public void match_should_return_expected_result_when_controller_is_implicitly_ve result.Should().Be( expected ); } - [Theory] - [InlineData( "v1", "1.0" )] - [InlineData( "v2.0", "2.0" )] - public void match_should_set_requested_api_version_from_route_prefix( string routePrefix, string apiVersion ) - { - // arrange - var requestedVersion = Parse( apiVersion ); - var model = TestModel; - var request = new HttpRequestMessage( Get, $"http://localhost/{routePrefix}/Tests(1)" ); - var values = new Dictionary() { { "odataPath", "Tests(1)" } }; - var constraint = NewVersionedODataPathRouteConstraint( request, model, requestedVersion, routePrefix ); - var route = request.GetConfiguration().Routes.Single(); - - // act - var result = constraint.Match( request, route, null, values, UriResolution ); - - // assert - result.Should().BeTrue(); - request.GetRequestedApiVersion().Should().Be( requestedVersion ); - } - [Fact] public void match_should_return_400_when_requested_api_version_is_ambiguous() { diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs index 44b18d06..c1993540 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs @@ -1,5 +1,4 @@ -using Xunit; -namespace Microsoft.Web.Http.Dispatcher +namespace Microsoft.Web.Http.Dispatcher { using Controllers; using FluentAssertions; @@ -955,7 +954,6 @@ public void select_controller_should_resolve_controller_using_api_versioning_con public void select_controller_should_resolve_controller_action_using_api_versioning_conventions() { // arrange - var supportedVersions = new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ) }; var configuration = new HttpConfiguration(); var request = new HttpRequestMessage( Get, "http://localhost/api/conventions?api-version=2.0" ); @@ -1000,7 +998,6 @@ public void select_controller_should_resolve_controller_action_using_api_version public void select_controller_should_report_correct_api_versions_using_conventions() { // arrange - var supportedVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }; var controllerTypeResolver = new Mock(); var controllerTypes = new Collection() { typeof( ConventionsController ), typeof( Conventions2Controller ) }; var configuration = new HttpConfiguration(); diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs index 98170067..8068c58e 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs @@ -3,16 +3,22 @@ using FluentAssertions; using Moq; using System.Collections.Generic; - using System.Linq; using System.Net.Http; + using System.Web.Http; + using System.Web.Http.Hosting; using System.Web.Http.Routing; using Xunit; + using static System.String; using static System.Web.Http.Routing.HttpRouteDirection; public class ApiVersionRouteConstraintTest { - [Fact] - public void match_should_return_false_for_uri_generation() + [Theory] + [InlineData( "apiVersion", "1", true )] + [InlineData( "apiVersion", null, false )] + [InlineData( "apiVersion", "", false )] + [InlineData( null, "", false )] + public void match_should_return_expected_result_for_url_generation( string key, string value, bool expected ) { // arrange var request = new HttpRequestMessage(); @@ -21,11 +27,16 @@ public void match_should_return_false_for_uri_generation() var routeDirection = UriGeneration; var constraint = new ApiVersionRouteConstraint(); + if ( !IsNullOrEmpty( key ) ) + { + values[key] = value; + } + // act - var matched = constraint.Match( request, route, "version", values, routeDirection ); + var matched = constraint.Match( request, route, key, values, routeDirection ); // assert - matched.Should().BeFalse(); + matched.Should().Be( expected ); } [Fact] @@ -82,5 +93,26 @@ public void match_should_return_true_when_matched() // assert matched.Should().BeTrue(); } + + [Fact] + public void url_helper_should_create_route_link_with_api_version_constriant() + { + // arrange + var request = new HttpRequestMessage(); + var routes = new HttpRouteCollection( "/" ); + var route = routes.MapHttpRoute( "Default", "v{apiVersion}/{controller}/{id}", defaults: null, constraints: new { apiVersion = new ApiVersionRouteConstraint() } ); + var values = new HttpRouteValueDictionary( new { apiVersion = "1", controller = "people", id = "123" } ); + var urlHelper = new UrlHelper( request ); + var routeValues = new { apiVersion = "1", controller = "people", id = "123" }; + + request.Properties[HttpPropertyKeys.HttpConfigurationKey] = new HttpConfiguration( routes ); + request.Properties[HttpPropertyKeys.HttpRouteDataKey] = new HttpRouteData( route, values ); + + // act + var url = urlHelper.Route( "Default", routeValues ); + + // assert + url.Should().Be( "/v1/people/123" ); + } } -} +} \ No newline at end of file