diff --git a/src/Common/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs b/src/Common/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs index a3154b93..cd997215 100644 --- a/src/Common/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs +++ b/src/Common/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs @@ -12,8 +12,6 @@ namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions /// public partial class ActionApiVersionConventionBuilderBase { - readonly HashSet mappedVersions = new HashSet(); - /// /// Initializes a new instance of the class. /// @@ -23,6 +21,6 @@ protected ActionApiVersionConventionBuilderBase() { } /// Gets the collection of API versions mapped to the current action. /// /// A collection of mapped API versions. - protected ICollection MappedVersions => mappedVersions; + protected ICollection MappedVersions { get; } = new HashSet(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs index 52ffc411..15e75487 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs @@ -5,35 +5,8 @@ static class HttpControllerDescriptorExtensions { - const string RelatedControllerCandidatesKey = "MS_RelatedControllerCandidates"; - internal static IEnumerable AsEnumerable( this HttpControllerDescriptor controllerDescriptor ) { - if ( controllerDescriptor.Properties.TryGetValue( RelatedControllerCandidatesKey, out object value ) ) - { - if ( value is IEnumerable relatedCandidates ) - { - using ( var relatedControllerDescriptors = relatedCandidates.GetEnumerator() ) - { - if ( relatedControllerDescriptors.MoveNext() ) - { - yield return controllerDescriptor; - - do - { - if ( relatedControllerDescriptors.Current != controllerDescriptor ) - { - yield return relatedControllerDescriptors.Current; - } - } - while ( relatedControllerDescriptors.MoveNext() ); - - yield break; - } - } - } - } - if ( controllerDescriptor is IEnumerable groupedControllerDescriptors ) { foreach ( var groupedControllerDescriptor in groupedControllerDescriptors ) diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpRouteCollectionExtensions.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpRouteCollectionExtensions.cs index f994845a..a7f7a632 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpRouteCollectionExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpRouteCollectionExtensions.cs @@ -1,10 +1,7 @@ namespace System.Web.Http { - using System.Collections.Generic; using System.Diagnostics.Contracts; - using System.Reflection; using System.Web.Http.Routing; - using static System.Reflection.BindingFlags; static class HttpRouteCollectionExtensions { @@ -13,9 +10,7 @@ internal static string GetRouteName( this HttpRouteCollection routes, IHttpRoute Contract.Requires( routes != null ); Contract.Requires( route != null ); - var items = CopyRouteEntries( routes ); - - foreach ( var item in items ) + foreach ( var item in routes.ToDictionary() ) { if ( Equals( item.Value, route ) ) { @@ -25,61 +20,5 @@ internal static string GetRouteName( this HttpRouteCollection routes, IHttpRoute return null; } - - static KeyValuePair[] CopyRouteEntries( HttpRouteCollection routes ) - { - Contract.Requires( routes != null ); - Contract.Ensures( Contract.Result[]>() != null ); - - var items = new KeyValuePair[routes.Count]; - - try - { - routes.CopyTo( items, 0 ); - } - catch ( NotSupportedException ) when ( routes.GetType().FullName == "System.Web.Http.WebHost.Routing.HostedHttpRouteCollection" ) - { - var keys = GetRouteKeys( routes ); - - for ( var i = 0; i < keys.Count; i++ ) - { - var key = keys[i]; - var route = routes[key]; - - items[i] = new KeyValuePair( key, route ); - } - } - - return items; - } - - static IReadOnlyList GetRouteKeys( HttpRouteCollection routes ) - { - Contract.Requires( routes != null ); - Contract.Ensures( Contract.Result>() != null ); - - var collection = GetKeys( routes ); - var keys = new string[collection.Count]; - - collection.CopyTo( keys, 0 ); - - return keys; - } - - static ICollection GetKeys( HttpRouteCollection routes ) - { - Contract.Requires( routes != null ); - Contract.Ensures( Contract.Result>() != null ); - - // HACK: System.Web.Routing.RouteCollection doesn't expose the names associated with registered routes. The - // HostedHttpRouteCollection could have provided an adapter to support it, but didn't. Instead, it always throws - // NotSupportedException for the HttpRouteCollection.CopyTo method. This only happens when hosted on IIS. The - // only way to get the keys is use reflection to poke at the underlying dictionary. - var routeCollection = routes.GetType().GetField( "_routeCollection", Instance | NonPublic ).GetValue( routes ); - var dictionary = routeCollection.GetType().GetField( "_namedMap", Instance | NonPublic ).GetValue( routeCollection ); - var keys = (ICollection) dictionary.GetType().GetRuntimeProperty( "Keys" ).GetValue( dictionary, null ); - - return keys; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs b/src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs index 6da353c7..adc08334 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs +++ b/src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs @@ -3,8 +3,8 @@ using Microsoft.Web.Http; using Microsoft.Web.Http.Versioning; using Microsoft.Web.Http.Versioning.Conventions; + using System; using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Web.Http; @@ -23,7 +23,6 @@ public partial class VersionedODataModelBuilder /// This constructor resolves the current from the /// extension method via the /// . - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract." )] public VersionedODataModelBuilder( HttpConfiguration configuration ) { Arg.NotNull( configuration, nameof( configuration ) ); @@ -55,15 +54,18 @@ protected virtual IReadOnlyList GetApiVersions() var assembliesResolver = services.GetAssembliesResolver(); var typeResolver = services.GetHttpControllerTypeResolver(); var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ).Where( TypeExtensions.IsODataController ); - var conventions = Options.Conventions; + var controllerDescriptors = services.GetHttpControllerSelector().GetControllerMapping().Values; var supported = new HashSet(); var deprecated = new HashSet(); foreach ( var controllerType in controllerTypes ) { - var descriptor = new HttpControllerDescriptor( Configuration, string.Empty, controllerType ); + var descriptor = FindControllerDescriptor( controllerDescriptors, controllerType ); - conventions.ApplyTo( descriptor ); + if ( descriptor == null ) + { + continue; + } var model = descriptor.GetApiVersionModel(); var versions = model.SupportedApiVersions; @@ -116,5 +118,31 @@ protected virtual void ConfigureMetadataController( IEnumerable supp controllerBuilder.ApplyTo( controllerDescriptor ); } + + static HttpControllerDescriptor FindControllerDescriptor( IEnumerable controllerDescriptors, Type controllerType ) + { + Contract.Requires( controllerDescriptors != null ); + Contract.Requires( controllerType != null ); + + foreach ( var controllerDescriptor in controllerDescriptors ) + { + if ( controllerDescriptor is IEnumerable groupedControllerDescriptors ) + { + foreach ( var groupedControllerDescriptor in groupedControllerDescriptors ) + { + if ( controllerType.Equals( groupedControllerDescriptor.ControllerType ) ) + { + return groupedControllerDescriptor; + } + } + } + else if ( controllerType.Equals( controllerDescriptor.ControllerType ) ) + { + return controllerDescriptor; + } + } + + return default; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs index 4e0795f7..4aa0d4ef 100644 --- a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs @@ -610,12 +610,9 @@ public static IEdmModel GetEdmModel( this HttpConfiguration configuration, ApiVe Arg.NotNull( configuration, nameof( configuration ) ); Arg.NotNull( apiVersion, nameof( apiVersion ) ); - var allRoutes = configuration.Routes; - var routes = new KeyValuePair[allRoutes.Count]; + var routes = configuration.Routes.ToDictionary(); var containers = configuration.GetRootContainerMappings(); - allRoutes.CopyTo( routes, 0 ); - foreach ( var route in routes ) { if ( !( route.Value is ODataRoute odataRoute ) ) diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs index b527c686..6e2dc63b 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs @@ -218,7 +218,7 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions() var routes = FlattenRoutes( Configuration.Routes ).ToArray(); - foreach ( var apiVersion in FlattenApiVersions() ) + foreach ( var apiVersion in FlattenApiVersions( controllerMappings ) ) { foreach ( var route in routes ) { @@ -441,15 +441,16 @@ static IEnumerable FlattenRoutes( IEnumerable routes ) } } - IEnumerable FlattenApiVersions() + IEnumerable FlattenApiVersions( IDictionary controllerMapping ) { + Contract.Requires( controllerMapping != null ); Contract.Ensures( Contract.Result>() != null ); var services = Configuration.Services; var assembliesResolver = services.GetAssembliesResolver(); var typeResolver = services.GetHttpControllerTypeResolver(); var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ); - var options = Configuration.GetApiVersioningOptions(); + var controllerDescriptors = controllerMapping.Values; var declared = new HashSet(); var supported = new HashSet(); var deprecated = new HashSet(); @@ -458,13 +459,12 @@ IEnumerable FlattenApiVersions() foreach ( var controllerType in controllerTypes ) { - var descriptor = new HttpControllerDescriptor() - { - Configuration = Configuration, - ControllerType = controllerType, - }; + var descriptor = FindControllerDescriptor( controllerDescriptors, controllerType ); - options.Conventions.ApplyTo( descriptor ); + if ( descriptor == null ) + { + continue; + } var model = descriptor.GetApiVersionModel(); @@ -494,13 +494,39 @@ IEnumerable FlattenApiVersions() if ( supported.Count == 0 ) { - supported.Add( options.DefaultApiVersion ); + supported.Add( Configuration.GetApiVersioningOptions().DefaultApiVersion ); return supported; } return supported.OrderBy( v => v ); } + static HttpControllerDescriptor FindControllerDescriptor( IEnumerable controllerDescriptors, Type controllerType ) + { + Contract.Requires( controllerDescriptors != null ); + Contract.Requires( controllerType != null ); + + foreach ( var controllerDescriptor in controllerDescriptors ) + { + if ( controllerDescriptor is IEnumerable groupedControllerDescriptors ) + { + foreach ( var groupedControllerDescriptor in groupedControllerDescriptors ) + { + if ( controllerType.Equals( groupedControllerDescriptor.ControllerType ) ) + { + return groupedControllerDescriptor; + } + } + } + else if ( controllerType.Equals( controllerDescriptor.ControllerType ) ) + { + return controllerDescriptor; + } + } + + return default; + } + static HttpControllerDescriptor GetDirectRouteController( CandidateAction[] directRouteCandidates, ApiVersion apiVersion ) { Contract.Requires( apiVersion != null ); @@ -1050,9 +1076,7 @@ static bool MatchRegexConstraint( IHttpRoute route, string parameterName, string } // note that we don't support custom constraint (IHttpRouteConstraint) because it might rely on the request and some runtime states - var constraintsRule = constraint as string; - - if ( constraintsRule == null ) + if ( !( constraint is string constraintsRule ) ) { return true; } diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs b/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs index df81b812..85b816a6 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs @@ -4,7 +4,6 @@ using Microsoft.Web.Http.Versioning; using System; using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Web.Http; @@ -19,32 +18,37 @@ public partial class ApiVersionActionSelector : IHttpActionSelector readonly object cacheKey = new object(); ActionSelectorCacheItem fastCache; - ActionSelectorCacheItem GetInternalSelector( HttpControllerDescriptor controllerDescriptor ) + /// + /// Selects and returns the action descriptor to invoke given the provided controller context. + /// + /// The current controller context. + /// The action descriptor that matches the current + /// controller context. + public virtual HttpActionDescriptor SelectAction( HttpControllerContext controllerContext ) { - Contract.Requires( controllerDescriptor != null ); - Contract.Ensures( Contract.Result() != null ); + Arg.NotNull( controllerContext, nameof( controllerContext ) ); + Contract.Ensures( Contract.Result() != null ); - if ( fastCache == null ) - { - var selector = new ActionSelectorCacheItem( controllerDescriptor ); - CompareExchange( ref fastCache, selector, null ); - return selector; - } - else if ( fastCache.HttpControllerDescriptor == controllerDescriptor ) - { - return fastCache; - } - else - { - if ( controllerDescriptor.Properties.TryGetValue( cacheKey, out var cacheValue ) ) - { - return (ActionSelectorCacheItem) cacheValue; - } + var internalSelector = GetInternalSelector( controllerContext.ControllerDescriptor ); + return internalSelector.SelectAction( controllerContext, SelectActionVersion ); + } - var selector = new ActionSelectorCacheItem( controllerDescriptor ); - controllerDescriptor.Properties.TryAdd( cacheKey, selector ); - return selector; - } + /// + /// Creates and returns an action descriptor mapping for the specified controller descriptor. + /// + /// The controller descriptor to create a mapping for. + /// A lookup, which represents the route-to-action mapping for the + /// specified controller descriptor. + public virtual ILookup GetActionMapping( HttpControllerDescriptor controllerDescriptor ) + { + Arg.NotNull( controllerDescriptor, nameof( controllerDescriptor ) ); + Contract.Ensures( Contract.Result>() != null ); + + var actionMappings = ( from descriptor in controllerDescriptor.AsEnumerable() + let selector = GetInternalSelector( descriptor ) + select selector.GetActionMapping() ).ToArray(); + + return actionMappings.Length == 1 ? actionMappings[0] : new AggregatedActionMapping( actionMappings ); } /// @@ -57,8 +61,6 @@ ActionSelectorCacheItem GetInternalSelector( HttpControllerDescriptor controller /// match is found. /// This method should return null if either no match is found or the matched action is /// ambiguous among the provided list of candidate actions. - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract." )] - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "Validated by a code contract." )] protected virtual HttpActionDescriptor SelectActionVersion( HttpControllerContext controllerContext, IReadOnlyList candidateActions ) { Arg.NotNull( controllerContext, nameof( controllerContext ) ); @@ -128,38 +130,32 @@ static Exception CreateAmbiguousActionException( IEnumerable - /// Selects and returns the action descriptor to invoke given the provided controller context. - /// - /// The current controller context. - /// The action descriptor that matches the current - /// controller context. - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract." )] - public virtual HttpActionDescriptor SelectAction( HttpControllerContext controllerContext ) - { - Arg.NotNull( controllerContext, nameof( controllerContext ) ); - Contract.Ensures( Contract.Result() != null ); - - var internalSelector = GetInternalSelector( controllerContext.ControllerDescriptor ); - return internalSelector.SelectAction( controllerContext, SelectActionVersion ); - } - - /// - /// Creates and returns an action descriptor mapping for the specified controller descriptor. - /// - /// The controller descriptor to create a mapping for. - /// A lookup, which represents the route-to-action mapping for the - /// specified controller descriptor. - public virtual ILookup GetActionMapping( HttpControllerDescriptor controllerDescriptor ) + ActionSelectorCacheItem GetInternalSelector( HttpControllerDescriptor controllerDescriptor ) { - Arg.NotNull( controllerDescriptor, nameof( controllerDescriptor ) ); - Contract.Ensures( Contract.Result>() != null ); + Contract.Requires( controllerDescriptor != null ); + Contract.Ensures( Contract.Result() != null ); - var actionMappings = ( from descriptor in controllerDescriptor.AsEnumerable() - let selector = GetInternalSelector( descriptor ) - select selector.GetActionMapping() ).ToArray(); + if ( fastCache == null ) + { + var selector = new ActionSelectorCacheItem( controllerDescriptor ); + CompareExchange( ref fastCache, selector, null ); + return selector; + } + else if ( fastCache.HttpControllerDescriptor == controllerDescriptor ) + { + return fastCache; + } + else + { + if ( controllerDescriptor.Properties.TryGetValue( cacheKey, out var cacheValue ) ) + { + return (ActionSelectorCacheItem) cacheValue; + } - return actionMappings.Length == 1 ? actionMappings[0] : new AggregatedActionMapping( actionMappings ); + var selector = new ActionSelectorCacheItem( controllerDescriptor ); + controllerDescriptor.Properties.TryAdd( cacheKey, selector ); + return selector; + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Controllers/HttpControllerDescriptorGroup.cs b/src/Microsoft.AspNet.WebApi.Versioning/Controllers/HttpControllerDescriptorGroup.cs index 46301dc6..15ce1617 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Controllers/HttpControllerDescriptorGroup.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Controllers/HttpControllerDescriptorGroup.cs @@ -4,7 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; - using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; using System.Linq; using System.Net.Http; using System.Web.Http; @@ -14,18 +14,19 @@ /// /// Represents a HTTP controller descriptor that is a grouped set of other HTTP controller descriptors. /// - [SuppressMessage( "Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Although the type is a collection, the term 'group' is more meaningful in this context." )] +#pragma warning disable CA1710 // Identifiers should have correct suffix public class HttpControllerDescriptorGroup : HttpControllerDescriptor, IReadOnlyList +#pragma warning restore CA1710 // Identifiers should have correct suffix { readonly HttpControllerDescriptor firstDescriptor; readonly IReadOnlyList descriptors; + readonly IReadOnlyDictionary controllerMapping; /// /// Initializes a new instance of the class. /// /// An array of /// HTTP controller descriptors. - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract." )] public HttpControllerDescriptorGroup( params HttpControllerDescriptor[] controllerDescriptors ) { Arg.NotNull( controllerDescriptors, nameof( controllerDescriptors ) ); @@ -33,6 +34,7 @@ public HttpControllerDescriptorGroup( params HttpControllerDescriptor[] controll firstDescriptor = controllerDescriptors[0]; descriptors = controllerDescriptors; + controllerMapping = MapApiVersionsToControllerDescriptors( descriptors ); } /// @@ -42,7 +44,6 @@ public HttpControllerDescriptorGroup( params HttpControllerDescriptor[] controll /// The name of the controller the controller descriptor represents. /// An array of /// HTTP controller descriptors. - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Justification = "Validated by a code contract." )] public HttpControllerDescriptorGroup( HttpConfiguration configuration, string controllerName, params HttpControllerDescriptor[] controllerDescriptors ) : base( configuration, controllerName, controllerDescriptors[0].ControllerType ) { @@ -53,6 +54,7 @@ public HttpControllerDescriptorGroup( HttpConfiguration configuration, string co firstDescriptor = controllerDescriptors[0]; descriptors = controllerDescriptors; + controllerMapping = MapApiVersionsToControllerDescriptors( descriptors ); } /// @@ -60,7 +62,6 @@ public HttpControllerDescriptorGroup( HttpConfiguration configuration, string co /// /// A read-only list of /// HTTP controller descriptors. - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract." )] public HttpControllerDescriptorGroup( IReadOnlyList controllerDescriptors ) { Arg.NotNull( controllerDescriptors, nameof( controllerDescriptors ) ); @@ -68,6 +69,7 @@ public HttpControllerDescriptorGroup( IReadOnlyList co firstDescriptor = controllerDescriptors[0]; descriptors = controllerDescriptors; + controllerMapping = MapApiVersionsToControllerDescriptors( descriptors ); } /// @@ -77,7 +79,6 @@ public HttpControllerDescriptorGroup( IReadOnlyList co /// The name of the controller the controller descriptor represents. /// A read-only list of /// HTTP controller descriptors. - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Justification = "Validated by a code contract." )] public HttpControllerDescriptorGroup( HttpConfiguration configuration, string controllerName, IReadOnlyList controllerDescriptors ) : base( configuration, controllerName, controllerDescriptors[0].ControllerType ) { @@ -88,6 +89,7 @@ public HttpControllerDescriptorGroup( HttpConfiguration configuration, string co firstDescriptor = controllerDescriptors[0]; descriptors = controllerDescriptors; + controllerMapping = MapApiVersionsToControllerDescriptors( descriptors ); } /// @@ -111,14 +113,12 @@ public override IHttpController CreateController( HttpRequestMessage request ) var version = request.GetRequestedApiVersion(); - if ( version == null ) + if ( version != null && controllerMapping.TryGetValue( version, out var descriptor ) ) { - return firstDescriptor.CreateController( request ); + return descriptor.CreateController( request ); } - var descriptor = descriptors.FirstOrDefault( d => d.GetDeclaredApiVersions().Contains( version ) ) ?? firstDescriptor; - - return descriptor.CreateController( request ); + return firstDescriptor.CreateController( request ); } /// @@ -180,5 +180,30 @@ public override Collection GetFilters() /// /// The total number of items in the group. public int Count => descriptors.Count; + + static Dictionary MapApiVersionsToControllerDescriptors( IReadOnlyList descriptors ) + { + Contract.Requires( descriptors != null ); + + if ( descriptors.Count < 2 ) + { + return default; + } + + var mapping = new Dictionary(); + + for ( var i = 0; i < descriptors.Count; i++ ) + { + var descriptor = descriptors[i]; + var apiVersions = descriptor.GetDeclaredApiVersions(); + + for ( var j = 0; j < apiVersions.Count; j++ ) + { + mapping[apiVersions[j]] = descriptor; + } + } + + return mapping; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs index b95f046f..61c006fe 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs @@ -2,6 +2,8 @@ { using Microsoft.Web.Http.Controllers; using Microsoft.Web.Http.Routing; + using Microsoft.Web.Http.Versioning; + using Microsoft.Web.Http.Versioning.Conventions; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -11,7 +13,6 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; - using Microsoft.Web.Http.Versioning; using static Controllers.HttpControllerDescriptorComparer; using static System.StringComparer; using static Versioning.ErrorCodes; @@ -52,10 +53,7 @@ public virtual IDictionary GetControllerMappin { Contract.Ensures( Contract.Result>() != null ); - var mapping = from pair in controllerInfoCache.Value - where pair.Value.Count > 0 - select pair; - + var mapping = controllerInfoCache.Value.Where( p => p.Value.Count > 0 ); return mapping.ToDictionary( p => p.Key, p => (HttpControllerDescriptor) p.Value, OrdinalIgnoreCase ); } @@ -71,32 +69,32 @@ public virtual HttpControllerDescriptor SelectController( HttpRequestMessage req EnsureRequestHasValidApiVersion( request ); - var aggregator = new ApiVersionControllerAggregator( request, GetControllerName, controllerInfoCache ); + var context = new ControllerSelectionContext( request, GetControllerName, controllerInfoCache ); var conventionRouteSelector = new ConventionRouteControllerSelector( options, controllerTypeCache ); var conventionRouteResult = default( ControllerSelectionResult ); - var exceptionFactory = new HttpResponseExceptionFactory( request, new Lazy( () => aggregator.AllVersions ) ); + var exceptionFactory = new HttpResponseExceptionFactory( request, new Lazy( () => context.AllVersions ) ); - if ( aggregator.RouteData == null ) + if ( context.RouteData == null ) { - conventionRouteResult = conventionRouteSelector.SelectController( aggregator ); + conventionRouteResult = conventionRouteSelector.SelectController( context ); if ( conventionRouteResult.Succeeded ) { return conventionRouteResult.Controller; } - throw exceptionFactory.NewNotFoundOrBadRequestException( conventionRouteResult, null ); + throw exceptionFactory.NewNotFoundOrBadRequestException( conventionRouteResult, default ); } var directRouteSelector = new DirectRouteControllerSelector( options ); - var directRouteResult = directRouteSelector.SelectController( aggregator ); + var directRouteResult = directRouteSelector.SelectController( context ); if ( directRouteResult.Succeeded ) { return directRouteResult.Controller; } - conventionRouteResult = conventionRouteSelector.SelectController( aggregator ); + conventionRouteResult = conventionRouteSelector.SelectController( context ); if ( conventionRouteResult.Succeeded ) { @@ -122,13 +120,42 @@ public virtual string GetControllerName( HttpRequestMessage request ) return null; } - routeData.Values.TryGetValue( RouteDataTokenKeys.Controller, out string controller ); + if ( routeData.Values.TryGetValue( RouteDataTokenKeys.Controller, out string controller ) ) + { + return controller; + } + + var configuration = request.GetConfiguration(); + var routes = configuration.Routes; + var context = request.GetRequestContext(); + var virtualPathRoot = routes.VirtualPathRoot; + + if ( context != null ) + { + virtualPathRoot = context.VirtualPathRoot ?? string.Empty; + } + + for ( var i = 0; i < routes.Count; i++ ) + { + var otherRouteData = routes[i].GetRouteData( virtualPathRoot, request ); + + if ( otherRouteData != null && + !routeData.Equals( otherRouteData ) && + otherRouteData.Values.TryGetValue( RouteDataTokenKeys.Controller, out controller ) ) + { + break; + } + } return controller; } ConcurrentDictionary InitializeControllerInfoCache() { + var options = configuration.GetApiVersioningOptions(); + var implicitVersionModel = new ApiVersionModel( options.DefaultApiVersion ); + var conventions = options.Conventions; + var actionSelector = configuration.Services.GetActionSelector(); var mapping = new ConcurrentDictionary( OrdinalIgnoreCase ); foreach ( var pair in controllerTypeCache.Cache ) @@ -140,13 +167,23 @@ ConcurrentDictionary InitializeController { foreach ( var type in grouping ) { - descriptors.Add( new HttpControllerDescriptor( configuration, key, type ) ); + var descriptor = new HttpControllerDescriptor( configuration, key, type ); + + if ( conventions.Count == 0 || !conventions.ApplyTo( descriptor ) ) + { + ApplyAttributeOrImplicitConventions( descriptor, actionSelector, implicitVersionModel ); + } + + descriptors.Add( descriptor ); } } descriptors.Sort( ByVersion ); - var descriptorGroup = new HttpControllerDescriptorGroup( configuration, key, descriptors.ToArray() ); + var descriptorGroup = + options.ReportApiVersions ? + new HttpControllerDescriptorGroup( configuration, key, ApplyCollatedModel( descriptors, actionSelector, CollateModel( descriptors ) ) ) : + new HttpControllerDescriptorGroup( configuration, key, descriptors.ToArray() ); mapping.TryAdd( key, descriptorGroup ); } @@ -154,6 +191,73 @@ ConcurrentDictionary InitializeController return mapping; } + static bool IsDecoratedWithAttributes( HttpControllerDescriptor controller ) + { + Contract.Requires( controller != null ); + + return controller.GetCustomAttributes().Count > 0 || + controller.GetCustomAttributes().Count > 0; + } + + static void ApplyImplicitConventions( HttpControllerDescriptor controller, IHttpActionSelector actionSelector, ApiVersionModel implicitVersionModel ) + { + Contract.Requires( controller != null ); + Contract.Requires( actionSelector != null ); + Contract.Requires( implicitVersionModel != null ); + + controller.SetProperty( implicitVersionModel ); + + var actions = actionSelector.GetActionMapping( controller ).SelectMany( g => g ); + + foreach ( var action in actions ) + { + action.SetProperty( implicitVersionModel ); + } + } + + static void ApplyAttributeOrImplicitConventions( HttpControllerDescriptor controller, IHttpActionSelector actionSelector, ApiVersionModel implicitVersionModel ) + { + Contract.Requires( controller != null ); + Contract.Requires( actionSelector != null ); + Contract.Requires( implicitVersionModel != null ); + + if ( IsDecoratedWithAttributes( controller ) ) + { + var conventions = new ControllerApiVersionConventionBuilder( controller.ControllerType ); + conventions.ApplyTo( controller ); + } + else + { + ApplyImplicitConventions( controller, actionSelector, implicitVersionModel ); + } + } + + static ApiVersionModel CollateModel( IEnumerable controllers ) => controllers.Select( c => c.GetApiVersionModel() ).Aggregate(); + + static HttpControllerDescriptor[] ApplyCollatedModel( List controllers, IHttpActionSelector actionSelector, ApiVersionModel collatedModel ) + { + Contract.Requires( controllers != null ); + Contract.Requires( actionSelector != null ); + Contract.Requires( collatedModel != null ); + Contract.Ensures( Contract.Result() != null ); + + foreach ( var controller in controllers ) + { + var model = controller.GetApiVersionModel(); + var actions = actionSelector.GetActionMapping( controller ).SelectMany( g => g ); + + controller.SetProperty( model.Aggregate( collatedModel ) ); + + foreach ( var action in actions ) + { + model = action.GetApiVersionModel(); + action.SetProperty( model.Aggregate( collatedModel ) ); + } + } + + return controllers.ToArray(); + } + static void EnsureRequestHasValidApiVersion( HttpRequestMessage request ) { Contract.Requires( request != null ); diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerAggregator.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ControllerSelectionContext.cs similarity index 85% rename from src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerAggregator.cs rename to src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ControllerSelectionContext.cs index 707ad232..72ea8a3a 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerAggregator.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ControllerSelectionContext.cs @@ -2,6 +2,7 @@ { using Microsoft.Web.Http.Controllers; using Microsoft.Web.Http.Routing; + using Microsoft.Web.Http.Versioning; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -11,10 +12,9 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Routing; - using Microsoft.Web.Http.Versioning; using static System.StringComparison; - sealed class ApiVersionControllerAggregator + sealed class ControllerSelectionContext { readonly Lazy controllerName; readonly Lazy> controllerInfoCache; @@ -22,7 +22,7 @@ sealed class ApiVersionControllerAggregator readonly Lazy directRouteCandidates; readonly Lazy allVersions; - internal ApiVersionControllerAggregator( + internal ControllerSelectionContext( HttpRequestMessage request, Func controllerName, Lazy> controllerInfoCache ) @@ -36,27 +36,9 @@ internal ApiVersionControllerAggregator( RouteData = request.GetRouteData(); conventionRouteCandidates = new Lazy( GetConventionRouteCandidates ); directRouteCandidates = new Lazy( () => RouteData?.GetDirectRouteCandidates() ); - allVersions = new Lazy( AggregateAllCandiateVersions ); - } - - HttpControllerDescriptorGroup GetConventionRouteCandidates() - { - if ( string.IsNullOrEmpty( ControllerName ) ) - { - return null; - } - - if ( controllerInfoCache.Value.TryGetValue( ControllerName, out var candidates ) ) - { - return candidates; - } - - return null; + allVersions = new Lazy( CreateAggregatedModel ); } - ApiVersionModel AggregateAllCandiateVersions() => - ( ConventionRouteCandidates ?? Enumerable.Empty() ).Union( EnumerateDirectRoutes() ).AggregateVersions(); - internal HttpRequestMessage Request { get; } internal IHttpRouteData RouteData { get; } @@ -75,6 +57,36 @@ ApiVersionModel AggregateAllCandiateVersions() => internal ApiVersionModel AllVersions => allVersions.Value; + static bool RouteTemplatesIntersect( string template1, string template2 ) => + template1.StartsWith( template2, OrdinalIgnoreCase ) || template2.StartsWith( template1, OrdinalIgnoreCase ); + + static IEnumerable EnumerateControllersInDataTokens( IDictionary dataTokens ) + { + Contract.Requires( dataTokens != null ); + Contract.Ensures( Contract.Result>() != null ); + + if ( dataTokens.TryGetValue( RouteDataTokenKeys.Controller, out var value ) ) + { + if ( value is HttpControllerDescriptor controllerDescriptor ) + { + yield return controllerDescriptor; + } + + yield break; + } + + if ( dataTokens.TryGetValue( RouteDataTokenKeys.Actions, out value ) ) + { + if ( value is HttpActionDescriptor[] actionDescriptors ) + { + foreach ( var actionDescriptor in actionDescriptors ) + { + yield return actionDescriptor.ControllerDescriptor; + } + } + } + } + IEnumerable EnumerateDirectRoutes() { Contract.Ensures( Contract.Result>() != null ); @@ -109,34 +121,24 @@ from controller in EnumerateControllersInDataTokens( route.DataTokens ) return controllers.Distinct(); } - static bool RouteTemplatesIntersect( string template1, string template2 ) => - template1.StartsWith( template2, OrdinalIgnoreCase ) || template2.StartsWith( template1, OrdinalIgnoreCase ); + HttpControllerDescriptorGroup GetConventionRouteCandidates() => + !string.IsNullOrEmpty( ControllerName ) && controllerInfoCache.Value.TryGetValue( ControllerName, out var candidates ) ? candidates : default; - static IEnumerable EnumerateControllersInDataTokens( IDictionary dataTokens ) + ApiVersionModel CreateAggregatedModel() { - Contract.Requires( dataTokens != null ); - Contract.Ensures( Contract.Result>() != null ); + var models = Enumerable.Empty(); - if ( dataTokens.TryGetValue( RouteDataTokenKeys.Controller, out var value ) ) + if ( HasConventionBasedRoutes ) { - if ( value is HttpControllerDescriptor controllerDescriptor ) - { - yield return controllerDescriptor; - } - - yield break; + models = models.Union( ConventionRouteCandidates.Select( c => c.GetProperty() ).Where( m => m != null ) ); } - if ( dataTokens.TryGetValue( RouteDataTokenKeys.Actions, out value ) ) + if ( HasAttributeBasedRoutes ) { - if ( value is HttpActionDescriptor[] actionDescriptors ) - { - foreach ( var actionDescriptor in actionDescriptors ) - { - yield return actionDescriptor.ControllerDescriptor; - } - } + models = models.Union( DirectRouteCandidates.Select( c => c.ActionDescriptor.GetProperty() ).Where( m => m != null ) ); } + + return models.Aggregate(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ControllerSelector.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ControllerSelector.cs index b65c7ee0..b1982a32 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ControllerSelector.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ControllerSelector.cs @@ -17,6 +17,6 @@ protected ControllerSelector( ApiVersioningOptions options ) protected IApiVersionSelector ApiVersionSelector => options.ApiVersionSelector; - internal abstract ControllerSelectionResult SelectController( ApiVersionControllerAggregator aggregator ); + internal abstract ControllerSelectionResult SelectController( ControllerSelectionContext context ); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ConventionRouteControllerSelector.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ConventionRouteControllerSelector.cs index 42b1c3cf..f1be5e50 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ConventionRouteControllerSelector.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ConventionRouteControllerSelector.cs @@ -1,41 +1,36 @@ namespace Microsoft.Web.Http.Dispatcher { + using Microsoft.Web.Http.Versioning; using System; using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Text; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Routing; - using Microsoft.Web.Http.Versioning; using static System.Environment; sealed class ConventionRouteControllerSelector : ControllerSelector { readonly HttpControllerTypeCache controllerTypeCache; - internal ConventionRouteControllerSelector( ApiVersioningOptions options, HttpControllerTypeCache controllerTypeCache ) : base( options ) - { - Contract.Requires( controllerTypeCache != null ); - this.controllerTypeCache = controllerTypeCache; - } + internal ConventionRouteControllerSelector( ApiVersioningOptions options, HttpControllerTypeCache controllerTypeCache ) + : base( options ) => this.controllerTypeCache = controllerTypeCache; - [SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Handled by the caller." )] - internal override ControllerSelectionResult SelectController( ApiVersionControllerAggregator aggregator ) + internal override ControllerSelectionResult SelectController( ControllerSelectionContext context ) { - Contract.Requires( aggregator != null ); + Contract.Requires( context != null ); Contract.Ensures( Contract.Result() != null ); - var request = aggregator.Request; - var requestedVersion = aggregator.RequestedApiVersion; - var controllerName = aggregator.ControllerName; + var request = context.Request; + var requestedVersion = context.RequestedApiVersion; + var controllerName = context.ControllerName; var result = new ControllerSelectionResult() { RequestedVersion = requestedVersion, ControllerName = controllerName, - HasCandidates = aggregator.HasConventionBasedRoutes, + HasCandidates = context.HasConventionBasedRoutes, }; if ( !result.HasCandidates ) @@ -43,8 +38,8 @@ internal override ControllerSelectionResult SelectController( ApiVersionControll return result; } - var ambiguousException = new Lazy( () => CreateAmbiguousControllerException( aggregator.RouteData.Route, controllerName, controllerTypeCache.GetControllerTypes( controllerName ) ) ); - var versionNeutralController = result.Controller = GetVersionNeutralController( aggregator.ConventionRouteCandidates, ambiguousException ); + var ambiguousException = new Lazy( () => CreateAmbiguousControllerException( context.RouteData.Route, controllerName, controllerTypeCache.GetControllerTypes( controllerName ) ) ); + var versionNeutralController = result.Controller = GetVersionNeutralController( context.ConventionRouteCandidates, ambiguousException ); if ( requestedVersion == null ) { @@ -53,7 +48,7 @@ internal override ControllerSelectionResult SelectController( ApiVersionControll return result; } - requestedVersion = ApiVersionSelector.SelectVersion( request, aggregator.AllVersions ); + requestedVersion = ApiVersionSelector.SelectVersion( request, context.AllVersions ); if ( requestedVersion == null ) { @@ -61,7 +56,7 @@ internal override ControllerSelectionResult SelectController( ApiVersionControll } } - var versionedController = GetVersionedController( aggregator, requestedVersion, ambiguousException ); + var versionedController = GetVersionedController( context, requestedVersion, ambiguousException ); if ( versionedController == null ) { @@ -74,6 +69,7 @@ internal override ControllerSelectionResult SelectController( ApiVersionControll } request.ApiVersionProperties().RequestedApiVersion = requestedVersion; + result.RequestedVersion = requestedVersion; result.Controller = versionedController; return result; @@ -109,13 +105,13 @@ static HttpControllerDescriptor GetVersionNeutralController( IEnumerable ambiguousException ) + static HttpControllerDescriptor GetVersionedController( ControllerSelectionContext context, ApiVersion requestedVersion, Lazy ambiguousException ) { - Contract.Requires( aggregator != null ); + Contract.Requires( context != null ); Contract.Requires( requestedVersion != null ); Contract.Requires( ambiguousException != null ); - var candidates = aggregator.ConventionRouteCandidates; + var candidates = context.ConventionRouteCandidates; var controller = candidates[0]; if ( candidates.Count == 1 ) @@ -133,11 +129,6 @@ static HttpControllerDescriptor GetVersionedController( ApiVersionControllerAggr } } - if ( !controller.HasApiVersionInfo() ) - { - controller.SetApiVersionModel( aggregator.AllVersions ); - } - controller.SetRelatedCandidates( candidates ); return controller; } diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/DirectRouteControllerSelector.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/DirectRouteControllerSelector.cs index cf68d79e..5f14e237 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/DirectRouteControllerSelector.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/DirectRouteControllerSelector.cs @@ -15,16 +15,16 @@ sealed class DirectRouteControllerSelector : ControllerSelector { internal DirectRouteControllerSelector( ApiVersioningOptions options ) : base( options ) { } - internal override ControllerSelectionResult SelectController( ApiVersionControllerAggregator aggregator ) + internal override ControllerSelectionResult SelectController( ControllerSelectionContext context ) { - Contract.Requires( aggregator != null ); + Contract.Requires( context != null ); Contract.Ensures( Contract.Result() != null ); - var request = aggregator.Request; - var requestedVersion = aggregator.RequestedApiVersion; + var request = context.Request; + var requestedVersion = context.RequestedApiVersion; var result = new ControllerSelectionResult() { - HasCandidates = aggregator.HasAttributeBasedRoutes, + HasCandidates = context.HasAttributeBasedRoutes, RequestedVersion = requestedVersion, }; @@ -33,7 +33,7 @@ internal override ControllerSelectionResult SelectController( ApiVersionControll return result; } - var versionNeutralController = result.Controller = GetVersionNeutralController( aggregator.DirectRouteCandidates ); + var versionNeutralController = result.Controller = GetVersionNeutralController( context.DirectRouteCandidates ); if ( requestedVersion == null ) { @@ -42,7 +42,7 @@ internal override ControllerSelectionResult SelectController( ApiVersionControll return result; } - requestedVersion = ApiVersionSelector.SelectVersion( request, aggregator.AllVersions ); + requestedVersion = ApiVersionSelector.SelectVersion( request, context.AllVersions ); if ( requestedVersion == null ) { @@ -50,7 +50,7 @@ internal override ControllerSelectionResult SelectController( ApiVersionControll } } - var versionedController = GetVersionedController( aggregator, requestedVersion ); + var versionedController = GetVersionedController( context, requestedVersion ); if ( versionedController == null ) { @@ -99,12 +99,12 @@ static HttpControllerDescriptor GetVersionNeutralController( CandidateAction[] d return controllerDescriptor; } - static HttpControllerDescriptor GetVersionedController( ApiVersionControllerAggregator aggregator, ApiVersion requestedVersion ) + static HttpControllerDescriptor GetVersionedController( ControllerSelectionContext context, ApiVersion requestedVersion ) { - Contract.Requires( aggregator != null ); + Contract.Requires( context != null ); Contract.Requires( requestedVersion != null ); - var directRouteCandidates = aggregator.DirectRouteCandidates; + var directRouteCandidates = context.DirectRouteCandidates; var controller = directRouteCandidates[0].ActionDescriptor.ControllerDescriptor; if ( directRouteCandidates.Length == 1 ) @@ -122,11 +122,6 @@ static HttpControllerDescriptor GetVersionedController( ApiVersionControllerAggr } } - if ( !controller.HasApiVersionInfo() ) - { - controller.SetApiVersionModel( aggregator.AllVersions ); - } - return controller; } diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpControllerTypeCache.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpControllerTypeCache.cs index 7c80ad77..ed32cc25 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpControllerTypeCache.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/HttpControllerTypeCache.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Reflection; using System.Web.Http; - using System.Web.Http.Dispatcher; + using static System.Web.Http.Dispatcher.DefaultHttpControllerSelector; sealed class HttpControllerTypeCache { @@ -36,11 +36,39 @@ static string GetControllerName( Type type ) return attribute.Name; } - // use standard convention for the controller name + // use standard convention for the controller name (ex: ValuesController -> Values) var name = type.Name; - var suffixLength = DefaultHttpControllerSelector.ControllerSuffix.Length; + var suffixLength = ControllerSuffix.Length; - return name.Substring( 0, name.Length - suffixLength ); + name = name.Substring( 0, name.Length - suffixLength ); + + // trim trailing numbers to enable grouping by convention (ex: Values1Controller -> Values, Values2Controller -> Values) + return TrimTrailingNumbers( name ); + } + + static string TrimTrailingNumbers( string name ) + { + if ( string.IsNullOrEmpty( name ) ) + { + return name; + } + + var last = name.Length - 1; + + for ( var i = last; i >= 0; i-- ) + { + if ( !char.IsNumber( name[i] ) ) + { + if ( i < last ) + { + return name.Substring( 0, i + 1 ); + } + + return name; + } + } + + return name; } Dictionary> InitializeCache() diff --git a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs index b9c2349b..ac50b15a 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs @@ -1,12 +1,11 @@ namespace System.Web.Http { - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Diagnostics.Contracts; - using System.Web.Http.Controllers; using Microsoft; using Microsoft.Web.Http; using Microsoft.Web.Http.Versioning; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Web.Http.Controllers; /// /// Provides extension methods for the class. @@ -14,33 +13,20 @@ public static class HttpActionDescriptorExtensions { const string AttributeRoutedPropertyKey = "MS_IsAttributeRouted"; - const string ApiVersionInfoKey = "MS_ApiVersionInfo"; - - internal static bool IsAttributeRouted( this HttpActionDescriptor actionDescriptor ) - { - Contract.Requires( actionDescriptor != null ); - - actionDescriptor.Properties.TryGetValue( AttributeRoutedPropertyKey, out bool? value ); - return value ?? false; - } /// /// Gets the API version information associated with a action. /// /// The action to evaluate. /// The API version information for the action. - [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract." )] public static ApiVersionModel GetApiVersionModel( this HttpActionDescriptor actionDescriptor ) { Arg.NotNull( actionDescriptor, nameof( actionDescriptor ) ); Contract.Ensures( Contract.Result() != null ); - return (ApiVersionModel) actionDescriptor.Properties.GetOrAdd( ApiVersionInfoKey, key => new ApiVersionModel( actionDescriptor ) ); + return actionDescriptor.GetProperty() ?? new ApiVersionModel( actionDescriptor ); } - internal static void SetApiVersionModel( this HttpActionDescriptor actionDescriptor, ApiVersionModel model ) => - actionDescriptor.Properties.AddOrUpdate( ApiVersionInfoKey, model, ( key, value ) => model ); - /// /// Gets a value indicating whether the action is API version neutral. /// @@ -55,5 +41,24 @@ internal static void SetApiVersionModel( this HttpActionDescriptor actionDescrip /// A read-only list of API versions /// declared by the action. public static IReadOnlyList GetApiVersions( this HttpActionDescriptor actionDescriptor ) => actionDescriptor.GetApiVersionModel().DeclaredApiVersions; + + internal static bool IsAttributeRouted( this HttpActionDescriptor actionDescriptor ) + { + Contract.Requires( actionDescriptor != null ); + + actionDescriptor.Properties.TryGetValue( AttributeRoutedPropertyKey, out bool? value ); + return value ?? false; + } + + internal static T GetProperty( this HttpActionDescriptor actionDescriptor ) => + actionDescriptor.Properties.TryGetValue( typeof( T ), out T value ) ? value : default; + + internal static void SetProperty( this HttpActionDescriptor actionDescriptor, T value ) => + actionDescriptor.Properties.AddOrUpdate( typeof( T ), value, ( key, oldValue ) => value ); + +#pragma warning disable CA1801 // Review unused parameters; intentional for type parameter + internal static void RemoveProperty( this HttpActionDescriptor actionDescriptor, T value ) => + actionDescriptor.Properties.TryRemove( typeof( T ), out _ ); +#pragma warning restore CA1801 } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs index 69825e19..a5851a4e 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs @@ -1,12 +1,11 @@ namespace System.Web.Http { - using System.Collections.Generic; - using System.Diagnostics.Contracts; - using System.Linq; - using System.Web.Http.Controllers; using Microsoft; using Microsoft.Web.Http; using Microsoft.Web.Http.Versioning; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Web.Http.Controllers; /// /// Provides extension methods for the class. @@ -14,44 +13,8 @@ public static class HttpControllerDescriptorExtensions { const string AttributeRoutedPropertyKey = "MS_IsAttributeRouted"; - const string ApiVersionInfoKey = "MS_ApiVersionInfo"; - const string ConventionsApiVersionInfoKey = "MS_ConventionsApiVersionInfo"; const string RelatedControllerCandidatesKey = "MS_RelatedControllerCandidates"; - internal static bool IsAttributeRouted( this HttpControllerDescriptor controllerDescriptor ) - { - Contract.Requires( controllerDescriptor != null ); - - controllerDescriptor.Properties.TryGetValue( AttributeRoutedPropertyKey, out bool? value ); - return value ?? false; - } - - internal static bool HasApiVersionInfo( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.Properties.ContainsKey( ApiVersionInfoKey ); - - internal static ApiVersionModel AggregateVersions( this IEnumerable controllerDescriptors ) - { - Contract.Requires( controllerDescriptors != null ); - Contract.Ensures( Contract.Result() != null ); - - using ( var iterator = controllerDescriptors.GetEnumerator() ) - { - if ( !iterator.MoveNext() ) - { - return ApiVersionModel.Empty; - } - - var version = iterator.Current.GetApiVersionModel(); - var otherVersions = new List(); - - while ( iterator.MoveNext() ) - { - otherVersions.Add( iterator.Current.GetApiVersionModel() ); - } - - return version.Aggregate( otherVersions ); - } - } - /// /// Gets the API version information associated with a controller. /// @@ -61,46 +24,61 @@ public static ApiVersionModel GetApiVersionModel( this HttpControllerDescriptor { Arg.NotNull( controllerDescriptor, nameof( controllerDescriptor ) ); Contract.Ensures( Contract.Result() != null ); + return controllerDescriptor.GetProperty() ?? new ApiVersionModel( controllerDescriptor ); + } - var properties = controllerDescriptor.Properties; + /// + /// Gets a value indicating whether the controller is API version neutral. + /// + /// The controller to evaluate. + /// True if the controller is API version neutral (e.g. "unaware"); otherwise, false. + public static bool IsApiVersionNeutral( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().IsApiVersionNeutral; - if ( properties.TryGetValue( ApiVersionInfoKey, out ApiVersionModel versionInfo ) ) - { - return versionInfo; - } + /// + /// Gets the API versions declared by the controller. + /// + /// The controller to evaluate. + /// A read-only list of API versions + /// declared by the controller. + /// The declared API versions are constrained to the versions declared explicitly by the specified controller. + public static IReadOnlyList GetDeclaredApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().DeclaredApiVersions; - var options = controllerDescriptor.Configuration.GetApiVersioningOptions(); + /// + /// Gets the API versions implemented by the controller. + /// + /// The controller to evaluate. + /// A read-only list of API versions + /// implemented by the controller. + /// The implemented API versions include the supported and deprecated API versions. + public static IReadOnlyList GetImplementedApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().ImplementedApiVersions; - if ( options.Conventions.Count == 0 ) - { - return new ApiVersionModel( controllerDescriptor ); - } + /// + /// Gets the API versions supported by the controller. + /// + /// The controller to evaluate. + /// A read-only list of API versions + /// supported by the controller. + public static IReadOnlyList GetSupportedApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().SupportedApiVersions; - options.Conventions.ApplyTo( controllerDescriptor ); - return properties.TryGetValue( ConventionsApiVersionInfoKey, out versionInfo ) ? versionInfo : new ApiVersionModel( controllerDescriptor ); - } + /// + /// Gets the API versions deprecated by the controller. + /// + /// The controller to evaluate. + /// A read-only list of API versions + /// deprecated by the controller. + /// A deprecated API version does not mean it is not supported by the controller. A deprecated API + /// version is typically advertised six months or more before it becomes unsupported; in which case, the + /// controller would no longer indicate that it is an implemented version. + public static IReadOnlyList GetDeprecatedApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().DeprecatedApiVersions; - internal static void SetApiVersionModel( this HttpControllerDescriptor controllerDescriptor, ApiVersionModel model ) + internal static bool IsAttributeRouted( this HttpControllerDescriptor controllerDescriptor ) { - var properties = controllerDescriptor.Properties; - - properties.AddOrUpdate( - ApiVersionInfoKey, - key => - { - if ( properties.TryRemove( ConventionsApiVersionInfoKey, out var value ) ) - { - return ( (ApiVersionModel) value ).Aggregate( model ); - } + Contract.Requires( controllerDescriptor != null ); - return new ApiVersionModel( controllerDescriptor, model ); - }, - ( key, value ) => ( (ApiVersionModel) value ).Aggregate( model ) ); + controllerDescriptor.Properties.TryGetValue( AttributeRoutedPropertyKey, out bool? value ); + return value ?? false; } - internal static void SetConventionsApiVersionModel( this HttpControllerDescriptor controllerDescriptor, ApiVersionModel model ) => - controllerDescriptor.Properties.AddOrUpdate( ConventionsApiVersionInfoKey, model, ( key, currentModel ) => ( (ApiVersionModel) currentModel ).Aggregate( model ) ); - internal static void SetRelatedCandidates( this HttpControllerDescriptor controllerDescriptor, IEnumerable value ) => controllerDescriptor.Properties.AddOrUpdate( RelatedControllerCandidatesKey, value, ( key, oldValue ) => value ); @@ -141,48 +119,31 @@ internal static IEnumerable AsEnumerable( this HttpCon } } - /// - /// Gets a value indicating whether the controller is API version neutral. - /// - /// The controller to evaluate. - /// True if the controller is API version neutral (e.g. "unaware"); otherwise, false. - public static bool IsApiVersionNeutral( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().IsApiVersionNeutral; + internal static T GetProperty( this HttpControllerDescriptor controllerDescriptor ) + { + Contract.Requires( controllerDescriptor != null ); - /// - /// Gets the API versions declared by the controller. - /// - /// The controller to evaluate. - /// A read-only list of API versions - /// declared by the controller. - /// The declared API versions are constrained to the versions declared explicitly by the specified controller. - public static IReadOnlyList GetDeclaredApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().DeclaredApiVersions; + if ( controllerDescriptor.Properties.TryGetValue( typeof( T ), out T value ) ) + { + return value; + } - /// - /// Gets the API versions implemented by the controller. - /// - /// The controller to evaluate. - /// A read-only list of API versions - /// implemented by the controller. - /// The implemented API versions include the supported and deprecated API versions. - public static IReadOnlyList GetImplementedApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().ImplementedApiVersions; + return default; + } - /// - /// Gets the API versions supported by the controller. - /// - /// The controller to evaluate. - /// A read-only list of API versions - /// supported by the controller. - public static IReadOnlyList GetSupportedApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().SupportedApiVersions; + internal static void SetProperty( this HttpControllerDescriptor controllerDescriptor, T value ) + { + Contract.Requires( controllerDescriptor != null ); - /// - /// Gets the API versions deprecated by the controller. - /// - /// The controller to evaluate. - /// A read-only list of API versions - /// deprecated by the controller. - /// A deprecated API version does not mean it is not supported by the controller. A deprecated API - /// version is typically advertised six months or more before it becomes unsupported; in which case, the - /// controller would no longer indicate that it is an implemented version. - public static IReadOnlyList GetDeprecatedApiVersions( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetApiVersionModel().DeprecatedApiVersions; + controllerDescriptor.Properties.AddOrUpdate( typeof( T ), value, ( key, oldValue ) => value ); + + if ( controllerDescriptor is IEnumerable groupedControllerDescriptors ) + { + foreach ( var groupedControllerDescriptor in groupedControllerDescriptors ) + { + groupedControllerDescriptor.Properties.AddOrUpdate( typeof( T ), value, ( key, oldValue ) => value ); + } + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpRouteCollectionExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpRouteCollectionExtensions.cs new file mode 100644 index 00000000..dec52c8d --- /dev/null +++ b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpRouteCollectionExtensions.cs @@ -0,0 +1,104 @@ +namespace System.Web.Http +{ + using Microsoft; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Reflection; + using System.Web.Http.Routing; + using static System.Reflection.BindingFlags; + + /// + /// Provides extension methods for the . + /// + public static class HttpRouteCollectionExtensions + { + /// + /// Returns the route collection as a read-only dictionary mapping configured names to routes. + /// + /// The route collection to convert. + /// A new read-only dictonary of + /// routes mapped to their name. + public static IReadOnlyDictionary ToDictionary( this HttpRouteCollection routes ) + { + Arg.NotNull( routes, nameof( routes ) ); + Contract.Ensures( Contract.Result>() != null ); + + const string HostedHttpRouteCollection = "System.Web.Http.WebHost.Routing.HostedHttpRouteCollection"; + + try + { + return routes.CopyToDictionary(); + } + catch ( NotSupportedException ) when ( routes.GetType().FullName == HostedHttpRouteCollection ) + { + return routes.BuildDictionaryFromKeys(); + } + } + + static IReadOnlyDictionary CopyToDictionary( this HttpRouteCollection routes ) + { + Contract.Requires( routes != null ); + Contract.Ensures( Contract.Result>() != null ); + + var items = new KeyValuePair[routes.Count]; + + routes.CopyTo( items, 0 ); + + var dictionary = new Dictionary( routes.Count, StringComparer.OrdinalIgnoreCase ); + + for ( var i = 0; i < items.Length; i++ ) + { + var item = items[i]; + dictionary[item.Key] = item.Value; + } + + return dictionary; + } + + static IReadOnlyDictionary BuildDictionaryFromKeys( this HttpRouteCollection routes ) + { + Contract.Requires( routes != null ); + Contract.Ensures( Contract.Result>() != null ); + + var keys = routes.Keys(); + var dictionary = new Dictionary( routes.Count, StringComparer.OrdinalIgnoreCase ); + + for ( var i = 0; i < keys.Count; i++ ) + { + var key = keys[i]; + dictionary[key] = routes[key]; + } + + return dictionary; + } + + static IReadOnlyList Keys( this HttpRouteCollection routes ) + { + Contract.Requires( routes != null ); + Contract.Ensures( Contract.Result>() != null ); + + var collection = GetDictionaryKeys( routes ); + var keys = new string[collection.Count]; + + collection.CopyTo( keys, 0 ); + + return keys; + } + + static ICollection GetDictionaryKeys( HttpRouteCollection routes ) + { + Contract.Requires( routes != null ); + Contract.Ensures( Contract.Result>() != null ); + + // HACK: System.Web.Routing.RouteCollection doesn't expose the names associated with registered routes. The + // HostedHttpRouteCollection could have provided an adapter to support it, but didn't. Instead, it always throws + // NotSupportedException for the HttpRouteCollection.CopyTo method. This only happens when hosted on IIS. The + // only way to get the keys is use reflection to poke at the underlying dictionary. + var routeCollection = routes.GetType().GetField( "_routeCollection", Instance | NonPublic ).GetValue( routes ); + var dictionary = routeCollection.GetType().GetField( "_namedMap", Instance | NonPublic ).GetValue( routeCollection ); + var keys = (ICollection) dictionary.GetType().GetRuntimeProperty( "Keys" ).GetValue( dictionary, null ); + + return keys; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/TupleExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/TupleExtensions.cs new file mode 100644 index 00000000..2f57fbc7 --- /dev/null +++ b/src/Microsoft.AspNet.WebApi.Versioning/TupleExtensions.cs @@ -0,0 +1,18 @@ +namespace Microsoft.Web.Http +{ + using System; + using System.Diagnostics.Contracts; + + internal static class TupleExtensions + { + public static void Deconstruct( this Tuple tuple, out T1 item1, out T2 item2, out T3 item3, out T4 item4 ) + { + Contract.Requires( tuple != null ); + + item1 = tuple.Item1; + item2 = tuple.Item2; + item3 = tuple.Item3; + item4 = tuple.Item4; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionModel.cs b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionModel.cs index 59e522bd..b6612c8c 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionModel.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionModel.cs @@ -44,27 +44,6 @@ public ApiVersionModel( HttpControllerDescriptor controllerDescriptor ) } } - internal ApiVersionModel( HttpControllerDescriptor controllerDescriptor, ApiVersionModel aggregatedVersions ) - { - Contract.Requires( controllerDescriptor != null ); - Contract.Requires( aggregatedVersions != null ); - - if ( IsApiVersionNeutral = controllerDescriptor.GetCustomAttributes( false ).Any() ) - { - declaredVersions = emptyVersions; - implementedVersions = emptyVersions; - supportedVersions = emptyVersions; - deprecatedVersions = emptyVersions; - } - else - { - declaredVersions = new Lazy>( () => GetDeclaredControllerApiVersions( controllerDescriptor ) ); - implementedVersions = aggregatedVersions.implementedVersions; - supportedVersions = aggregatedVersions.supportedVersions; - deprecatedVersions = aggregatedVersions.deprecatedVersions; - } - } - /// /// Initializes a new instance of the class. /// @@ -73,7 +52,9 @@ public ApiVersionModel( HttpActionDescriptor actionDescriptor ) { Arg.NotNull( actionDescriptor, nameof( actionDescriptor ) ); - if ( IsApiVersionNeutral = actionDescriptor.ControllerDescriptor.GetCustomAttributes( false ).Any() ) + var controllerModel = actionDescriptor.ControllerDescriptor.GetApiVersionModel(); + + if ( IsApiVersionNeutral = controllerModel.IsApiVersionNeutral ) { declaredVersions = emptyVersions; implementedVersions = emptyVersions; @@ -83,9 +64,9 @@ public ApiVersionModel( HttpActionDescriptor actionDescriptor ) else { declaredVersions = new Lazy>( actionDescriptor.GetCustomAttributes( false ).GetImplementedApiVersions ); - implementedVersions = declaredVersions; - supportedVersions = new Lazy>( actionDescriptor.GetCustomAttributes( false ).GetSupportedApiVersions ); - deprecatedVersions = new Lazy>( actionDescriptor.GetCustomAttributes( false ).GetDeprecatedApiVersions ); + implementedVersions = new Lazy>( () => controllerModel.ImplementedApiVersions ); + supportedVersions = new Lazy>( () => controllerModel.SupportedApiVersions ); + deprecatedVersions = new Lazy>( () => controllerModel.DeprecatedApiVersions ); } } diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs index 478fb3ca..cb2c1bb0 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs @@ -1,6 +1,7 @@ namespace Microsoft.Web.Http.Versioning.Conventions { using System; + using System.Collections.Generic; using System.Linq; using System.Web.Http; using System.Web.Http.Controllers; @@ -19,20 +20,25 @@ public virtual void ApplyTo( HttpActionDescriptor actionDescriptor ) { Arg.NotNull( actionDescriptor, nameof( actionDescriptor ) ); - mappedVersions.UnionWith( from provider in actionDescriptor.GetCustomAttributes() - where !provider.AdvertiseOnly && !provider.Deprecated - from version in provider.Versions - select version ); + MappedVersions.AddRange( from provider in actionDescriptor.GetCustomAttributes() + where !provider.AdvertiseOnly && !provider.Deprecated + from version in provider.Versions + select version ); - var noVersions = Enumerable.Empty(); - var model = new ApiVersionModel( - apiVersionNeutral: false, - supported: mappedVersions, - deprecated: noVersions, - advertised: noVersions, - deprecatedAdvertised: noVersions ); + var (supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions) = + actionDescriptor.GetProperty, + IEnumerable, + IEnumerable, + IEnumerable>>(); - actionDescriptor.SetApiVersionModel( model ); + var versionModel = new ApiVersionModel( + declaredVersions: MappedVersions, + supportedVersions, + deprecatedVersions, + advertisedVersions, + deprecatedAdvertisedVersions ); + + actionDescriptor.SetProperty( versionModel ); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs index 0eaa55d5..21345edf 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs @@ -1,6 +1,7 @@ namespace Microsoft.Web.Http.Versioning.Conventions { using System; + using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -20,13 +21,7 @@ public partial class ControllerApiVersionConventionBuilderBase : IApiVersionConv public virtual void ApplyTo( HttpControllerDescriptor controllerDescriptor ) { Arg.NotNull( controllerDescriptor, nameof( controllerDescriptor ) ); - - ApplyControllerConventions( controllerDescriptor ); - - if ( HasActionConventions ) - { - ApplyActionConventions( controllerDescriptor ); - } + ApplyActionConventions( controllerDescriptor, ApplyControllerConventions( controllerDescriptor ) ); } /// @@ -43,23 +38,39 @@ public virtual void ApplyTo( HttpControllerDescriptor controllerDescriptor ) /// True if the convention was successfully retrieved; otherwise, false. protected abstract bool TryGetConvention( MethodInfo method, out IApiVersionConvention convention ); - void ApplyControllerConventions( HttpControllerDescriptor controllerDescriptor ) + static void ApplyNeutralModelToActions( HttpControllerDescriptor controller ) + { + Contract.Requires( controller != null ); + + var actionSelector = controller.Configuration.Services.GetActionSelector(); + var actions = actionSelector.GetActionMapping( controller ).SelectMany( g => g ); + + foreach ( var action in actions ) + { + action.SetProperty( ApiVersionModel.Neutral ); + } + } + + Tuple, IEnumerable, IEnumerable, IEnumerable> ApplyControllerConventions( HttpControllerDescriptor controllerDescriptor ) { Contract.Requires( controllerDescriptor != null ); + Contract.Ensures( Contract.Result, IEnumerable, IEnumerable, IEnumerable>>() != null ); - MergeAttributesWithConventions( controllerDescriptor ); + MergeControllerAttributesWithConventions( controllerDescriptor ); if ( VersionNeutral ) { - controllerDescriptor.SetConventionsApiVersionModel( ApiVersionModel.Neutral ); + controllerDescriptor.SetProperty( ApiVersionModel.Neutral ); } else { - controllerDescriptor.SetConventionsApiVersionModel( new ApiVersionModel( VersionNeutral, supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions ) ); + controllerDescriptor.SetProperty( new ApiVersionModel( VersionNeutral, supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions ) ); } + + return Tuple.Create( supportedVersions.AsEnumerable(), deprecatedVersions.AsEnumerable(), advertisedVersions.AsEnumerable(), deprecatedAdvertisedVersions.AsEnumerable() ); } - void MergeAttributesWithConventions( HttpControllerDescriptor controllerDescriptor ) + void MergeControllerAttributesWithConventions( HttpControllerDescriptor controllerDescriptor ) { Contract.Requires( controllerDescriptor != null ); @@ -96,20 +107,40 @@ from version in provider.Versions select version ); } - void ApplyActionConventions( HttpControllerDescriptor controllerDescriptor ) + void ApplyActionConventions( HttpControllerDescriptor controller, Tuple, IEnumerable, IEnumerable, IEnumerable> controllerVersionInfo ) { - Contract.Requires( controllerDescriptor != null ); + Contract.Requires( controller != null ); - var actionSelector = controllerDescriptor.Configuration.Services.GetActionSelector(); - var actionDescriptors = actionSelector.GetActionMapping( controllerDescriptor ).SelectMany( g => g.OfType() ); + if ( VersionNeutral ) + { + ApplyNeutralModelToActions( controller ); + } + else + { + MergeActionAttributesWithConventions( controller, controllerVersionInfo ); + } + } + + void MergeActionAttributesWithConventions( HttpControllerDescriptor controller, Tuple, IEnumerable, IEnumerable, IEnumerable> controllerVersionInfo ) + { + Contract.Requires( controller != null ); - foreach ( var actionDescriptor in actionDescriptors ) + var actionSelector = controller.Configuration.Services.GetActionSelector(); + var actions = actionSelector.GetActionMapping( controller ).SelectMany( g => g.OfType() ); + + foreach ( var action in actions ) { - var key = actionDescriptor.MethodInfo; + var key = action.MethodInfo; if ( TryGetConvention( key, out var actionConvention ) ) { - actionConvention.ApplyTo( actionDescriptor ); + action.SetProperty( controllerVersionInfo ); + actionConvention.ApplyTo( action ); + action.RemoveProperty( controllerVersionInfo ); + } + else + { + action.SetProperty( new ApiVersionModel( action ) ); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs index 33aac6fe..0b157eba 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs @@ -1,8 +1,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { using System; - using System.Linq; - using Microsoft.AspNetCore.Mvc.Versioning; /// /// Provides extension methods for application models, controller models, @@ -17,7 +15,7 @@ public static class ModelExtensions /// The type and key of the property. /// The model to get the property from. /// The property value of or its default value. - public static T GetProperty( this ControllerModel controller ) where T : class + public static T GetProperty( this ControllerModel controller ) { Arg.NotNull( controller, nameof( controller ) ); return controller.Properties.GetOrDefault( typeof( T ), default( T ) ); @@ -29,7 +27,7 @@ public static T GetProperty( this ControllerModel controller ) where T : clas /// The type and key of the property. /// The model to set the property for. /// The property value to set. - public static void SetProperty( this ControllerModel controller, T value ) where T : class + public static void SetProperty( this ControllerModel controller, T value ) { Arg.NotNull( controller, nameof( controller ) ); controller.Properties.SetOrRemove( typeof( T ), value ); @@ -41,7 +39,7 @@ public static void SetProperty( this ControllerModel controller, T value ) wh /// The type and key of the property. /// The model to get the property from. /// The property value of or its default value. - public static T GetProperty( this ActionModel action ) where T : class + public static T GetProperty( this ActionModel action ) { Arg.NotNull( action, nameof( action ) ); return action.Properties.GetOrDefault( typeof( T ), default( T ) ); @@ -53,10 +51,14 @@ public static T GetProperty( this ActionModel action ) where T : class /// The type and key of the property. /// The model to set the property for. /// The property value to set. - public static void SetProperty( this ActionModel action, T value ) where T : class + public static void SetProperty( this ActionModel action, T value ) { Arg.NotNull( action, nameof( action ) ); action.Properties.SetOrRemove( typeof( T ), value ); } + +#pragma warning disable CA1801 // Review unused parameters; intentional for type parameter + internal static void RemoveProperty( this ActionModel action, T value ) => action.Properties.Remove( typeof( T ) ); +#pragma warning restore CA1801 } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/CollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/CollectionExtensions.cs index e4883d9e..75792ae4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/CollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/CollectionExtensions.cs @@ -15,11 +15,11 @@ internal static TValue GetOrDefault( this IDictionary( this IDictionary dictionary, TKey key, TValue value ) where TValue : class + internal static void SetOrRemove( this IDictionary dictionary, TKey key, TValue value ) { Contract.Requires( dictionary != null ); - if ( value == null ) + if ( value == default && default( TValue ) == null ) { dictionary.Remove( key ); } diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs index ffe9b29b..285ed4d0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderBase.cs @@ -1,14 +1,9 @@ -#pragma warning disable SA1200 // Using directives should be placed correctly; false positive - required for inner, short-hand type aliasing -using System; -using System.Collections.Generic; -#pragma warning restore SA1200 - -namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions +namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions { using Microsoft.AspNetCore.Mvc.ApplicationModels; using System; + using System.Collections.Generic; using System.Linq; - using ControllerVersionInfo = System.Tuple, System.Collections.Generic.IEnumerable, System.Collections.Generic.IEnumerable, System.Collections.Generic.IEnumerable>; /// /// Provides additional implementation specific to Microsoft ASP.NET Core. @@ -29,13 +24,18 @@ public virtual void ApplyTo( ActionModel actionModel ) from version in provider.Versions select version ); - var controllerVersionInfo = actionModel.GetProperty(); + var (supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions) = + actionModel.GetProperty, + IEnumerable, + IEnumerable, + IEnumerable>>(); + var versionModel = new ApiVersionModel( declaredVersions: MappedVersions, - supportedVersions: controllerVersionInfo.Item1, - deprecatedVersions: controllerVersionInfo.Item2, - advertisedVersions: controllerVersionInfo.Item3, - deprecatedAdvertisedVersions: controllerVersionInfo.Item4 ); + supportedVersions, + deprecatedVersions, + advertisedVersions, + deprecatedAdvertisedVersions ); actionModel.SetProperty( versionModel ); } diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs index b047bc29..46f56be7 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderBase.cs @@ -1,16 +1,11 @@ -#pragma warning disable SA1200 // Using directives should be placed correctly; false positive - required for inner, short-hand type aliasing -using System; -using System.Collections.Generic; -#pragma warning restore SA1200 - -namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions +namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions { using Microsoft.AspNetCore.Mvc.ApplicationModels; using System; + using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; - using ControllerVersionInfo = System.Tuple, System.Collections.Generic.IEnumerable, System.Collections.Generic.IEnumerable, System.Collections.Generic.IEnumerable>; /// /// Provides additional implementation specific to Microsoft ASP.NET Core. @@ -47,10 +42,9 @@ static void ApplyNeutralModelToActions( ControllerModel controller ) } } - ControllerVersionInfo ApplyControllerConventions( ControllerModel controllerModel ) + Tuple, IEnumerable, IEnumerable, IEnumerable> ApplyControllerConventions( ControllerModel controllerModel ) { Contract.Requires( controllerModel != null ); - Contract.Ensures( Contract.Result() != null ); MergeControllerAttributesWithConventions( controllerModel ); @@ -63,7 +57,7 @@ ControllerVersionInfo ApplyControllerConventions( ControllerModel controllerMode controllerModel.SetProperty( new ApiVersionModel( VersionNeutral, supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions ) ); } - return new ControllerVersionInfo( supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions ); + return Tuple.Create( supportedVersions.AsEnumerable(), deprecatedVersions.AsEnumerable(), advertisedVersions.AsEnumerable(), deprecatedAdvertisedVersions.AsEnumerable() ); } void MergeControllerAttributesWithConventions( ControllerModel controllerModel ) @@ -103,10 +97,9 @@ from version in provider.Versions select version ); } - void ApplyActionConventions( ControllerModel controller, ControllerVersionInfo controllerVersionInfo ) + void ApplyActionConventions( ControllerModel controller, Tuple, IEnumerable, IEnumerable, IEnumerable> controllerVersionInfo ) { Contract.Requires( controller != null ); - Contract.Requires( controllerVersionInfo != null ); if ( VersionNeutral ) { @@ -118,10 +111,9 @@ void ApplyActionConventions( ControllerModel controller, ControllerVersionInfo c } } - void MergeActionAttributesWithConventions( ControllerModel controller, ControllerVersionInfo controllerVersionInfo ) + void MergeActionAttributesWithConventions( ControllerModel controller, Tuple, IEnumerable, IEnumerable, IEnumerable> controllerVersionInfo ) { Contract.Requires( controller != null ); - Contract.Requires( controllerVersionInfo != null ); foreach ( var action in controller.Actions ) { @@ -133,7 +125,7 @@ void MergeActionAttributesWithConventions( ControllerModel controller, Controlle { action.SetProperty( controllerVersionInfo ); actionConvention.ApplyTo( action ); - action.SetProperty( default( ControllerVersionInfo ) ); + action.RemoveProperty( controllerVersionInfo ); } else { diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs index 9f66e2d9..95a1dea5 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs @@ -14,8 +14,6 @@ public class ODataApiExplorerTest public void api_descriptions_should_collate_expected_versions( HttpConfiguration configuration ) { // arrange - var assembliesResolver = configuration.Services.GetAssembliesResolver(); - var controllerTypes = configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes( assembliesResolver ); var apiExplorer = new ODataApiExplorer( configuration ); // act @@ -74,8 +72,6 @@ public void api_descriptions_should_flatten_versioned_controllers( HttpConfigura public void api_descriptions_should_not_contain_metadata_controllers( HttpConfiguration configuration ) { // arrange - var assembliesResolver = configuration.Services.GetAssembliesResolver(); - var controllerTypes = configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes( assembliesResolver ); var apiExplorer = new ODataApiExplorer( configuration ); // act diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/TestConfigurations.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/TestConfigurations.cs index 159f119b..e09f4125 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/TestConfigurations.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Description/TestConfigurations.cs @@ -2,7 +2,6 @@ { using Microsoft.AspNet.OData.Builder; using Microsoft.Web.Http.Simulators.Configuration; - using Microsoft.Web.Http.Simulators.Models; using Microsoft.Web.Http.Versioning.Conventions; using System.Collections; using System.Collections.Generic; @@ -34,7 +33,7 @@ public static HttpConfiguration NewOrdersConfiguration() options.Conventions.Controller() .HasApiVersion( 1, 0 ) .HasDeprecatedApiVersion( 0, 9 ) - .Action( c => c.Post( default( Order ) ) ).MapToApiVersion( 1, 0 ); + .Action( c => c.Post( default ) ).MapToApiVersion( 1, 0 ); options.Conventions.Controller() .HasApiVersion( 2, 0 ); options.Conventions.Controller() @@ -59,14 +58,16 @@ public static HttpConfiguration NewPeopleConfiguration() typeof( Simulators.V1.PeopleController ), typeof( Simulators.V2.PeopleController ), typeof( Simulators.V3.PeopleController ) ); + + configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver ); + configuration.AddApiVersioning(); + var builder = new VersionedODataModelBuilder( configuration ) { ModelConfigurations = { new PersonModelConfiguration() } }; var models = builder.GetEdmModels(); - configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver ); - configuration.AddApiVersioning(); configuration.MapVersionedODataRoutes( "odata", "api/v{apiVersion}", models ); return configuration; diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs index 9eaf033b..66806698 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs @@ -120,6 +120,7 @@ static IEnumerable CreateModels( HttpConfiguration configuration ) controllerTypeResolver.Setup( ctr => ctr.GetControllerTypes( It.IsAny() ) ).Returns( controllerTypes ); configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object ); + configuration.AddApiVersioning(); var builder = new VersionedODataModelBuilder( configuration ); diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Controllers/HttpControllerDescriptorGroupTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Controllers/HttpControllerDescriptorGroupTest.cs index 34978821..3db75dea 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Controllers/HttpControllerDescriptorGroupTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Controllers/HttpControllerDescriptorGroupTest.cs @@ -19,11 +19,10 @@ public class HttpControllerDescriptorGroupTest public void get_enumerator_should_iterate_over_expected_items() { // arrange - var expected = new[] { new HttpControllerDescriptor(), new HttpControllerDescriptor(), new HttpControllerDescriptor() }; - var group = new HttpControllerDescriptorGroup( expected ); + var expected = NewControllerDescriptors( 3 ); // act - + var group = new HttpControllerDescriptorGroup( expected ); // assert group.Should().BeEquivalentTo( expected ); @@ -33,7 +32,7 @@ public void get_enumerator_should_iterate_over_expected_items() public void indexer_should_return_expected_item() { // arrange - var expected = new[] { new HttpControllerDescriptor(), new HttpControllerDescriptor(), new HttpControllerDescriptor() }; + var expected = NewControllerDescriptors( 3 ); var group = new HttpControllerDescriptorGroup( expected ); var list = new List(); @@ -53,12 +52,24 @@ public void get_custom_attributes_should_aggregate_attributes() // arrange var descriptor1 = new Mock() { CallBase = true }; var descriptor2 = new Mock() { CallBase = true }; + var configuration = new HttpConfiguration(); descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) .Returns( () => new Collection() { new ApiVersionAttribute( "1.0" ) } ); + descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) .Returns( () => new Collection() { new ApiVersionAttribute( "2.0" ) } ); + descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + + descriptor1.Object.Configuration = configuration; + descriptor2.Object.Configuration = configuration; var group = new HttpControllerDescriptorGroup( descriptor1.Object, descriptor2.Object ); var expected = new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ) }; @@ -78,9 +89,22 @@ public void get_filters_should_aggregate_filters() var filter2 = new Mock().Object; var descriptor1 = new Mock() { CallBase = true }; var descriptor2 = new Mock() { CallBase = true }; + var configuration = new HttpConfiguration(); descriptor1.Setup( d => d.GetFilters() ).Returns( () => new Collection() { filter1 } ); + descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor2.Setup( d => d.GetFilters() ).Returns( () => new Collection() { filter2 } ); + descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + + descriptor1.Object.Configuration = configuration; + descriptor2.Object.Configuration = configuration; var group = new HttpControllerDescriptorGroup( descriptor1.Object, descriptor2.Object ); @@ -116,11 +140,24 @@ public void create_controller_should_return_first_instance_when_version_is_unspe // arrange var expected = new Mock().Object; var controller2 = new Mock().Object; - var descriptor1 = new Mock(); - var descriptor2 = new Mock(); + var descriptor1 = new Mock() { CallBase = true }; + var descriptor2 = new Mock() { CallBase = true }; + var configuration = new HttpConfiguration(); descriptor1.Setup( d => d.CreateController( It.IsAny() ) ).Returns( expected ); + descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor2.Setup( d => d.CreateController( It.IsAny() ) ).Returns( controller2 ); + descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) + .Returns( () => new Collection() ); + + descriptor1.Object.Configuration = configuration; + descriptor2.Object.Configuration = configuration; var group = new HttpControllerDescriptorGroup( descriptor1.Object, descriptor2.Object ); var request = new HttpRequestMessage(); @@ -205,5 +242,25 @@ public void create_controller_should_return_default_instance_when_versioned_cont descriptor1.Verify( d => d.CreateController( request ), Once() ); descriptor2.Verify( d => d.CreateController( request ), Never() ); } + + static IReadOnlyList NewControllerDescriptors( int count ) + { + var configuration = new HttpConfiguration(); + var list = new List(); + + for ( var i = 0; i < count; i++ ) + { + list.Add( NewControllerDescriptor( configuration ) ); + } + + return list; + } + + static HttpControllerDescriptor NewControllerDescriptor( HttpConfiguration configuration ) => + new HttpControllerDescriptor() + { + Configuration = configuration, + ControllerType = typeof( IHttpController ), + }; } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs index a2248662..4f7b0944 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs @@ -95,7 +95,7 @@ public void select_controller_should_return_correct_versionedX2C_attributeX2Dbas var configuration = AttributeRoutingEnabledConfiguration; var request = new HttpRequestMessage( Get, "http://localhost/api/test?api-version=" + version ); - configuration.AddApiVersioning(); + configuration.AddApiVersioning( options => options.ReportApiVersions = true ); configuration.EnsureInitialized(); var routeData = configuration.Routes.GetRouteData( request ); @@ -124,7 +124,7 @@ public void select_controller_should_return_correct_versionedX2C_conventionX2Dba var configuration = new HttpConfiguration(); var request = new HttpRequestMessage( Get, "http://localhost/api/test?api-version=" + version ); - configuration.AddApiVersioning(); + configuration.AddApiVersioning( options => options.ReportApiVersions = true ); configuration.Routes.MapHttpRoute( "Default", "api/{controller}/{id}", new { id = Optional } ); configuration.EnsureInitialized(); @@ -633,10 +633,10 @@ public void select_controller_should_use_api_version_selector_for_attributeX2Dba var configuration = AttributeRoutingEnabledConfiguration; var request = new HttpRequestMessage( Get, "http://localhost/orders" ); - configuration.AddApiVersioning( o => + configuration.AddApiVersioning( options => { - o.AssumeDefaultVersionWhenUnspecified = true; - o.ApiVersionSelector = new LowestImplementedApiVersionSelector( o ); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionSelector = new LowestImplementedApiVersionSelector( options ); } ); configuration.Routes.MapHttpRoute( "Default", "{controller}/{id}", new { id = Optional } ); configuration.EnsureInitialized(); @@ -655,7 +655,7 @@ public void select_controller_should_use_api_version_selector_for_attributeX2Dba RequestContext = new HttpRequestContext() { Configuration = configuration, - RouteData = routeData + RouteData = routeData, } }; @@ -936,7 +936,7 @@ public void select_controller_should_return_correct_controller_for_versioned_url var configuration = AttributeRoutingEnabledConfiguration; var request = new HttpRequestMessage( Get, requestUri ); - configuration.AddApiVersioning(); + configuration.AddApiVersioning( options => options.ReportApiVersions = true ); configuration.EnsureInitialized(); var routeData = configuration.Routes.GetRouteData( request ); @@ -1142,15 +1142,16 @@ public void select_controller_should_report_correct_api_versions_using_conventio controllerTypeResolver.Setup( r => r.GetControllerTypes( It.IsAny() ) ).Returns( controllerTypes ); configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object ); - configuration.AddApiVersioning( o => + configuration.AddApiVersioning( options => { - o.Conventions.Controller() - .HasApiVersion( 1, 0 ) - .HasApiVersion( 2, 0 ) - .Action( c => c.GetV2() ).MapToApiVersion( 2, 0 ) - .Action( c => c.GetV2( default ) ).MapToApiVersion( 2, 0 ); - - o.Conventions.Controller().HasApiVersion( 3, 0 ); + options.ReportApiVersions = true; + options.Conventions.Controller() + .HasApiVersion( 1, 0 ) + .HasApiVersion( 2, 0 ) + .Action( c => c.GetV2() ).MapToApiVersion( 2, 0 ) + .Action( c => c.GetV2( default ) ).MapToApiVersion( 2, 0 ); + + options.Conventions.Controller().HasApiVersion( 3, 0 ); } ); configuration.Routes.MapHttpRoute( "Default", "api/{controller}/{id}", new { id = Optional } ); configuration.MapHttpAttributeRoutes(); diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Microsoft.AspNet.WebApi.Versioning.Tests.csproj b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Microsoft.AspNet.WebApi.Versioning.Tests.csproj index 646bcb86..bcfb5cac 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Microsoft.AspNet.WebApi.Versioning.Tests.csproj +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Microsoft.AspNet.WebApi.Versioning.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpActionDescriptorExtensionsTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpActionDescriptorExtensionsTest.cs index cde9eee3..fc880a68 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpActionDescriptorExtensionsTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpActionDescriptorExtensionsTest.cs @@ -39,7 +39,7 @@ public static IEnumerable ApiVersionData } [Fact] - public void get_api_version_info_should_add_and_return_new_instance_for_action_descriptor() + public void get_api_version_info_should_return_new_instance_for_action_descriptor() { // arrange var controller = new Mock().Object; @@ -53,11 +53,11 @@ public void get_api_version_info_should_add_and_return_new_instance_for_action_d // assert versionInfo.Should().NotBeNull(); - actionDescriptor.Properties.ContainsKey( "MS_ApiVersionInfo" ).Should().BeTrue(); + actionDescriptor.Properties.ContainsKey( typeof( ApiVersionModel ) ).Should().BeFalse(); } [Fact] - public void get_api_version_info_should_returne_exising_instance_for_action_descriptor() + public void get_api_version_info_should_return_exising_instance_for_action_descriptor() { // arrange var controller = new Mock().Object; @@ -65,7 +65,7 @@ public void get_api_version_info_should_returne_exising_instance_for_action_desc var actionDescriptor = new Mock( controllerDescriptor ) { CallBase = true }.Object; var assignedVersionInfo = ApiVersionModel.Default; - actionDescriptor.Properties["MS_ApiVersionInfo"] = assignedVersionInfo; + actionDescriptor.Properties[typeof(ApiVersionModel)] = assignedVersionInfo; // act var versionInfo = actionDescriptor.GetApiVersionModel(); diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpControllerDescriptorExtensionsTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpControllerDescriptorExtensionsTest.cs index ce891bc5..1e2098cc 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpControllerDescriptorExtensionsTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpControllerDescriptorExtensionsTest.cs @@ -11,72 +11,6 @@ public class HttpControllerDescriptorExtensionsTest { - public static IEnumerable DeclaredApiVersionData - { - get - { - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestController ) ), - new[] { ApiVersion.Default } - }; - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestVersion2Controller ) ), - new[] { new ApiVersion( 1, 8 ), new ApiVersion( 1, 9 ), new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) } - }; - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( AttributeRoutedTest4Controller ) ), - new[] { new ApiVersion( 4, 0 ) } - }; - } - } - - public static IEnumerable SupportedApiVersionData - { - get - { - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestController ) ), - new[] { ApiVersion.Default } - }; - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestVersion2Controller ) ), - new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) } - }; - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( AttributeRoutedTest4Controller ) ), - new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ), new ApiVersion( 4, 0 ) } - }; - } - } - - public static IEnumerable DeprecatedApiVersionData - { - get - { - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestController ) ), - new ApiVersion[0] - }; - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestVersion2Controller ) ), - new[] { new ApiVersion( 1, 8 ), new ApiVersion( 1, 9 ) } - }; - yield return new object[] - { - new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( AttributeRoutedTest4Controller ) ), - new[] { new ApiVersion( 3, 0, "Alpha" ) } - }; - } - } - [Fact] public void get_api_version_info_should_add_and_return_new_instance_for_controller_descriptor() { @@ -94,14 +28,14 @@ public void get_api_version_info_should_add_and_return_new_instance_for_controll } [Fact] - public void get_api_version_info_should_returne_exising_instance_for_controller_descriptor() + public void get_api_version_info_should_return_exising_instance_for_controller_descriptor() { // arrange var controller = new Mock().Object; var controllerDescriptor = new HttpControllerDescriptor( new HttpConfiguration(), "Tests", controller.GetType() ); var assignedVersionInfo = ApiVersionModel.Default; - controllerDescriptor.Properties["MS_ApiVersionInfo"] = assignedVersionInfo; + controllerDescriptor.Properties[typeof(ApiVersionModel)] = assignedVersionInfo; // act var versionInfo = controllerDescriptor.GetApiVersionModel(); @@ -212,5 +146,71 @@ public void get_deprecated_api_versions_should_return_expected_controller_descri // assert deprecatedVersions.Should().BeEquivalentTo( expectedVersions ); } + + public static IEnumerable DeclaredApiVersionData + { + get + { + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestController ) ), + new[] { ApiVersion.Default } + }; + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestVersion2Controller ) ), + new[] { new ApiVersion( 1, 8 ), new ApiVersion( 1, 9 ), new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) } + }; + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( AttributeRoutedTest4Controller ) ), + new[] { new ApiVersion( 4, 0 ) } + }; + } + } + + public static IEnumerable SupportedApiVersionData + { + get + { + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestController ) ), + new[] { ApiVersion.Default } + }; + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestVersion2Controller ) ), + new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) } + }; + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( AttributeRoutedTest4Controller ) ), + new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ), new ApiVersion( 4, 0 ) } + }; + } + } + + public static IEnumerable DeprecatedApiVersionData + { + get + { + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestController ) ), + new ApiVersion[0] + }; + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( TestVersion2Controller ) ), + new[] { new ApiVersion( 1, 8 ), new ApiVersion( 1, 9 ) } + }; + yield return new object[] + { + new HttpControllerDescriptor( new HttpConfiguration(), "Tests", typeof( AttributeRoutedTest4Controller ) ), + new[] { new ApiVersion( 3, 0, "Alpha" ) } + }; + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpRouteCollectionExtensionsTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpRouteCollectionExtensionsTest.cs new file mode 100644 index 00000000..88817d83 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/System.Web.Http/HttpRouteCollectionExtensionsTest.cs @@ -0,0 +1,142 @@ +namespace System.Web.Http +{ + using FluentAssertions; + using Moq; + using System.Collections.Generic; + using System.Web.Http.Routing; + using System.Web.Http.WebHost.Routing; + using Xunit; + + public class HttpRouteCollectionExtensionsTest + { + [Fact] + public void to_dictionary_should_convert_route_collection() + { + // arrange + var route = Mock.Of(); + var routes = new HttpRouteCollection() + { + { "test", route }, + }; + + // act + var dictionary = routes.ToDictionary(); + + // assert + dictionary.Should().BeEquivalentTo( new Dictionary() { ["test"] = route } ); + } + + [Fact] + public void to_dictionary_should_convert_route_collection_when_hosted_with_SystemX2EWeb() + { + // arrange + var route = Mock.Of(); + var routes = new HostedHttpRouteCollection() + { + { "test", route }, + }; + + // act + var dictionary = routes.ToDictionary(); + + // assert + dictionary.Should().BeEquivalentTo( new Dictionary() { ["test"] = route } ); + } + } +} + +// note: HostedHttpRouteCollection is an internal type. in order to test the expected behavior of the +// HttpRouteCollectionExtensions.ToDictionary hack, the bare minimum implementation is duplicated here +namespace System.Web.Http.WebHost.Routing +{ + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Web.Http.Routing; + using System.Web.Routing; + + internal sealed class HostedHttpRouteCollection : HttpRouteCollection + { + private readonly RouteCollection _routeCollection = new RouteCollection(); + + public override string VirtualPathRoot => throw NotUsedInUnitTest(); + + public override int Count => _routeCollection.Count; + + public override IHttpRoute this[string name] => ( (HttpWebRoute) _routeCollection[name] ).HttpRoute; + + public override IHttpRoute this[int index] => ( (HttpWebRoute) _routeCollection[index] ).HttpRoute; + + public override IHttpRouteData GetRouteData( HttpRequestMessage request ) => throw NotUsedInUnitTest(); + + public override IHttpVirtualPathData GetVirtualPath( HttpRequestMessage request, string name, IDictionary values ) => throw NotUsedInUnitTest(); + + public override IHttpRoute CreateRoute( string uriTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, HttpMessageHandler handler ) => throw NotUsedInUnitTest(); + + public override void Add( string name, IHttpRoute route ) => _routeCollection.Add( name, new HttpWebRoute( route ) ); + + public override void Clear() => _routeCollection.Clear(); + + public override bool Contains( IHttpRoute item ) + { + foreach ( var route in _routeCollection ) + { + if ( route is HttpWebRoute webRoute && webRoute.HttpRoute == item ) + { + return true; + } + } + + return false; + } + + public override bool ContainsKey( string name ) => _routeCollection[name] != null; + + public override void CopyTo( IHttpRoute[] array, int arrayIndex ) => throw NotSupportedByHostedRouteCollection(); + + public override void CopyTo( KeyValuePair[] array, int arrayIndex ) => throw NotSupportedByRouteCollection(); + + public override void Insert( int index, string name, IHttpRoute value ) => throw NotSupportedByRouteCollection(); + + public override bool Remove( string name ) => throw NotSupportedByRouteCollection(); + + public override IEnumerator GetEnumerator() => _routeCollection.OfType().Select( r => r.HttpRoute ).GetEnumerator(); + + public override bool TryGetValue( string name, out IHttpRoute route ) + { + if ( _routeCollection[name] is HttpWebRoute rt ) + { + route = rt.HttpRoute; + return true; + } + + route = null; + return false; + } + + static NotSupportedException NotSupportedByRouteCollection() => new NotSupportedException(); + + static NotSupportedException NotSupportedByHostedRouteCollection() => new NotSupportedException(); + + static NotImplementedException NotUsedInUnitTest() => new NotImplementedException( "Not used in unit tests" ); + } +} + +namespace System.Web.Http.WebHost.Routing +{ + using Moq; + using System.Web.Http.Routing; + using System.Web.Routing; + + internal sealed class HttpWebRoute : Route + { + public HttpWebRoute( IHttpRoute httpRoute ) + : base( httpRoute.RouteTemplate, + new RouteValueDictionary(), + new RouteValueDictionary(), + new RouteValueDictionary(), + Mock.Of() ) => HttpRoute = httpRoute; + + public IHttpRoute HttpRoute { get; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs index 8e45dd12..d332d9ef 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs @@ -2,7 +2,9 @@ { using FluentAssertions; using Moq; + using System; using System.Collections.ObjectModel; + using System.Linq; using System.Reflection; using System.Web.Http; using System.Web.Http.Controllers; @@ -18,8 +20,11 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ var controllerBuilder = new ControllerApiVersionConventionBuilder(); var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; + var empty = Enumerable.Empty(); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Object.Properties[controllerVersionInfo.GetType()] = controllerVersionInfo; // act actionBuilder.ApplyTo( actionDescriptor.Object ); @@ -32,7 +37,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ DeclaredApiVersions = new ApiVersion[0], SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -43,8 +48,11 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var controllerBuilder = new ControllerApiVersionConventionBuilder(); var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; + var empty = Enumerable.Empty(); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Object.Properties[controllerVersionInfo.GetType()] = controllerVersionInfo; actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ); // act @@ -56,9 +64,9 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ { IsApiVersionNeutral = false, DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ) }, - SupportedApiVersions = new[] { new ApiVersion( 2, 0 ) }, + SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new[] { new ApiVersion( 2, 0 ) } + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -71,7 +79,10 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var controllerDescriptor = new HttpControllerDescriptor() { ControllerType = typeof( DecoratedController ) }; var method = typeof( DecoratedController ).GetMethod( nameof( DecoratedController.Get ) ); var actionDescriptor = new ReflectedHttpActionDescriptor( controllerDescriptor, method ); + var empty = Enumerable.Empty(); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); + actionDescriptor.Properties[controllerVersionInfo.GetType()] = controllerVersionInfo; actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ) .MapToApiVersion( new ApiVersion( 3, 0 ) ); @@ -84,9 +95,9 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ { IsApiVersionNeutral = false, DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }, - SupportedApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }, + SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) } + ImplementedApiVersions = new ApiVersion[0], } ); } diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs index f74ddb68..ad4a0ca7 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs @@ -2,7 +2,9 @@ { using FluentAssertions; using Moq; + using System; using System.Collections.ObjectModel; + using System.Linq; using System.Reflection; using System.Web.Http; using System.Web.Http.Controllers; @@ -18,8 +20,11 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ var controllerBuilder = new ControllerApiVersionConventionBuilder( typeof( UndecoratedController ) ); var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; + var empty = Enumerable.Empty(); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Object.Properties[controllerVersionInfo.GetType()] = controllerVersionInfo; // act actionBuilder.ApplyTo( actionDescriptor.Object ); @@ -32,7 +37,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ DeclaredApiVersions = new ApiVersion[0], SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -43,8 +48,11 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var controllerBuilder = new ControllerApiVersionConventionBuilder( typeof( UndecoratedController ) ); var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; + var empty = Enumerable.Empty(); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Object.Properties[controllerVersionInfo.GetType()] = controllerVersionInfo; actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ); // act @@ -56,9 +64,9 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ { IsApiVersionNeutral = false, DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ) }, - SupportedApiVersions = new[] { new ApiVersion( 2, 0 ) }, + SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new[] { new ApiVersion( 2, 0 ) } + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -71,7 +79,10 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var controllerDescriptor = new HttpControllerDescriptor() { ControllerType = typeof( DecoratedController ) }; var method = typeof( DecoratedController ).GetMethod( nameof( DecoratedController.Get ) ); var actionDescriptor = new ReflectedHttpActionDescriptor( controllerDescriptor, method ); + var empty = Enumerable.Empty(); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); + actionDescriptor.Properties[controllerVersionInfo.GetType()] = controllerVersionInfo; actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ) .MapToApiVersion( new ApiVersion( 3, 0 ) ); @@ -84,9 +95,9 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ { IsApiVersionNeutral = false, DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }, - SupportedApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }, + SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) } + ImplementedApiVersions = new ApiVersion[0], } ); } diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs index 8d4629d9..cf1f1ec2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTTest.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions +namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions { using ApplicationModels; using FluentAssertions; using Moq; + using System; using System.Linq; using System.Reflection; using Xunit; using static Moq.Times; - using ControllerVersionInfo = Tuple, IEnumerable, IEnumerable, IEnumerable>; public class ActionApiVersionConventionBuilderTTest { @@ -23,7 +20,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ var method = typeof( UndecoratedController ).GetMethod( nameof( UndecoratedController.Get ) ); var actionModel = new ActionModel( method, new object[0] ); var empty = Enumerable.Empty(); - var controllerVersionInfo = new ControllerVersionInfo( empty, empty, empty, empty ); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionModel.SetProperty( controllerVersionInfo ); @@ -38,7 +35,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ DeclaredApiVersions = new ApiVersion[0], SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -52,7 +49,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var attributes = new object[] { new MapToApiVersionAttribute( "2.0" ) }; var actionModel = new ActionModel( method, attributes ); var empty = Enumerable.Empty(); - var controllerVersionInfo = new ControllerVersionInfo( empty, empty, empty, empty ); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionModel.SetProperty( controllerVersionInfo ); actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ); @@ -68,7 +65,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ) }, SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -82,7 +79,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var attributes = method.GetCustomAttributes().Cast().ToArray(); var actionModel = new ActionModel( method, attributes ); var empty = Enumerable.Empty(); - var controllerVersionInfo = new ControllerVersionInfo( empty, empty, empty, empty ); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionModel.SetProperty( controllerVersionInfo ); actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ) @@ -99,7 +96,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }, SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); } diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs index 681f53f2..4f3f65b8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/Conventions/ActionApiVersionConventionBuilderTest.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions +namespace Microsoft.AspNetCore.Mvc.Versioning.Conventions { using ApplicationModels; using FluentAssertions; using Moq; + using System; using System.Linq; using System.Reflection; using Xunit; using static Moq.Times; - using ControllerVersionInfo = Tuple, IEnumerable, IEnumerable, IEnumerable>; public class ActionApiVersionConventionBuilderTest { @@ -23,7 +20,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ var method = typeof( UndecoratedController ).GetMethod( nameof( UndecoratedController.Get ) ); var actionModel = new ActionModel( method, new object[0] ); var empty = Enumerable.Empty(); - var controllerVersionInfo = new ControllerVersionInfo( empty, empty, empty, empty ); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionModel.SetProperty( controllerVersionInfo ); @@ -38,7 +35,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ DeclaredApiVersions = new ApiVersion[0], SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -52,7 +49,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var attributes = new object[] { new MapToApiVersionAttribute( "2.0" ) }; var actionModel = new ActionModel( method, attributes ); var empty = Enumerable.Empty(); - var controllerVersionInfo = new ControllerVersionInfo( empty, empty, empty, empty ); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionModel.SetProperty( controllerVersionInfo ); actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ); @@ -68,7 +65,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ) }, SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); } @@ -82,7 +79,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var attributes = method.GetCustomAttributes().Cast().ToArray(); var actionModel = new ActionModel( method, attributes ); var empty = Enumerable.Empty(); - var controllerVersionInfo = new ControllerVersionInfo( empty, empty, empty, empty ); + var controllerVersionInfo = Tuple.Create( empty, empty, empty, empty ); actionModel.SetProperty( controllerVersionInfo ); actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ) @@ -99,7 +96,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ DeclaredApiVersions = new[] { new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }, SupportedApiVersions = new ApiVersion[0], DeprecatedApiVersions = new ApiVersion[0], - ImplementedApiVersions = new ApiVersion[0] + ImplementedApiVersions = new ApiVersion[0], } ); }