From 4bbcf22e3ea11475da9220255b37d7d9b200dd5b Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Wed, 22 Feb 2017 12:00:27 -0800 Subject: [PATCH] Refactor to more easily enable extensibility; addresses #87. --- src/Common/Versioning/ApiVersionModel.cs | 37 +++- .../Versioning/ApiVersionModel.cs | 16 +- .../ApiVersionConventionBuilder.cs | 7 +- .../ApiVersionAttribute.cs | 28 --- .../ApiVersionNeutralAttribute.cs | 29 ---- .../ApplicationModels/ModelExtensions.cs | 11 -- .../IServiceCollectionExtensions.cs | 7 +- .../Versioning/ApiVersionConvention.cs | 92 +++++++++- .../Versioning/ApiVersionModel.cs | 88 ++++++---- .../ActionApiVersionConventionBuilderT.cs | 10 +- .../ApiVersionConventionBuilder.cs | 7 +- .../ControllerApiVersionConventionBuilderT.cs | 52 ++++-- .../ImplicitControllerVersionConvention.cs | 46 ----- .../ApiVersionAttributeTest.cs | 62 ------- .../ApiVersionNeutralAttributeTest.cs | 38 ----- .../ApplicationModels/ModelExtensionsTest.cs | 34 +--- .../IServiceCollectionExtensionsTest.cs | 4 +- .../Versioning/ApiVersionConventionTest.cs | 159 ++++++++++++++++++ ...ImplicitControllerVersionConventionTest.cs | 88 ---------- 19 files changed, 402 insertions(+), 413 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionAttribute.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionNeutralAttribute.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ImplicitControllerVersionConvention.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionAttributeTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionNeutralAttributeTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionConventionTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ImplicitControllerVersionConventionTest.cs diff --git a/src/Common/Versioning/ApiVersionModel.cs b/src/Common/Versioning/ApiVersionModel.cs index 8be2a03b..d9e5fadc 100644 --- a/src/Common/Versioning/ApiVersionModel.cs +++ b/src/Common/Versioning/ApiVersionModel.cs @@ -96,10 +96,10 @@ internal ApiVersionModel( } else { - declaredVersions = new Lazy>( () => supported.Union( deprecated ).OrderBy( v => v ).ToArray() ); - supportedVersions = new Lazy>( () => supported.Union( advertised ).OrderBy( v => v ).ToArray() ); - deprecatedVersions = new Lazy>( () => deprecated.Union( deprecatedAdvertised ).OrderBy( v => v ).ToArray() ); - implementedVersions = new Lazy>( () => supportedVersions.Value.Union( deprecatedVersions.Value ).OrderBy( v => v ).ToArray() ); + declaredVersions = new Lazy>( supported.Union( deprecated ).ToSortedReadOnlyList ); + supportedVersions = new Lazy>( supported.Union( advertised ).ToSortedReadOnlyList ); + deprecatedVersions = new Lazy>( deprecated.Union( deprecatedAdvertised ).ToSortedReadOnlyList ); + implementedVersions = new Lazy>( () => supportedVersions.Value.Union( deprecatedVersions.Value ).ToSortedReadOnlyList() ); } } @@ -138,6 +138,33 @@ public ApiVersionModel( IEnumerable supportedVersions, IEnumerable>( deprecatedVersions.ToSortedReadOnlyList ); } + /// + /// Initializes a new instance of the class. + /// + /// The declared sequence of API versions on a controller or action. + /// The supported sequence of API versions on a controller. + /// The deprecated sequence of API versions on a controller. + /// The advertised sequence of API versions on a controller. + /// The deprecated, advertised sequence of API versions on a controller. + public ApiVersionModel( + IEnumerable declaredVersions, + IEnumerable supportedVersions, + IEnumerable deprecatedVersions, + IEnumerable advertisedVersions, + IEnumerable deprecatedAdvertisedVersions ) + { + Arg.NotNull( declaredVersions, nameof( declaredVersions ) ); + Arg.NotNull( supportedVersions, nameof( supportedVersions ) ); + Arg.NotNull( deprecatedVersions, nameof( deprecatedVersions ) ); + Arg.NotNull( advertisedVersions, nameof( advertisedVersions ) ); + Arg.NotNull( deprecatedAdvertisedVersions, nameof( deprecatedAdvertisedVersions ) ); + + this.declaredVersions = new Lazy>( declaredVersions.ToSortedReadOnlyList ); + this.supportedVersions = new Lazy>( supportedVersions.Union( advertisedVersions ).ToSortedReadOnlyList ); + this.deprecatedVersions = new Lazy>( deprecatedVersions.Union( deprecatedAdvertisedVersions ).ToSortedReadOnlyList ); + this.implementedVersions = new Lazy>( () => this.supportedVersions.Value.Union( this.deprecatedVersions.Value ).ToSortedReadOnlyList() ); + } + private string DebuggerDisplayText => IsApiVersionNeutral ? "*.*" : string.Join( ", ", DeclaredApiVersions ); /// @@ -198,4 +225,4 @@ public ApiVersionModel( IEnumerable supportedVersions, IEnumerableimplemented version. public IReadOnlyList DeprecatedApiVersions => deprecatedVersions.Value; } -} +} \ 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 9cd56e6b..025dc6b9 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionModel.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionModel.cs @@ -20,9 +20,13 @@ private ApiVersionModel() deprecatedVersions = emptyVersions; } - internal ApiVersionModel( HttpControllerDescriptor controllerDescriptor ) + /// + /// Initializes a new instance of the class. + /// + /// The to initialize the API version model from. + public ApiVersionModel( HttpControllerDescriptor controllerDescriptor ) { - Contract.Requires( controllerDescriptor != null ); + Arg.NotNull( controllerDescriptor, nameof( controllerDescriptor ) ); if ( IsApiVersionNeutral = controllerDescriptor.GetCustomAttributes( false ).Any() ) { @@ -61,9 +65,13 @@ internal ApiVersionModel( HttpControllerDescriptor controllerDescriptor, ApiVers } } - internal ApiVersionModel( HttpActionDescriptor actionDescriptor ) + /// + /// Initializes a new instance of the class. + /// + /// The to initialize the API version model from. + public ApiVersionModel( HttpActionDescriptor actionDescriptor ) { - Contract.Requires( actionDescriptor != null ); + Arg.NotNull( actionDescriptor, nameof( actionDescriptor ) ); if ( IsApiVersionNeutral = actionDescriptor.ControllerDescriptor.GetCustomAttributes( false ).Any() ) { diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs index b6e11e2e..926c14c4 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs @@ -50,7 +50,9 @@ public virtual ControllerApiVersionConventionBuilder Controller /// The controller descriptor /// to apply configured conventions to. - public virtual void ApplyTo( HttpControllerDescriptor controllerDescriptor ) + /// True if any conventions were applied to the + /// controller descriptor; otherwise, false. + public virtual bool ApplyTo( HttpControllerDescriptor controllerDescriptor ) { Arg.NotNull( controllerDescriptor, nameof( controllerDescriptor ) ); @@ -60,7 +62,10 @@ public virtual void ApplyTo( HttpControllerDescriptor controllerDescriptor ) if ( ControllerConventions.TryGetValue( key, out convention ) ) { convention.ApplyTo( controllerDescriptor ); + return true; } + + return false; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionAttribute.cs deleted file mode 100644 index 264ead21..00000000 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionAttribute.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc -{ - using ApplicationModels; - using System; - using Versioning; - - /// - /// Provides additional implementation specific to ASP.NET Core. - /// - [CLSCompliant( false )] - public partial class ApiVersionAttribute : IControllerModelConvention - { - /// - /// Applies the conventions to the specified controller model. - /// - /// The controller model to apply the conventions to. - public void Apply( ControllerModel controller ) - { - controller.SetProperty( new ApiVersionModel( controller ) ); - - foreach ( var action in controller.Actions ) - { - action.SetProperty( controller ); - action.SetProperty( new ApiVersionModel( controller, action ) ); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionNeutralAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionNeutralAttribute.cs deleted file mode 100644 index a9c30d8c..00000000 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/ApiVersionNeutralAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc -{ - using ApplicationModels; - using System; - using static Versioning.ApiVersionModel; - - /// - /// Provides additional implementation specific to ASP.NET Core. - /// - [CLSCompliant( false )] - public partial class ApiVersionNeutralAttribute : IControllerModelConvention - { - /// - /// Applies the conventions to the specified controller model. - /// - /// The controller model to apply the conventions to. - public void Apply( ControllerModel controller ) - { - var model = Neutral; - - controller.SetProperty( model ); - - foreach ( var action in controller.Actions ) - { - action.SetProperty( model ); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs index f83748b0..abc8befe 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/ApplicationModels/ModelExtensions.cs @@ -11,17 +11,6 @@ [CLSCompliant( false )] public static class ModelExtensions { - /// - /// Returns a value indicating whether controller model has explicit version information. - /// - /// The model to evaluate. - /// True if the has explicit version information; otherwise, false. - public static bool HasExplicitVersioning( this ControllerModel controller ) - { - Arg.NotNull( controller, nameof( controller ) ); - return controller.Properties.ContainsKey( typeof( ApiVersionModel ) ) || controller.Attributes.OfType().Any(); - } - /// /// Gets the property associated with the controller model. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs index c9c87b33..794c4e43 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs @@ -55,12 +55,7 @@ public static IServiceCollection AddApiVersioning( this IServiceCollection servi mvcOptions.Filters.Add( typeof( ReportApiVersionsAttribute ) ); } - if ( options.Conventions.Count > 0 ) - { - mvcOptions.Conventions.Add( new ApiVersionConvention( options.Conventions ) ); - } - - mvcOptions.Conventions.Add( new ImplicitControllerVersionConvention( options.DefaultApiVersion ) ); + mvcOptions.Conventions.Add( new ApiVersionConvention( options.DefaultApiVersion, options.Conventions ) ); } ); services.AddRouting( mvcOptions => mvcOptions.ConstraintMap.Add( "apiVersion", typeof( ApiVersionRouteConstraint ) ) ); diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionConvention.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionConvention.cs index 56d258ae..dee86be2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionConvention.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionConvention.cs @@ -3,6 +3,7 @@ using ApplicationModels; using Conventions; using System; + using System.Diagnostics.Contracts; /// /// Represents an application model convention which applies @@ -12,15 +13,43 @@ public class ApiVersionConvention : IApplicationModelConvention { private readonly ApiVersionConventionBuilder conventionBuilder; + private readonly ApiVersionModel implicitVersionModel; /// /// Initializes a new instance of the class. /// + public ApiVersionConvention() + { + implicitVersionModel = ApiVersionModel.Default; + conventionBuilder = new ApiVersionConventionBuilder(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The implicitly declared API version for + /// controllers and actions that have no other API versioning information applied. + public ApiVersionConvention( ApiVersion implicitlyDeclaredVersion ) + { + Arg.NotNull( implicitlyDeclaredVersion, nameof( implicitlyDeclaredVersion ) ); + + implicitVersionModel = new ApiVersionModel( implicitlyDeclaredVersion ); + conventionBuilder = new ApiVersionConventionBuilder(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The implicitly declared API version for + /// controllers and actions that have no other API versioning information applied. /// The convention builder - /// containing the configured conventions to aply. - public ApiVersionConvention( ApiVersionConventionBuilder conventionBuilder ) + /// containing the configured conventions to apply. + public ApiVersionConvention( ApiVersion implicitlyDeclaredVersion, ApiVersionConventionBuilder conventionBuilder ) { + Arg.NotNull( implicitlyDeclaredVersion, nameof( implicitlyDeclaredVersion ) ); Arg.NotNull( conventionBuilder, nameof( conventionBuilder ) ); + + implicitVersionModel = new ApiVersionModel( implicitlyDeclaredVersion ); this.conventionBuilder = conventionBuilder; } @@ -30,9 +59,64 @@ public ApiVersionConvention( ApiVersionConventionBuilder conventionBuilder ) /// The application to apply the convention to. public void Apply( ApplicationModel application ) { - foreach ( var controller in application.Controllers ) + if ( conventionBuilder.Count == 0 ) + { + foreach ( var controller in application.Controllers ) + { + ApplyAttributeOrImplicitConventions( controller ); + } + } + else + { + foreach ( var controller in application.Controllers ) + { + if ( !conventionBuilder.ApplyTo( controller ) ) + { + ApplyAttributeOrImplicitConventions( controller ); + } + } + } + } + + private static bool IsDecoratedWithAttributes( ControllerModel controller ) + { + Contract.Requires( controller != null ); + + foreach ( var attribute in controller.Attributes ) + { + if ( attribute is IApiVersionProvider || attribute is IApiVersionNeutral ) + { + return true; + } + } + + return false; + } + + private void ApplyImplicitConventions( ControllerModel controller ) + { + Contract.Requires( controller != null ); + + controller.SetProperty( implicitVersionModel ); + + foreach ( var action in controller.Actions ) + { + action.SetProperty( implicitVersionModel ); + } + } + + private void ApplyAttributeOrImplicitConventions( ControllerModel controller ) + { + Contract.Requires( controller != null ); + + if ( IsDecoratedWithAttributes( controller ) ) + { + var conventions = new ControllerApiVersionConventionBuilder(); + conventions.ApplyTo( controller ); + } + else { - conventionBuilder.ApplyTo( controller ); + ApplyImplicitConventions( controller ); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionModel.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionModel.cs index 2acc7fe4..2b7d23c3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionModel.cs @@ -3,7 +3,6 @@ using ApplicationModels; using System; using System.Collections.Generic; - using System.Diagnostics.Contracts; using System.Linq; /// @@ -11,11 +10,16 @@ /// public sealed partial class ApiVersionModel { - internal ApiVersionModel( ControllerModel controller ) + /// + /// Initializes a new instance of the class. + /// + /// The to initialize the API version model from. + [CLSCompliant( false )] + public ApiVersionModel( ControllerModel controllerModel ) { - Contract.Requires( controller != null ); + Arg.NotNull( controllerModel, nameof( controllerModel ) ); - if ( IsApiVersionNeutral = controller.Attributes.OfType().Any() ) + if ( IsApiVersionNeutral = controllerModel.Attributes.OfType().Any() ) { declaredVersions = emptyVersions; implementedVersions = emptyVersions; @@ -24,50 +28,60 @@ internal ApiVersionModel( ControllerModel controller ) } else { - declaredVersions = new Lazy>( controller.Attributes.OfType().GetImplementedApiVersions ); + declaredVersions = new Lazy>( controllerModel.Attributes.OfType().GetImplementedApiVersions ); implementedVersions = declaredVersions; - supportedVersions = new Lazy>( controller.Attributes.OfType().GetSupportedApiVersions ); - deprecatedVersions = new Lazy>( controller.Attributes.OfType().GetDeprecatedApiVersions ); + supportedVersions = new Lazy>( controllerModel.Attributes.OfType().GetSupportedApiVersions ); + deprecatedVersions = new Lazy>( controllerModel.Attributes.OfType().GetDeprecatedApiVersions ); } } - internal ApiVersionModel( ControllerModel controller, ActionModel action ) + /// + /// Initializes a new instance of the class. + /// + /// The to initialize the API version model from. + /// The to initialize the API version model from. + [CLSCompliant( false )] + public ApiVersionModel( ControllerModel controllerModel, ActionModel actionModel ) { - Contract.Requires( controller != null ); + Arg.NotNull( controllerModel, nameof( controllerModel ) ); + Arg.NotNull( actionModel, nameof( actionModel ) ); - if ( IsApiVersionNeutral = controller.Attributes.OfType().Any() ) + var versionModel = controllerModel.GetProperty(); + + if ( versionModel == null ) { - declaredVersions = emptyVersions; - implementedVersions = emptyVersions; - supportedVersions = emptyVersions; - deprecatedVersions = emptyVersions; + if ( IsApiVersionNeutral = controllerModel.Attributes.OfType().Any() ) + { + declaredVersions = emptyVersions; + implementedVersions = emptyVersions; + supportedVersions = emptyVersions; + deprecatedVersions = emptyVersions; + } + else + { + declaredVersions = new Lazy>( actionModel.Attributes.OfType().GetImplementedApiVersions ); + implementedVersions = new Lazy>( controllerModel.Attributes.OfType().GetImplementedApiVersions ); + supportedVersions = new Lazy>( controllerModel.Attributes.OfType().GetSupportedApiVersions ); + deprecatedVersions = new Lazy>( controllerModel.Attributes.OfType().GetDeprecatedApiVersions ); + } } else { - declaredVersions = new Lazy>( action.Attributes.OfType().GetImplementedApiVersions ); - implementedVersions = new Lazy>( controller.Attributes.OfType().GetImplementedApiVersions ); - supportedVersions = new Lazy>( controller.Attributes.OfType().GetSupportedApiVersions ); - deprecatedVersions = new Lazy>( controller.Attributes.OfType().GetDeprecatedApiVersions ); + if ( IsApiVersionNeutral = versionModel.IsApiVersionNeutral ) + { + declaredVersions = emptyVersions; + implementedVersions = emptyVersions; + supportedVersions = emptyVersions; + deprecatedVersions = emptyVersions; + } + else + { + declaredVersions = new Lazy>( actionModel.Attributes.OfType().GetImplementedApiVersions ); + implementedVersions = new Lazy>( () => versionModel.ImplementedApiVersions ); + supportedVersions = new Lazy>( () => versionModel.SupportedApiVersions ); + deprecatedVersions = new Lazy>( () => versionModel.DeprecatedApiVersions ); + } } } - - internal ApiVersionModel( - IEnumerable declared, - IEnumerable supported, - IEnumerable deprecated, - IEnumerable advertised, - IEnumerable deprecatedAdvertised ) - { - Contract.Requires( declared != null ); - Contract.Requires( supported != null ); - Contract.Requires( deprecated != null ); - Contract.Requires( advertised != null ); - Contract.Requires( deprecatedAdvertised != null ); - - declaredVersions = new Lazy>( () => declared.OrderBy( v => v ).ToArray() ); - supportedVersions = new Lazy>( () => supported.Union( advertised ).OrderBy( v => v ).ToArray() ); - deprecatedVersions = new Lazy>( () => deprecated.Union( deprecatedAdvertised ).OrderBy( v => v ).ToArray() ); - implementedVersions = new Lazy>( () => supportedVersions.Value.Union( deprecatedVersions.Value ).OrderBy( v => v ).ToArray() ); - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderT.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderT.cs index 6196aaf1..7ccdff6c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderT.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ActionApiVersionConventionBuilderT.cs @@ -30,11 +30,11 @@ from version in provider.Versions var controllerControllerVersionInfo = actionModel.GetProperty(); var versionModel = new ApiVersionModel( - declared: mappedVersions, - supported: controllerControllerVersionInfo.Item1, - deprecated: controllerControllerVersionInfo.Item2, - advertised: controllerControllerVersionInfo.Item3, - deprecatedAdvertised: controllerControllerVersionInfo.Item4 ); + declaredVersions: mappedVersions, + supportedVersions: controllerControllerVersionInfo.Item1, + deprecatedVersions: controllerControllerVersionInfo.Item2, + advertisedVersions: controllerControllerVersionInfo.Item3, + deprecatedAdvertisedVersions: controllerControllerVersionInfo.Item4 ); actionModel.SetProperty( versionModel ); } diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs index a247af31..e2e5b31e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ApiVersionConventionBuilder.cs @@ -52,7 +52,9 @@ public virtual ControllerApiVersionConventionBuilder Controller /// The controller model /// to apply configured conventions to. - public virtual void ApplyTo( ControllerModel controllerModel ) + /// True if any conventions were applied to the + /// controller model; otherwise, false. + public virtual bool ApplyTo( ControllerModel controllerModel ) { Arg.NotNull( controllerModel, nameof( controllerModel ) ); @@ -62,7 +64,10 @@ public virtual void ApplyTo( ControllerModel controllerModel ) if ( ControllerConventions.TryGetValue( key, out convention ) ) { convention.ApplyTo( controllerModel ); + return true; } + + return false; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderT.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderT.cs index 12586dab..5262d119 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderT.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/Conventions/ControllerApiVersionConventionBuilderT.cs @@ -23,13 +23,7 @@ public partial class ControllerApiVersionConventionBuilder : IApiVersionConve public void ApplyTo( ControllerModel controllerModel ) { Arg.NotNull( controllerModel, nameof( controllerModel ) ); - - var controllerVersionInfo = ApplyControllerConventions( controllerModel ); - - if ( ActionBuilders.Count > 0 ) - { - ApplyActionConventions( controllerModel, controllerVersionInfo ); - } + ApplyActionConventions( controllerModel, ApplyControllerConventions( controllerModel ) ); } private ControllerVersionInfo ApplyControllerConventions( ControllerModel controllerModel ) @@ -37,20 +31,25 @@ private ControllerVersionInfo ApplyControllerConventions( ControllerModel contro Contract.Requires( controllerModel != null ); Contract.Ensures( Contract.Result() != null ); - MergeAttributesWithConventions( controllerModel ); + MergeControllerAttributesWithConventions( controllerModel ); - var model = new ApiVersionModel( VersionNeutral, supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions ); - - controllerModel.SetProperty( model ); + if ( VersionNeutral ) + { + controllerModel.SetProperty( ApiVersionModel.Neutral ); + } + else + { + controllerModel.SetProperty( new ApiVersionModel( VersionNeutral, supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions ) ); + } return new ControllerVersionInfo( supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions ); } - private void MergeAttributesWithConventions( ControllerModel controllerModel ) + private void MergeControllerAttributesWithConventions( ControllerModel controllerModel ) { Contract.Requires( controllerModel != null ); - if ( VersionNeutral ) + if ( VersionNeutral |= controllerModel.Attributes.OfType().Any() ) { return; } @@ -86,6 +85,33 @@ from version in provider.Versions private void ApplyActionConventions( ControllerModel controller, ControllerVersionInfo controllerVersionInfo ) { Contract.Requires( controller != null ); + Contract.Requires( controllerVersionInfo != null ); + + if ( VersionNeutral ) + { + ApplyNeutralModelToActions( controller ); + } + else + { + MergeActionAttributesWithConventions( controller, controllerVersionInfo ); + } + } + + private void ApplyNeutralModelToActions( ControllerModel controller ) + { + Contract.Requires( controller != null ); + + foreach ( var action in controller.Actions ) + { + action.SetProperty( controller ); + action.SetProperty( ApiVersionModel.Neutral ); + } + } + + private void MergeActionAttributesWithConventions( ControllerModel controller, ControllerVersionInfo controllerVersionInfo ) + { + Contract.Requires( controller != null ); + Contract.Requires( controllerVersionInfo != null ); foreach ( var action in controller.Actions ) { diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ImplicitControllerVersionConvention.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ImplicitControllerVersionConvention.cs deleted file mode 100644 index bd11f50b..00000000 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ImplicitControllerVersionConvention.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.Versioning -{ - using ApplicationModels; - using System; - using System.Diagnostics.Contracts; - using System.Linq; - - /// - /// Represents an application model convention which implicitly versions controllers. - /// - /// This convention is used to apply a default service API version model to controllers - /// that do have explicit version information, which is particularly useful for existing services. - [CLSCompliant( false )] - public sealed class ImplicitControllerVersionConvention : IApplicationModelConvention - { - private readonly ApiVersionModel model; - - /// - /// Initializes a new instance of the class. - /// - /// The declared service API version applied to controllers - /// that do not have explicit versions. - public ImplicitControllerVersionConvention( ApiVersion declaredVersion ) - { - Arg.NotNull( declaredVersion, nameof( declaredVersion ) ); - model = new ApiVersionModel( declaredVersion ); - } - - /// - /// Applies the convention to the specified application. - /// - /// The application to apply the convention to. - public void Apply( ApplicationModel application ) - { - foreach ( var controller in application.Controllers.Where( c => !c.HasExplicitVersioning() ) ) - { - controller.SetProperty( model ); - - foreach ( var action in controller.Actions ) - { - action.SetProperty( model ); - } - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionAttributeTest.cs deleted file mode 100644 index e367bf8d..00000000 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionAttributeTest.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc -{ - using ApplicationModels; - using FluentAssertions; - using System.Linq; - using System.Reflection; - using Versioning; - using Xunit; - using static System.Type; - - public class ApiVersionAttributeTest - { - [Fact] - public void convention_should_apply_api_version_model() - { - // arrange - var supported = new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }; - var deprecated = new[] { new ApiVersion( 0, 9 ) }; - var model = new ApiVersionModel( supported, deprecated ); - var type = typeof( object ); - var attributes = new object[] - { - new ApiVersionAttribute( "1.0" ), - new ApiVersionAttribute( "2.0" ), - new ApiVersionAttribute( "3.0" ), - new ApiVersionAttribute( "0.9" ) { Deprecated = true } - }; - var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); - var controller = new ControllerModel( type.GetTypeInfo(), attributes ) - { - Actions = - { - new ActionModel( actionMethod, attributes ) - } - }; - var convention = (ApiVersionAttribute) attributes[0]; - - // act - convention.Apply( controller ); - - // assert - controller.GetProperty().ShouldBeEquivalentTo( - new - { - IsApiVersionNeutral = false, - DeclaredApiVersions = deprecated.Union( supported ).ToArray(), - ImplementedApiVersions = deprecated.Union( supported ).ToArray(), - SupportedApiVersions = supported, - DeprecatedApiVersions = deprecated - } ); - controller.Actions.Single().GetProperty().ShouldBeEquivalentTo( - new - { - IsApiVersionNeutral = false, - DeclaredApiVersions = deprecated.Union( supported ).ToArray(), - ImplementedApiVersions = deprecated.Union( supported ).ToArray(), - SupportedApiVersions = supported, - DeprecatedApiVersions = deprecated - } ); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionNeutralAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionNeutralAttributeTest.cs deleted file mode 100644 index 206a43ae..00000000 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApiVersionNeutralAttributeTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc -{ - using ApplicationModels; - using FluentAssertions; - using System.Linq; - using System.Reflection; - using Versioning; - using Xunit; - using static System.Type; - - public class ApiVersionNeutralAttributeTest - { - [Fact] - public void convention_should_apply_api_versionX2Dneutral_model() - { - // arrange - var model = ApiVersionModel.Neutral; - var type = typeof( object ); - var attributes = new object[0]; - var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); - var controller = new ControllerModel( type.GetTypeInfo(), attributes ) - { - Actions = - { - new ActionModel( actionMethod, attributes ) - } - }; - var convention = new ApiVersionNeutralAttribute(); - - // act - convention.Apply( controller ); - - // assert - controller.GetProperty().Should().BeSameAs( model ); - controller.Actions.Single().GetProperty().Should().BeSameAs( model ); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApplicationModels/ModelExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApplicationModels/ModelExtensionsTest.cs index 7efa8675..2461be04 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApplicationModels/ModelExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/ApplicationModels/ModelExtensionsTest.cs @@ -1,45 +1,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { using FluentAssertions; - using System.Collections.Generic; using System.Reflection; using Xunit; using static System.Type; public class ModelExtensionsTest { - private sealed class TestPropertyValue - { - } - - [Fact] - public void controller_model_should_not_have_explicit_versioning_by_default() - { - // arrange - var controllerType = typeof( object ).GetTypeInfo(); - var controller = new ControllerModel( controllerType, new object[0] ); - - // act - var result = controller.HasExplicitVersioning(); - - // assert - result.Should().BeFalse(); - } - - [Fact] - public void controller_model_with_api_version_should_have_explicit_versioning() - { - // arrange - var controllerType = typeof( object ).GetTypeInfo(); - var attributes = new object[] { new ApiVersionAttribute( "42.0" ) }; - var controller = new ControllerModel( controllerType, attributes ); - - // act - var result = controller.HasExplicitVersioning(); - - // assert - result.Should().BeTrue(); - } + private sealed class TestPropertyValue { } [Fact] public void set_property_should_update_controller_model_properties() diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs index c02a3972..01aba873 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs @@ -36,7 +36,7 @@ public void add_api_versioning_should_configure_mvc_with_default_options() services.Single( sd => sd.ServiceType == typeof( IApiVersionReader ) ).ImplementationInstance.Should().BeOfType(); services.Single( sd => sd.ServiceType == typeof( IApiVersionSelector ) ).ImplementationInstance.Should().BeOfType(); services.Single( sd => sd.ServiceType == typeof( IActionSelector ) ).ImplementationType.Should().Be( typeof( ApiVersionActionSelector ) ); - mvcOptions.Conventions.Single().Should().BeOfType(); + mvcOptions.Conventions.Single().Should().BeOfType(); routeOptions.ConstraintMap["apiVersion"].Should().Be( typeof( ApiVersionRouteConstraint ) ); } @@ -71,7 +71,7 @@ public void add_api_versioning_should_configure_mvc_with_custom_options() services.Single( sd => sd.ServiceType == typeof( IActionSelector ) ).ImplementationType.Should().Be( typeof( ApiVersionActionSelector ) ); services.Single( sd => sd.ServiceType == typeof( ReportApiVersionsAttribute ) ).ImplementationType.Should().Be( typeof( ReportApiVersionsAttribute ) ); mvcOptions.Filters.OfType().Single().ImplementationType.Should().Be( typeof( ReportApiVersionsAttribute ) ); - mvcOptions.Conventions.Single().Should().BeOfType(); + mvcOptions.Conventions.Single().Should().BeOfType(); routeOptions.ConstraintMap["apiVersion"].Should().Be( typeof( ApiVersionRouteConstraint ) ); } } diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionConventionTest.cs new file mode 100644 index 00000000..f2afcb08 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionConventionTest.cs @@ -0,0 +1,159 @@ +namespace Microsoft.AspNetCore.Mvc.Versioning +{ + using ApplicationModels; + using FluentAssertions; + using System.Linq; + using System.Reflection; + using Xunit; + using static System.Type; + + public class ApiVersionConventionTest + { + [Fact] + public void convention_should_apply_api_version_model() + { + // arrange + var supported = new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) }; + var deprecated = new[] { new ApiVersion( 0, 9 ) }; + var model = new ApiVersionModel( supported, deprecated ); + var type = typeof( object ); + var attributes = new object[] + { + new ApiVersionAttribute( "1.0" ), + new ApiVersionAttribute( "2.0" ), + new ApiVersionAttribute( "3.0" ), + new ApiVersionAttribute( "0.9" ) { Deprecated = true } + }; + var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); + var controller = new ControllerModel( type.GetTypeInfo(), attributes ) + { + Actions = { new ActionModel( actionMethod, attributes ) } + }; + var application = new ApplicationModel() { Controllers = { controller } }; + var convention = new ApiVersionConvention(); + + // act + convention.Apply( application ); + + // assert + controller.GetProperty().ShouldBeEquivalentTo( + new + { + IsApiVersionNeutral = false, + DeclaredApiVersions = deprecated.Union( supported ).ToArray(), + ImplementedApiVersions = deprecated.Union( supported ).ToArray(), + SupportedApiVersions = supported, + DeprecatedApiVersions = deprecated + } ); + controller.Actions.Single().GetProperty().ShouldBeEquivalentTo( + new + { + IsApiVersionNeutral = false, + DeclaredApiVersions = deprecated.Union( supported ).ToArray(), + ImplementedApiVersions = deprecated.Union( supported ).ToArray(), + SupportedApiVersions = supported, + DeprecatedApiVersions = deprecated + } ); + } + + [Fact] + public void convention_should_apply_api_versionX2Dneutral_model() + { + // arrange + var model = ApiVersionModel.Neutral; + var type = typeof( object ); + var attributes = new object[] { new ApiVersionNeutralAttribute() }; + var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); + var controller = new ControllerModel( type.GetTypeInfo(), attributes ) + { + Actions = { new ActionModel( actionMethod, attributes ) } + }; + var application = new ApplicationModel() { Controllers = { controller } }; + var convention = new ApiVersionConvention(); + + // act + convention.Apply( application ); + + // assert + controller.GetProperty().Should().BeSameAs( model ); + controller.Actions.Single().GetProperty().Should().BeSameAs( model ); + } + + [Fact] + public void convention_should_apply_implicit_api_version_model() + { + // arrange + var type = typeof( object ); + var attributes = new object[0]; + var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); + var application = new ApplicationModel() + { + Controllers = + { + new ControllerModel( type.GetTypeInfo(), attributes ) + { + Actions = + { + new ActionModel( actionMethod, attributes ) + } + } + } + }; + var convention = new ApiVersionConvention( new ApiVersion( 1, 0 ) ); + + // act + convention.Apply( application ); + + // assert + application.Controllers.Single().GetProperty().ShouldBeEquivalentTo( + new + { + IsApiVersionNeutral = false, + DeclaredApiVersions = new[] { new ApiVersion( 1, 0 ) }, + ImplementedApiVersions = new[] { new ApiVersion( 1, 0 ) }, + SupportedApiVersions = new[] { new ApiVersion( 1, 0 ) }, + DeprecatedApiVersions = new ApiVersion[0], + } ); + application.Controllers.Single().Actions.Single().GetProperty().ShouldBeEquivalentTo( + new + { + IsApiVersionNeutral = false, + DeclaredApiVersions = new[] { new ApiVersion( 1, 0 ) }, + ImplementedApiVersions = new[] { new ApiVersion( 1, 0 ) }, + SupportedApiVersions = new[] { new ApiVersion( 1, 0 ) }, + DeprecatedApiVersions = new ApiVersion[0], + } ); + } + + [Fact] + public void convention_should_not_apply_implicit_api_version_model_to_controller_and_actions_with_explicit_api_versions() + { + // arrange + var type = typeof( object ); + var attributes = new object[] { new ApiVersionAttribute( "2.0" ) }; + var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); + var v1 = new ApiVersion( 1, 0 ); + var application = new ApplicationModel() + { + Controllers = + { + new ControllerModel( type.GetTypeInfo(), attributes ) + { + Actions = + { + new ActionModel( actionMethod, attributes ) + } + } + } + }; + var convention = new ApiVersionConvention( v1 ); + + // act + convention.Apply( application ); + + // assert + application.Controllers.Single().GetProperty().ImplementedApiVersions.Should().NotContain( v1 ); + application.Controllers.Single().Actions.Single().GetProperty().ImplementedApiVersions.Should().NotContain( v1 ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ImplicitControllerVersionConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ImplicitControllerVersionConventionTest.cs deleted file mode 100644 index 921ddefe..00000000 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ImplicitControllerVersionConventionTest.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.Versioning -{ - using ApplicationModels; - using FluentAssertions; - using System.Linq; - using System.Reflection; - using Xunit; - using static System.Type; - - public class ImplicitControllerVersionConventionTest - { - [Fact] - public void convention_should_apply_implicit_api_version_model() - { - // arrange - var type = typeof( object ); - var attributes = new object[0]; - var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); - var application = new ApplicationModel() - { - Controllers = - { - new ControllerModel( type.GetTypeInfo(), attributes ) - { - Actions = - { - new ActionModel( actionMethod, attributes ) - } - } - } - }; - var convention = new ImplicitControllerVersionConvention( new ApiVersion( 1, 0 ) ); - - // act - convention.Apply( application ); - - // assert - application.Controllers.Single().GetProperty().ShouldBeEquivalentTo( - new - { - IsApiVersionNeutral = false, - DeclaredApiVersions = new[] { new ApiVersion( 1, 0 ) }, - ImplementedApiVersions = new[] { new ApiVersion( 1, 0 ) }, - SupportedApiVersions = new[] { new ApiVersion( 1, 0 ) }, - DeprecatedApiVersions = new ApiVersion[0], - } ); - application.Controllers.Single().Actions.Single().GetProperty().ShouldBeEquivalentTo( - new - { - IsApiVersionNeutral = false, - DeclaredApiVersions = new[] { new ApiVersion( 1, 0 ) }, - ImplementedApiVersions = new[] { new ApiVersion( 1, 0 ) }, - SupportedApiVersions = new[] { new ApiVersion( 1, 0 ) }, - DeprecatedApiVersions = new ApiVersion[0], - } ); - } - - [Fact] - public void convention_should_not_apply_to_controller_and_actions_with_explicit_api_versions() - { - // arrange - var type = typeof( object ); - var attributes = new object[] { new ApiVersionAttribute( "2.0" ) }; - var actionMethod = type.GetRuntimeMethod( nameof( object.ToString ), EmptyTypes ); - var application = new ApplicationModel() - { - Controllers = - { - new ControllerModel( type.GetTypeInfo(), attributes ) - { - Actions = - { - new ActionModel( actionMethod, attributes ) - } - } - } - }; - var convention = new ImplicitControllerVersionConvention( new ApiVersion( 1, 0 ) ); - - // act - convention.Apply( application ); - - // assert - application.Controllers.Single().GetProperty().Should().BeNull(); - application.Controllers.Single().Actions.Single().GetProperty().Should().BeNull(); - } - } -}