From e11e3a329aafaf8bc2ce6ae64254b1b451e5a051 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 11 Nov 2022 11:03:12 -0800 Subject: [PATCH 01/35] Fix code analysis violation after .NET 7 SDK update --- .../TestServer.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/TestServer.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/TestServer.cs index 5965ecb7..4b1db41c 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/TestServer.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/TestServer.cs @@ -7,6 +7,7 @@ namespace Asp.Versioning.Http; internal sealed class TestServer : HttpMessageHandler { private readonly HttpResponseMessage response; + private bool disposed; public TestServer() => response = new( HttpStatusCode.OK ); @@ -14,4 +15,20 @@ internal sealed class TestServer : HttpMessageHandler protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) => Task.FromResult( response ); + + protected override void Dispose( bool disposing ) + { + if ( disposed ) + { + return; + } + + base.Dispose( disposing ); + disposed = true; + + if ( disposing ) + { + response.Dispose(); + } + } } \ No newline at end of file From c6aa4ee222c78b686ea94432f39fba42463cbe67 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 11 Nov 2022 11:18:05 -0800 Subject: [PATCH 02/35] Report parameter location and handle empty parameter name. Fixes #904 --- .../ApiVersionParameterDescriptionContext.cs | 5 +++++ .../ApplyContentTypeVersionActionFilter.cs | 1 - .../System.Web.Http/HttpConfigurationExtensions.cs | 12 ++++++++++-- .../Builder/IEndpointConventionBuilderExtensions.cs | 12 +++++++++--- .../ApiVersionParameterDescriptionContext.cs | 5 +++++ .../ApiVersioningMvcOptionsSetup.cs | 12 ++++++++++-- .../src/Common/MediaTypeApiVersionReaderBuilder.cs | 13 ++++++++++--- 7 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs index b33fa087..c1e5fdf6 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs @@ -157,6 +157,11 @@ protected virtual void UpdateUrlSegment() /// The name of the media type parameter. protected virtual void AddMediaTypeParameter( string name ) { + if ( string.IsNullOrEmpty( name ) ) + { + return; + } + var parameter = new NameValueHeaderValue( name, ApiVersion.ToString() ); CloneFormattersAndAddMediaTypeParameter( parameter, ApiDescription.SupportedRequestBodyFormatters ); diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApplyContentTypeVersionActionFilter.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApplyContentTypeVersionActionFilter.cs index 8e9f64b3..609d4854 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApplyContentTypeVersionActionFilter.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApplyContentTypeVersionActionFilter.cs @@ -3,7 +3,6 @@ namespace Asp.Versioning; using System.Net.Http.Headers; -using System.Web.Http; using System.Web.Http.Filters; using static Asp.Versioning.ApiVersionParameterLocation; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs index 2d922842..932639c4 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs @@ -7,6 +7,7 @@ namespace System.Web.Http; using Asp.Versioning.Dispatcher; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; +using static Asp.Versioning.ApiVersionParameterLocation; /// /// Provides extension methods for the class. @@ -79,9 +80,16 @@ private static void AddApiVersioning( this HttpConfiguration configuration, ApiV configuration.Filters.Add( new ReportApiVersionsAttribute() ); } - if ( options.ApiVersionReader.VersionsByMediaType() ) + var reader = options.ApiVersionReader; + + if ( reader.VersionsByMediaType() ) { - configuration.Filters.Add( new ApplyContentTypeVersionActionFilter( options.ApiVersionReader ) ); + var parameterName = reader.GetParameterName( MediaTypeParameter ); + + if ( !string.IsNullOrEmpty( parameterName ) ) + { + configuration.Filters.Add( new ApplyContentTypeVersionActionFilter( reader ) ); + } } configuration.Properties.AddOrUpdate( ApiVersioningOptionsKey, options, ( key, oldValue ) => options ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/net6.0/Builder/IEndpointConventionBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/net6.0/Builder/IEndpointConventionBuilderExtensions.cs index bcedd700..65a7c102 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/net6.0/Builder/IEndpointConventionBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/net6.0/Builder/IEndpointConventionBuilderExtensions.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System.Globalization; +using System.Runtime.CompilerServices; using static Asp.Versioning.ApiVersionParameterLocation; /// @@ -100,12 +101,17 @@ private static void Apply( if ( parameterSource.VersionsByMediaType() ) { var parameterName = parameterSource.GetParameterName( MediaTypeParameter ); - requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate ); - requestDelegate = new ContentTypeApiVersionDecorator( requestDelegate, parameterName ); - endpointBuilder.RequestDelegate = requestDelegate; + + if ( !string.IsNullOrEmpty( parameterName ) ) + { + requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate ); + requestDelegate = new ContentTypeApiVersionDecorator( requestDelegate, parameterName ); + endpointBuilder.RequestDelegate = requestDelegate; + } } } + [MethodImpl( MethodImplOptions.AggressiveInlining )] private static RequestDelegate EnsureRequestDelegate( RequestDelegate? current, RequestDelegate? original ) => ( current ?? original ) ?? throw new InvalidOperationException( diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs index 54441340..c95bc1fc 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs @@ -200,6 +200,11 @@ protected virtual void UpdateUrlSegment() /// The name of the media type parameter. protected virtual void AddMediaTypeParameter( string name ) { + if ( string.IsNullOrEmpty( name ) ) + { + return; + } + var requestFormats = ApiDescription.SupportedRequestFormats.ToArray(); var responseTypes = ApiDescription.SupportedResponseTypes.ToArray(); var parameter = $"{name}={ApiVersion}"; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs index f550e878..352b18d3 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs @@ -6,6 +6,7 @@ namespace Asp.Versioning; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.Options; +using static Asp.Versioning.ApiVersionParameterLocation; /// /// Represents the API versioning configuration for ASP.NET Core MVC options. @@ -36,9 +37,16 @@ public virtual void PostConfigure( string name, MvcOptions options ) options.Filters.AddService(); } - if ( value.ApiVersionReader.VersionsByMediaType() ) + var reader = value.ApiVersionReader; + + if ( reader.VersionsByMediaType() ) { - options.Filters.AddService(); + var parameterName = reader.GetParameterName( MediaTypeParameter ); + + if ( !string.IsNullOrEmpty( parameterName ) ) + { + options.Filters.AddService(); + } } var modelMetadataDetailsProviders = options.ModelMetadataDetailsProviders; diff --git a/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs b/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs index ca54d123..0142d423 100644 --- a/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs +++ b/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs @@ -301,9 +301,16 @@ public void AddParameters( IApiVersionParameterDescriptionContext context ) throw new ArgumentNullException( nameof( context ) ); } - for ( var i = 0; i < parameters.Count; i++ ) + if ( parameters.Count == 0 ) { - context.AddParameter( parameters[i], MediaTypeParameter ); + context.AddParameter( name: string.Empty, MediaTypeParameter ); + } + else + { + for ( var i = 0; i < parameters.Count; i++ ) + { + context.AddParameter( parameters[i], MediaTypeParameter ); + } } } @@ -427,7 +434,7 @@ private void Read( versions.Add( result[j] ); } } + } } } - } } \ No newline at end of file From ec227e0d2c87fd666bf05f6488cd7f84f1c0f656 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 11 Nov 2022 11:18:18 -0800 Subject: [PATCH 03/35] Bump version and release notes --- .../Asp.Versioning.WebApi.OData.ApiExplorer.csproj | 2 +- .../Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt | 2 +- .../Asp.Versioning.WebApi.OData.csproj | 2 +- .../OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt | 2 +- .../Asp.Versioning.WebApi.ApiExplorer.csproj | 2 +- .../src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt | 2 +- .../src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj | 2 +- src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt | 2 +- .../Asp.Versioning.OData.ApiExplorer.csproj | 2 +- .../OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt | 2 +- .../OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj | 2 +- src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt | 2 +- .../WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj | 2 +- src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt | 2 +- .../Asp.Versioning.Mvc.ApiExplorer.csproj | 2 +- .../WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt | 2 +- .../WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj | 2 +- src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj index 0465c87e..44cd9e06 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net45;net472 Asp.Versioning diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj index 8dfc8a6f..504275fe 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net45;net472 Asp.Versioning diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj index fd35f31a..c04f1fe3 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net45;net472 ASP.NET Web API Versioning API Explorer diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj index ee683f0f..b2f49ecd 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net45;net472 ASP.NET Web API Versioning diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj index 16759695..9d30402c 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj index 16d4b375..db8d2f56 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj index 6e440ad7..39b0d935 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj index 4212eaad..6e9c4dd0 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net6.0;netcoreapp3.1 Asp.Versioning.ApiExplorer diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj index 597fc408..df0ec3fb 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt index 5f282702..40001e18 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file From d92b96799aa41228da94d2a8d20d643be0917fb4 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Wed, 16 Nov 2022 16:51:27 -0800 Subject: [PATCH 04/35] Use 404 vs 400 when versioning only by URL and no requested versions. Fixes #911 --- .../Routing/ApiVersionPolicyJumpTable.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs index 950c6cd8..ed8db5bd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs @@ -11,6 +11,7 @@ namespace Asp.Versioning.Routing; internal sealed class ApiVersionPolicyJumpTable : PolicyJumpTable { private readonly bool versionsByUrl; + private readonly bool versionsByUrlOnly; private readonly bool versionsByMediaTypeOnly; private readonly RouteDestination rejection; private readonly IReadOnlyDictionary destinations; @@ -32,6 +33,7 @@ internal ApiVersionPolicyJumpTable( this.parser = parser; this.options = options; versionsByUrl = routePatterns.Count > 0; + versionsByUrlOnly = source.VersionsByUrl( allowMultipleLocations: false ); versionsByMediaTypeOnly = source.VersionsByMediaType( allowMultipleLocations: false ); } @@ -61,15 +63,18 @@ public override int GetDestination( HttpContext httpContext ) return destination; } - // 2. short-circuit if a default version cannot be assumed - if ( !options.AssumeDefaultVersionWhenUnspecified ) + // 2. IApiVersionSelector cannot be used yet because there are no candidates that an + // aggregated version model can be computed from to select the 'default' API version + if ( options.AssumeDefaultVersionWhenUnspecified ) { - return rejection.Unspecified; // 400 + return rejection.AssumeDefault; } - // 3. IApiVersionSelector cannot be used yet because there are no candidates that an - // aggregated version model can be computed from to select the 'default' API version - return rejection.AssumeDefault; + // 3. unspecified + return versionsByUrlOnly + /* 404 */ ? rejection.Exit + /* 400 */ : rejection.Unspecified; + case 1: rawApiVersion = apiVersions[0]; From 16b31f2bed089d50bb468859db0a79567adb308e Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Wed, 16 Nov 2022 16:54:15 -0800 Subject: [PATCH 05/35] Update version and release notes --- .../WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj | 2 +- src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj index 39b0d935..0224ba9c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj @@ -1,7 +1,7 @@  - 6.2.1 + 6.2.2 6.2.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt index 40001e18..696f3a30 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file +[Fix #911](https://github.com/dotnet/aspnet-api-versioning/issues/911) \ No newline at end of file From 215b04e7dbcbab31d7d997386ec0fcc70eac3788 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Thu, 17 Nov 2022 11:39:59 -0800 Subject: [PATCH 06/35] Remove use of Reflection; not AOT friendly --- .../ApiBehaviorSpecification.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs index 092a3472..438a76f0 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs @@ -3,6 +3,7 @@ namespace Asp.Versioning.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Infrastructure; using System.Reflection; /// @@ -11,17 +12,22 @@ namespace Asp.Versioning.ApplicationModels; [CLSCompliant( false )] public sealed class ApiBehaviorSpecification : IApiControllerSpecification { - static ApiBehaviorSpecification() + /// + public bool IsSatisfiedBy( ControllerModel controller ) { - const string ApiBehaviorApplicationModelProviderTypeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ApiBehaviorApplicationModelProvider, Microsoft.AspNetCore.Mvc.Core"; - var type = Type.GetType( ApiBehaviorApplicationModelProviderTypeName, throwOnError: true )!; - var method = type.GetRuntimeMethods().Single( m => m.Name == "IsApiController" ); + if ( controller == null ) + { + throw new ArgumentNullException( nameof( controller ) ); + } - IsApiController = (Func) method.CreateDelegate( typeof( Func ) ); - } + // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs + if ( controller.Attributes.OfType().Any() ) + { + return true; + } - private static Func IsApiController { get; } + var assembly = controller.ControllerType.Assembly; - /// - public bool IsSatisfiedBy( ControllerModel controller ) => IsApiController( controller ); + return assembly.GetCustomAttributes().OfType().Any(); + } } \ No newline at end of file From 6eb994fe1babcaf4b448cf5121313c0e406ab26c Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Thu, 17 Nov 2022 12:35:42 -0800 Subject: [PATCH 07/35] Ensure depend services are added --- .../IApiVersioningBuilderExtensions.cs | 17 ++++++++--------- .../IApiVersioningBuilderExtensions.cs | 1 + 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs index 2763a3cf..05abf023 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -31,7 +31,7 @@ public static IApiVersioningBuilder AddApiExplorer( this IApiVersioningBuilder b throw new ArgumentNullException( nameof( builder ) ); } - AddApiExplorerServices( builder.Services ); + AddApiExplorerServices( builder ); return builder; } @@ -48,18 +48,17 @@ public static IApiVersioningBuilder AddApiExplorer( this IApiVersioningBuilder b throw new ArgumentNullException( nameof( builder ) ); } - var services = builder.Services; - AddApiExplorerServices( services ); - services.Configure( setupAction ); + AddApiExplorerServices( builder ); + builder.Services.Configure( setupAction ); + return builder; } - private static void AddApiExplorerServices( IServiceCollection services ) + private static void AddApiExplorerServices( IApiVersioningBuilder builder ) { - if ( services == null ) - { - throw new ArgumentNullException( nameof( services ) ); - } + builder.AddMvc(); + + var services = builder.Services; services.AddMvcCore().AddApiExplorer(); services.TryAddSingleton, ApiExplorerOptionsFactory>(); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs index 151a4a5f..348c5a76 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -62,6 +62,7 @@ public static IApiVersioningBuilder AddMvc( this IApiVersioningBuilder builder, private static void AddServices( IServiceCollection services ) { + services.AddMvcCore(); services.TryAddSingleton, MvcApiVersioningOptionsFactory>(); services.TryAddSingleton(); services.TryAddSingleton( sp => new ApiVersionConventionBuilder( sp.GetRequiredService() ) ); From 8a6d9ffc5577d3628f238b2dd5b1d1e585923079 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Thu, 17 Nov 2022 12:58:44 -0800 Subject: [PATCH 08/35] Do not explore unversioned endpoint more than one. Fixes #917 --- .../VersionedApiDescriptionProvider.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs index 0fda8310..e71c67d0 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs @@ -123,7 +123,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) } var groupResults = new List( capacity: results.Count ); - var unversioned = default( List ); + var unversioned = default( Dictionary ); var formatGroupName = Options.FormatGroupName; foreach ( var version in FlattenApiVersions( results ) ) @@ -132,6 +132,11 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) for ( var i = 0; i < results.Count; i++ ) { + if ( unversioned != null && unversioned.ContainsKey( i ) ) + { + continue; + } + var result = results[i]; var action = result.ActionDescriptor; @@ -140,7 +145,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) if ( IsUnversioned( action ) ) { unversioned ??= new(); - unversioned.Add( result ); + unversioned.Add( i, result ); } continue; @@ -183,9 +188,9 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) return; } - for ( var i = 0; i < unversioned.Count; i++ ) + foreach ( var result in unversioned.Values ) { - results.Add( unversioned[i] ); + results.Add( result ); } } From 93495adfea3341789b275e6caa6f3bdddb035aca Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Nov 2022 09:14:56 -0800 Subject: [PATCH 09/35] Sunset policy should be reported during non-success if possible --- .../src/Common/DefaultApiVersionReporter.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs index e877d8cd..30d0e67d 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -79,18 +79,6 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) AddApiVersionHeader( headers, apiSupportedVersionsName, apiVersionModel.SupportedApiVersions ); AddApiVersionHeader( headers, apiDeprecatedVersionsName, apiVersionModel.DeprecatedApiVersions ); -#if NETFRAMEWORK - var statusCode = (int) response.StatusCode; -#else - var context = response.HttpContext; - var statusCode = response.StatusCode; -#endif - - if ( statusCode < 200 || statusCode > 299 ) - { - return; - } - #if NETFRAMEWORK if ( response.RequestMessage is not HttpRequestMessage request || request.GetActionDescriptor()?.GetApiVersionMetadata() is not ApiVersionMetadata metadata ) @@ -102,6 +90,8 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) var policyManager = request.GetConfiguration().DependencyResolver.GetSunsetPolicyManager(); var version = request.GetRequestedApiVersion(); #else + var context = response.HttpContext; + if ( context.GetEndpoint()?.Metadata.GetMetadata() is not ApiVersionMetadata metadata ) { return; @@ -113,8 +103,8 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) #endif if ( policyManager.TryGetPolicy( name, version, out var policy ) || - ( !string.IsNullOrEmpty( name ) && policyManager.TryGetPolicy( name, out policy ) ) || - ( version != null && policyManager.TryGetPolicy( version, out policy ) ) ) + ( !string.IsNullOrEmpty( name ) && policyManager.TryGetPolicy( name, out policy ) ) || + ( version != null && policyManager.TryGetPolicy( version, out policy ) ) ) { response.WriteSunsetPolicy( policy ); } From 0a999316aebc81fb1bf3842a2980901f9539978b Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Nov 2022 09:18:50 -0800 Subject: [PATCH 10/35] Case 'code' attribute for JSON --- .../src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs | 1 - .../DefaultProblemDetailsFactory.cs | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs index ecb723fc..3bb429dd 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs @@ -4,7 +4,6 @@ namespace Asp.Versioning; using Newtonsoft.Json; -using System.Web.Http; using static Asp.Versioning.ProblemDetailsDefaults; using static Newtonsoft.Json.NullValueHandling; using static System.Globalization.CultureInfo; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultProblemDetailsFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultProblemDetailsFactory.cs index 32f3b311..55a7975d 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultProblemDetailsFactory.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultProblemDetailsFactory.cs @@ -51,24 +51,24 @@ public ProblemDetails CreateProblemDetails( /// is null. public static void ApplyExtensions( ProblemDetails problemDetails ) { - const string Code = nameof( Code ); + const string code = nameof( code ); var type = ( problemDetails ?? throw new ArgumentNullException( nameof( problemDetails ) ) ).Type; if ( type == Ambiguous.Type ) { - problemDetails.Extensions[Code] = Ambiguous.Code; + problemDetails.Extensions[code] = Ambiguous.Code; } else if ( type == Invalid.Type ) { - problemDetails.Extensions[Code] = Invalid.Code; + problemDetails.Extensions[code] = Invalid.Code; } else if ( type == Unspecified.Type ) { - problemDetails.Extensions[Code] = Unspecified.Code; + problemDetails.Extensions[code] = Unspecified.Code; } else if ( type == Unsupported.Type ) { - problemDetails.Extensions[Code] = Unsupported.Code; + problemDetails.Extensions[code] = Unsupported.Code; } } } \ No newline at end of file From 4c53a86ba548816b4e0193a21a7e9f61bc0eff47 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Nov 2022 16:31:48 -0800 Subject: [PATCH 11/35] Add option to configure the status code when a version is unsupported --- src/Common/src/Common/ApiVersioningOptions.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/Common/src/Common/ApiVersioningOptions.cs b/src/Common/src/Common/ApiVersioningOptions.cs index 3b0c8c98..6fb980dd 100644 --- a/src/Common/src/Common/ApiVersioningOptions.cs +++ b/src/Common/src/Common/ApiVersioningOptions.cs @@ -3,6 +3,9 @@ namespace Asp.Versioning; using Asp.Versioning.Routing; +#if NETFRAMEWORK +using System.Net; +#endif using static Asp.Versioning.ApiVersionReader; /// @@ -100,4 +103,35 @@ public IApiVersioningPolicyBuilder Policies get => apiVersioningPolicyBuilder ??= new ApiVersioningPolicyBuilder(); set => apiVersioningPolicyBuilder = value; } + + /// + /// Gets or sets the HTTP status code used for unsupported versions of an API. + /// + /// The HTTP status code. The default value is 400 (Bad Request). + /// + /// While any HTTP status code can be provided, the following are the most sensible: + /// + /// + /// Status + /// Description + /// + /// + /// 400 (Bad Request) + /// The API doesn't support this version + /// + /// + /// 404 (Not Found) + /// The API doesn't exist + /// + /// + /// 501 (Not Implemented) + /// The API isn't implemented + /// + /// + /// +#if NETFRAMEWORK + public HttpStatusCode UnsupportedApiVersionStatusCode { get; set; } = HttpStatusCode.BadRequest; +#else + public int UnsupportedApiVersionStatusCode { get; set; } = 400; +#endif } \ No newline at end of file From a9c46c172bf8482b1be06a4594a8d16e361dd16c Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Nov 2022 16:33:47 -0800 Subject: [PATCH 12/35] Restore functionality to report versions when unmatched. Fixes #876. Fixes #918. --- ...a query string and split into two types.cs | 4 +- ...a query string and split into two types.cs | 4 +- .../when using a query string.cs | 4 +- .../when people is any version.cs | 4 +- ...a query string and split into two types.cs | 8 +- .../when using a query string.cs | 4 +- .../OData/ODataAcceptanceTest.cs | 8 +- ...a query string and split into two types.cs | 8 +- .../when using a query string.cs | 4 +- .../HttpResponseExceptionFactory.cs | 30 ++++- .../ApiVersionControllerSelectorTest.cs | 16 +-- .../when using an endpoint.cs | 2 +- ...a query string and split into two types.cs | 4 +- ...a query string and split into two types.cs | 4 +- .../when using a query string.cs | 4 +- .../when people is any version.cs | 5 +- ...a query string and split into two types.cs | 8 +- .../when using a query string.cs | 15 ++- .../OData/ODataAcceptanceTest.cs | 8 +- ...a query string and split into two types.cs | 8 +- .../when using a query string.cs | 13 +- .../Routing/ApiVersionMatcherPolicy.cs | 121 ++++++++++++++---- .../Routing/ApiVersionPolicyFeature.cs | 10 ++ .../Routing/ApiVersionPolicyJumpTable.cs | 18 ++- .../Routing/ClientErrorEndpointBuilder.cs | 6 +- .../Routing/EdgeBuilder.cs | 44 ++++--- .../Asp.Versioning.Http/Routing/EdgeKey.cs | 50 ++++++-- .../Routing/EndpointProblem.cs | 20 ++- .../Routing/EndpointType.cs | 1 + .../Routing/NotAcceptableEndpoint.cs | 12 +- .../Routing/RouteDestination.cs | 2 + .../Routing/UnsupportedApiVersionEndpoint.cs | 15 ++- .../Routing/UnsupportedMediaTypeEndpoint.cs | 12 +- .../ReportApiVersionsAttributeTest.cs | 6 +- 34 files changed, 338 insertions(+), 144 deletions(-) create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyFeature.cs diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs index fab6d7d1..cffde314 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs @@ -34,7 +34,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -44,7 +44,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs index 08ecf18c..1080b60e 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs @@ -35,7 +35,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -45,7 +45,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs index 652b9347..06c84e76 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs @@ -32,7 +32,7 @@ public async Task then_get_should_return_200( Type controllerType, string apiVer } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -42,7 +42,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs index 9070597e..b354d6f6 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs @@ -9,7 +9,7 @@ namespace given_a_versioned_ODataController_mixed_with_Web_API_controllers; public class when_people_is_any_version : AdvancedAcceptanceTest { [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { lastName = "Me" }; @@ -19,7 +19,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs index f32b79c3..ad4f5364 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -38,7 +38,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } @@ -73,7 +73,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version( stri } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -83,7 +83,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs index 591fc75f..b73cde2c 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs @@ -24,7 +24,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -34,7 +34,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs index 32491a77..d69604f8 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs @@ -37,7 +37,7 @@ public async Task then_the_service_document_should_be_versionX2Dspecific( string } [Fact] - public async Task then_the_service_document_should_return_404_for_an_unsupported_version() + public async Task then_the_service_document_should_return_400_for_an_unsupported_version() { // arrange @@ -47,7 +47,7 @@ public async Task then_the_service_document_should_return_404_for_an_unsupported var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } @@ -83,7 +83,7 @@ public async Task then_X24metadata_should_be_versionX2Dspecific( string apiVersi } [Fact] - public async Task then_X24metadata_should_return_404_for_an_unsupported_version() + public async Task then_X24metadata_should_return_400_for_an_unsupported_version() { // arrange Client.DefaultRequestHeaders.Clear(); @@ -93,7 +93,7 @@ public async Task then_X24metadata_should_return_404_for_an_unsupported_version( var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs index 0cd5170d..46e15b8d 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -38,7 +38,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } @@ -73,7 +73,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version( stri } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -83,7 +83,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs index ba475388..4956d346 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs @@ -24,7 +24,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -34,7 +34,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs index 82a9761d..4afb0774 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs @@ -9,8 +9,6 @@ namespace Asp.Versioning.Dispatcher; using System.Web.Http.Tracing; using static System.Net.HttpStatusCode; -#pragma warning disable CA2000 // Dispose objects before losing scope - internal sealed class HttpResponseExceptionFactory { private const string Allow = nameof( Allow ); @@ -64,7 +62,8 @@ internal HttpResponseException NewUnmatchedException( } } - var versionsOnlyByMediaType = Options.ApiVersionReader.VersionsByMediaType( allowMultipleLocations: false ); + var options = Options; + var versionsOnlyByMediaType = options.ApiVersionReader.VersionsByMediaType( allowMultipleLocations: false ); if ( versionsOnlyByMediaType ) { @@ -75,9 +74,28 @@ internal HttpResponseException NewUnmatchedException( if ( couldMatch ) { properties ??= request.ApiVersionProperties(); - response = properties.RequestedApiVersion is ApiVersion apiVersion - ? CreateResponseForUnsupportedApiVersion( apiVersion, NotFound ) - : CreateNotFound( conventionRouteResult ); + + if ( properties.RequestedApiVersion is ApiVersion apiVersion ) + { + HttpStatusCode statusCode; + var matchedUrlSegment = !string.IsNullOrEmpty( properties.RouteParameter ); + + if ( matchedUrlSegment ) + { + statusCode = NotFound; + } + else + { + var versionsByUrlOnly = options.ApiVersionReader.VersionsByUrl( allowMultipleLocations: false ); + statusCode = versionsByUrlOnly ? NotFound : options.UnsupportedApiVersionStatusCode; + } + + response = CreateResponseForUnsupportedApiVersion( apiVersion, statusCode ); + } + else + { + response = CreateNotFound( conventionRouteResult ); + } } else { diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs index 3dc5df0d..25c3be0e 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs @@ -163,7 +163,7 @@ public void select_controller_should_return_correct_versionX2DneutralX2C_convent } [Fact] - public async Task select_controller_should_return_404_for_unmatchedX2C_attributeX2Dbased_controller_version() + public async Task select_controller_should_return_400_for_unmatchedX2C_attributeX2Dbased_controller_version() { // arrange var detail = "The HTTP resource that matches the request URI 'http://localhost/api/test' does not support the API version '42.0'."; @@ -190,13 +190,13 @@ public async Task select_controller_should_return_404_for_unmatchedX2C_attribute var content = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0, 4.0" ); response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "3.0-Alpha" ); content.Should().BeEquivalentTo( new ProblemDetails() { - Status = 404, + Status = 400, Title = "Unsupported API version", Type = ProblemDetailsDefaults.Unsupported.Type, Detail = detail, @@ -255,7 +255,7 @@ public async Task select_controller_should_return_400_for_attributeX2Dbased_cont } [Fact] - public async Task select_controller_should_return_404_for_unmatchedX2C_conventionX2Dbased_controller_version() + public async Task select_controller_should_return_400_for_unmatchedX2C_conventionX2Dbased_controller_version() { // arrange var detail = "The HTTP resource that matches the request URI 'http://localhost/api/test' does not support the API version '4.0'."; @@ -283,13 +283,13 @@ public async Task select_controller_should_return_404_for_unmatchedX2C_conventio var content = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "1.8, 1.9" ); content.Should().BeEquivalentTo( new ProblemDetails() { - Status = 404, + Status = 400, Title = "Unsupported API version", Type = ProblemDetailsDefaults.Unsupported.Type, Detail = detail, @@ -413,7 +413,7 @@ public void select_controller_should_return_400_when_no_version_is_specified_and } [Fact] - public void select_controller_should_return_404_for_unmatched_action() + public void select_controller_should_return_400_for_unmatched_action() { // arrange var configuration = AttributeRoutingEnabledConfiguration; @@ -433,7 +433,7 @@ public void select_controller_should_return_404_for_unmatched_action() var response = selectController.Should().Throw().Subject.Single().Response; // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0, 4.0" ); response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "3.0-Alpha" ); } diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using an endpoint.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using an endpoint.cs index 45329292..de479673 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using an endpoint.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using an endpoint.cs @@ -11,7 +11,7 @@ namespace given_a_versioned_minimal_API; public class when_using_an_endpoint : AcceptanceTest { [Theory] - [InlineData( "api/order?api-version=0.9", NotFound )] + [InlineData( "api/order?api-version=0.9", BadRequest )] [InlineData( "api/order?api-version=1.0", OK )] [InlineData( "api/order?api-version=2.0", OK )] [InlineData( "api/order/42?api-version=0.9", OK )] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs index f692f92d..7c78efd4 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs @@ -87,7 +87,7 @@ public async Task then_delete_should_return_405( string apiVersion ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -96,7 +96,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/values?api-version=3.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs index 069d4c36..081fae3c 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs @@ -29,7 +29,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -38,7 +38,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/values?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs index 18669a23..7a1f29a4 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs @@ -34,7 +34,7 @@ public async Task then_get_should_return_200( Type controllerType, string apiVer } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -43,7 +43,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/agreements/42?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs index dc1ff8bb..82aced7b 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs @@ -2,14 +2,13 @@ namespace given_a_versioned_ODataController_mixed_with_base_controllers; -using Asp.Versioning; using Asp.Versioning.OData.Advanced; using static System.Net.HttpStatusCode; public class when_people_is_any_version : AdvancedAcceptanceTest { [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { lastName = "Me" }; @@ -18,7 +17,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var response = await PatchAsync( $"api/people/42?api-version=4.0", person ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs index a42aeb03..3f51e4f7 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -37,7 +37,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/people?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] @@ -84,7 +84,7 @@ public async Task then_delete_should_return_405_for_unmatched_action() } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -93,7 +93,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var response = await PatchAsync( "api/people/42?api-version=4.0", person ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs index 94fa7576..79c317d7 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs @@ -4,6 +4,7 @@ namespace given_a_versioned_ODataController; using Asp.Versioning; using Asp.Versioning.OData.Basic; +using System.Net; using static System.Net.HttpStatusCode; public class when_using_a_query_string : BasicAcceptanceTest @@ -17,23 +18,29 @@ public async Task then_get_should_return_200( string requestUrl ) // act - var response = (await GetAsync( requestUrl )).EnsureSuccessStatusCode(); + var response = ( await GetAsync( requestUrl ) ).EnsureSuccessStatusCode(); // assert response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange - + // note: it's not clear why this is, but it appears to be a change + // in the routing system from netcoreapp3.1 to net6.0+ +#if NETCOREAPP3_1 + const HttpStatusCode StatusCode = NotFound; +#else + const HttpStatusCode StatusCode = BadRequest; +#endif // act var response = await GetAsync( "api/orders?api-version=2.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( StatusCode ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs index 3bec69f6..940703e6 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs @@ -37,7 +37,7 @@ public async Task then_the_service_document_should_be_versionX2Dspecific( string } [Fact] - public async Task then_the_service_document_should_return_404_for_an_unsupported_version() + public async Task then_the_service_document_should_return_400_for_an_unsupported_version() { // arrange @@ -46,7 +46,7 @@ public async Task then_the_service_document_should_return_404_for_an_unsupported var response = await GetAsync( "api?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] @@ -79,7 +79,7 @@ public async Task then_X24metadata_should_be_versionX2Dspecific( string apiVersi } [Fact] - public async Task then_X24metadata_should_return_404_for_an_unsupported_version() + public async Task then_X24metadata_should_return_400_for_an_unsupported_version() { // arrange @@ -88,7 +88,7 @@ public async Task then_X24metadata_should_return_404_for_an_unsupported_version( var response = await GetAsync( "api/$metadata?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } protected ODataAcceptanceTest( ODataFixture fixture ) : base( fixture ) { } diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs index 012ab31d..dca28575 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -37,7 +37,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/people?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] @@ -71,7 +71,7 @@ public async Task then_patch_should_return_400_if_supported_in_any_version( stri } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -80,7 +80,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var response = await PatchAsync( "api/people/42?api-version=4.0", person ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs index 64dd1dea..e145ac04 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs @@ -4,6 +4,7 @@ namespace given_a_versioned_ODataController_using_conventions; using Asp.Versioning; using Asp.Versioning.OData.UsingConventions; +using System.Net; using static System.Net.HttpStatusCode; public class when_using_a_query_string : ConventionsAcceptanceTest @@ -24,16 +25,22 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange - + // note: it's not clear why this is, but it appears to be a change + // in the routing system from netcoreapp3.1 to net6.0+ +#if NETCOREAPP3_1 + const HttpStatusCode StatusCode = NotFound; +#else + const HttpStatusCode StatusCode = BadRequest; +#endif // act var response = await GetAsync( "api/orders?api-version=2.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( StatusCode ); } [Fact] diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs index 47fdad17..46b93a13 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs @@ -93,7 +93,7 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) if ( !matched && hasCandidates && !DifferByRouteConstraintsOnly( candidates ) ) { - var builder = new ClientErrorEndpointBuilder( feature, candidates, logger ); + var builder = new ClientErrorEndpointBuilder( feature, candidates, Options, logger ); httpContext.SetEndpoint( builder.Build() ); } @@ -108,19 +108,23 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList( capacity ); var source = ApiVersionSource; - var versionsByUrl = source.VersionsByUrl(); - var routePatterns = default( List ); + var supported = default( SortedSet ); + var deprecated = default( SortedSet ); + var routePatterns = default( RoutePattern[] ); for ( var i = 0; i < edges.Count; i++ ) { var edge = edges[i]; var state = (EdgeKey) edge.State; - var version = state.ApiVersion; + + if ( Options.ReportApiVersions ) + { + Collate( state.Metadata, ref supported, ref deprecated ); + } switch ( state.EndpointType ) { @@ -133,6 +137,9 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList 0 ) - { - routePatterns ??= new(); - routePatterns.AddRange( state.RoutePatterns ); - } - - destinations.Add( version, edge.Destination ); + // the route patterns provided to each edge is a + // singleton so any edge will do + routePatterns ??= state.RoutePatterns.ToArray(); + destinations.Add( state.ApiVersion, edge.Destination ); break; } } @@ -157,7 +161,8 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList) Array.Empty(), + NewPolicyFeature( supported, deprecated ), + routePatterns ?? Array.Empty(), apiVersionParser, source, Options ); @@ -174,8 +179,8 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints var capacity = endpoints.Count; var builder = new EdgeBuilder( capacity, ApiVersionSource, Options, logger ); var versions = new SortedSet(); - var neutralEndpoints = default( List ); - var versionedEndpoints = new (RouteEndpoint, ApiVersionModel)[capacity]; + var neutralEndpoints = default( List<(RouteEndpoint, ApiVersionMetadata)> ); + var versionedEndpoints = new (RouteEndpoint, ApiVersionModel, ApiVersionMetadata)[capacity]; var count = 0; for ( var i = 0; i < endpoints.Count; i++ ) @@ -190,14 +195,14 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints if ( model.IsApiVersionNeutral ) { - builder.Add( endpoint, ApiVersion.Neutral ); + builder.Add( endpoint, ApiVersion.Neutral, metadata ); neutralEndpoints ??= new(); - neutralEndpoints.Add( endpoint ); + neutralEndpoints.Add( (endpoint, metadata) ); } else { builder.Add( endpoint ); - versionedEndpoints[count++] = (endpoint, model); + versionedEndpoints[count++] = (endpoint, model, metadata); versions.AddRange( model.DeclaredApiVersions ); } } @@ -206,12 +211,12 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints { for ( var j = 0; j < count; j++ ) { - var (endpoint, model) = versionedEndpoints[j]; + var (endpoint, model, metadata) = versionedEndpoints[j]; var mappedWithImplementation = model.ImplementedApiVersions.Contains( version ); if ( mappedWithImplementation ) { - builder.Add( endpoint, version ); + builder.Add( endpoint, version, metadata ); } } @@ -223,7 +228,8 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints // add an edge for all known versions because version-neutral endpoints can map to any api version for ( var j = 0; j < neutralEndpoints.Count; j++ ) { - builder.Add( neutralEndpoints[j], version ); + var (endpoint, metadata) = neutralEndpoints[j]; + builder.Add( endpoint, version, metadata ); } } @@ -293,6 +299,77 @@ private static bool DifferByRouteConstraintsOnly( CandidateSet candidates ) return false; } + private static void Collate( + ApiVersionMetadata metadata, + ref SortedSet? supported, + ref SortedSet? deprecated ) + { + var model = metadata.Map( Implicit | Explicit ); + var versions = model.SupportedApiVersions; + + if ( versions.Count > 0 ) + { + supported ??= new(); + + for ( var j = 0; j < versions.Count; j++ ) + { + supported.Add( versions[j] ); + } + } + + versions = model.DeprecatedApiVersions; + + if ( versions.Count == 0 ) + { + return; + } + + deprecated ??= new(); + + for ( var j = 0; j < versions.Count; j++ ) + { + deprecated.Add( versions[j] ); + } + } + + private static ApiVersionPolicyFeature? NewPolicyFeature( + SortedSet? supported, + SortedSet? deprecated ) + { + // this is a best guess effort at collating all supported and deprecated + // versions for an api when unmatched and it needs to be reported. it's + // impossible to sure as there is no way to correlate an arbitrary + // request url by endpoint or name. the routing system will build a tree + // based on the route template before the jump table policy is created, + // which provides a natural method of grouping. manual, contrived tests + // demonstrated that were the results were correctly collated together. + // it is possible there is an edge case that isn't covered, but it's + // unclear what that would look like. one or more test cases should be + // added to document that if discovered + ApiVersionModel model; + + if ( supported == null ) + { + if ( deprecated == null ) + { + return default; + } + + model = new( Enumerable.Empty(), deprecated ); + } + else if ( deprecated == null ) + { + model = new( supported, Enumerable.Empty() ); + } + else + { + deprecated.ExceptWith( supported ); + model = new( supported, deprecated ); + } + + return new( new( model, model ) ); + } + private static (bool Matched, bool HasCandidates) MatchApiVersion( CandidateSet candidates, ApiVersion? apiVersion ) { var total = candidates.Count; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyFeature.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyFeature.cs new file mode 100644 index 00000000..d6ca9ed7 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyFeature.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Routing; + +internal sealed class ApiVersionPolicyFeature +{ + public ApiVersionPolicyFeature( ApiVersionMetadata metadata ) => Metadata = metadata; + + public ApiVersionMetadata Metadata { get; } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs index ed8db5bd..a7eb03da 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs @@ -15,6 +15,7 @@ internal sealed class ApiVersionPolicyJumpTable : PolicyJumpTable private readonly bool versionsByMediaTypeOnly; private readonly RouteDestination rejection; private readonly IReadOnlyDictionary destinations; + private readonly ApiVersionPolicyFeature? policyFeature; private readonly IReadOnlyList routePatterns; private readonly IApiVersionParser parser; private readonly ApiVersioningOptions options; @@ -22,6 +23,7 @@ internal sealed class ApiVersionPolicyJumpTable : PolicyJumpTable internal ApiVersionPolicyJumpTable( RouteDestination rejection, IReadOnlyDictionary destinations, + ApiVersionPolicyFeature? policyFeature, IReadOnlyList routePatterns, IApiVersionParser parser, IApiVersionParameterSource source, @@ -29,6 +31,7 @@ internal ApiVersionPolicyJumpTable( { this.rejection = rejection; this.destinations = destinations; + this.policyFeature = policyFeature; this.routePatterns = routePatterns; this.parser = parser; this.options = options; @@ -42,6 +45,7 @@ public override int GetDestination( HttpContext httpContext ) var request = httpContext.Request; var feature = httpContext.ApiVersioningFeature(); var apiVersions = new List( capacity: feature.RawRequestedApiVersions.Count + 1 ); + var addedFromUrl = false; apiVersions.AddRange( feature.RawRequestedApiVersions ); @@ -50,6 +54,7 @@ public override int GetDestination( HttpContext httpContext ) DoesNotContainApiVersion( apiVersions, rawApiVersion ) ) { apiVersions.Add( rawApiVersion ); + addedFromUrl = apiVersions.Count == apiVersions.Capacity; } int destination; @@ -83,6 +88,11 @@ public override int GetDestination( HttpContext httpContext ) if ( versionsByUrl ) { feature.RawRequestedApiVersion = rawApiVersion; + + if ( versionsByUrlOnly ) + { + return rejection.Exit; // 404 + } } return rejection.Malformed; // 400 @@ -93,6 +103,8 @@ public override int GetDestination( HttpContext httpContext ) return destination; } + httpContext.Features.Set( policyFeature ); + if ( versionsByMediaTypeOnly ) { if ( request.Headers.ContainsKey( HeaderNames.ContentType ) ) @@ -103,11 +115,11 @@ public override int GetDestination( HttpContext httpContext ) return rejection.NotAcceptable; // 406 } - return rejection.Exit; // 404 + return addedFromUrl + /* 404 */ ? rejection.Exit + /* 400 */ : rejection.Unsupported; } - var addedFromUrl = apiVersions.Count == apiVersions.Capacity; - if ( addedFromUrl ) { feature.RawRequestedApiVersions = apiVersions; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs index cef85a64..8077e8f9 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs @@ -6,21 +6,23 @@ namespace Asp.Versioning.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Logging; -using System.Text; internal sealed class ClientErrorEndpointBuilder { private readonly IApiVersioningFeature feature; private readonly CandidateSet candidates; + private readonly ApiVersioningOptions options; private readonly ILogger logger; public ClientErrorEndpointBuilder( IApiVersioningFeature feature, CandidateSet candidates, + ApiVersioningOptions options, ILogger logger ) { this.feature = feature; this.candidates = candidates; + this.options = options; this.logger = logger; } @@ -31,7 +33,7 @@ public Endpoint Build() return new UnspecifiedApiVersionEndpoint( logger, GetDisplayNames() ); } - return new UnsupportedApiVersionEndpoint(); + return new UnsupportedApiVersionEndpoint( options ); } private static string DisplayName( Endpoint endpoint ) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs index 7663a809..010210f7 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs @@ -10,13 +10,15 @@ namespace Asp.Versioning.Routing; internal sealed class EdgeBuilder { + private const int RejectionEndpointCapacity = NumberOfRejectionEndpoints + 1; + internal const int NumberOfRejectionEndpoints = 6; private readonly bool versionsByUrl; - private readonly bool unspecifiedNotAllowed; + private readonly bool unspecifiedAllowed; private readonly string constraintName; private readonly HashSet keys; private readonly Dictionary> edges; + private readonly HashSet routePatterns = new( new RoutePatternComparer() ); private EdgeKey assumeDefault = EdgeKey.AssumeDefault; - private HashSet? routePatterns; public EdgeBuilder( int capacity, @@ -25,35 +27,41 @@ public EdgeBuilder( ILogger logger ) { versionsByUrl = source.VersionsByUrl(); - unspecifiedNotAllowed = !options.AssumeDefaultVersionWhenUnspecified; + unspecifiedAllowed = options.AssumeDefaultVersionWhenUnspecified; constraintName = options.RouteConstraintName; keys = new( capacity + 1 ); - edges = new( capacity + 6 ) + edges = new( capacity + RejectionEndpointCapacity ) { [EdgeKey.Malformed] = new( capacity: 1 ) { new MalformedApiVersionEndpoint( logger ) }, [EdgeKey.Ambiguous] = new( capacity: 1 ) { new AmbiguousApiVersionEndpoint( logger ) }, [EdgeKey.Unspecified] = new( capacity: 1 ) { new UnspecifiedApiVersionEndpoint( logger ) }, - [EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint() }, - [EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint() }, + [EdgeKey.Unsupported] = new( capacity: 1 ) { new UnsupportedApiVersionEndpoint( options ) }, + [EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint( options ) }, + [EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint( options ) }, }; } - public IReadOnlyList Build() => - edges.Select( edge => new PolicyNodeEdge( edge.Key, edge.Value ) ).ToArray(); + public IReadOnlyList Build() + { + routePatterns.TrimExcess(); + return edges.Select( edge => new PolicyNodeEdge( edge.Key, edge.Value ) ).ToArray(); + } public void Add( RouteEndpoint endpoint ) { - if ( unspecifiedNotAllowed ) + if ( unspecifiedAllowed ) { - return; + Add( ref assumeDefault, endpoint ); } - - Add( ref assumeDefault, endpoint ); } - public void Add( RouteEndpoint endpoint, ApiVersion apiVersion ) + public void Add( RouteEndpoint endpoint, ApiVersion apiVersion, ApiVersionMetadata metadata ) { - var key = new EdgeKey( apiVersion ); + // use a singleton of all route patterns that version by url segment. this + // is needed to extract the value for selecting a destination in the jump + // table. any matching template will do and every edge should have the + // same list known through the application, which may be zero + var key = new EdgeKey( apiVersion, metadata, routePatterns ); Add( ref key, endpoint ); } @@ -73,13 +81,7 @@ private void Add( ref EdgeKey key, RouteEndpoint endpoint ) if ( needsRoutePattern ) { - routePatterns ??= new( new RoutePatternComparer() ); - needsRoutePattern &= routePatterns.Add( routePattern ); - - if ( needsRoutePattern ) - { - key.RoutePatterns.Add( routePattern ); - } + routePatterns.Add( routePattern ); } if ( !edges.TryGetValue( key, out var endpoints ) ) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs index bc8208a6..bdc5c835 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs @@ -9,43 +9,60 @@ namespace Asp.Versioning.Routing; internal readonly struct EdgeKey : IEquatable { public readonly ApiVersion ApiVersion; - public readonly List RoutePatterns; + public readonly ApiVersionMetadata Metadata; + public readonly HashSet RoutePatterns; public readonly EndpointType EndpointType; - private EdgeKey( EndpointType endpointType, List routePatterns ) + private EdgeKey( EndpointType endpointType, HashSet routePatterns ) { ApiVersion = ApiVersion.Default; + Metadata = ApiVersionMetadata.Empty; RoutePatterns = routePatterns; EndpointType = endpointType; } - internal EdgeKey( ApiVersion apiVersion ) + internal EdgeKey( + ApiVersion apiVersion, + ApiVersionMetadata metadata, + HashSet routePatterns ) { ApiVersion = apiVersion; - RoutePatterns = new(); + Metadata = metadata; + RoutePatterns = routePatterns; EndpointType = UserDefined; } - internal static EdgeKey Ambiguous => new( EndpointType.Ambiguous, new( capacity: 0 ) ); + internal static EdgeKey Ambiguous => new( EndpointType.Ambiguous, Set.Empty ); + + internal static EdgeKey Malformed => new( EndpointType.Malformed, Set.Empty ); - internal static EdgeKey Malformed => new( EndpointType.Malformed, new( capacity: 0 ) ); + internal static EdgeKey Unspecified => new( EndpointType.Unspecified, Set.Empty ); - internal static EdgeKey Unspecified => new( EndpointType.Unspecified, new( capacity: 0 ) ); + internal static EdgeKey Unsupported => new( EndpointType.Unsupported, Set.Empty ); - internal static EdgeKey UnsupportedMediaType => new( EndpointType.UnsupportedMediaType, new( capacity: 0 ) ); + internal static EdgeKey UnsupportedMediaType => new( EndpointType.UnsupportedMediaType, Set.Empty ); - internal static EdgeKey NotAcceptable => new( EndpointType.NotAcceptable, new( capacity: 0 ) ); + internal static EdgeKey NotAcceptable => new( EndpointType.NotAcceptable, Set.Empty ); - internal static EdgeKey AssumeDefault => new( EndpointType.AssumeDefault, new() ); + internal static EdgeKey AssumeDefault => new( EndpointType.AssumeDefault, new( new RoutePatternComparer() ) ); public bool Equals( [AllowNull] EdgeKey other ) => GetHashCode() == other.GetHashCode(); public override bool Equals( object? obj ) => obj is EdgeKey other && Equals( other ); - public override int GetHashCode() => - EndpointType == UserDefined ? - HashCode.Combine( ApiVersion, EndpointType ) : - EndpointType.GetHashCode(); + public override int GetHashCode() + { + var result = default( HashCode ); + + result.Add( EndpointType ); + + if ( EndpointType == UserDefined ) + { + result.Add( ApiVersion ); + } + + return result.ToHashCode(); + } public override string ToString() { @@ -66,4 +83,9 @@ public override string ToString() return "VER: " + value; } + + private static class Set + { + public static readonly HashSet Empty = new( capacity: 0 ); + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs index ea7b4f10..8ad75c35 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs @@ -9,7 +9,10 @@ namespace Asp.Versioning.Routing; internal static class EndpointProblem { - internal static Task UnsupportedApiVersion( HttpContext context, int statusCode ) + internal static Task UnsupportedApiVersion( + HttpContext context, + ApiVersioningOptions options, + int statusCode ) { var services = context.RequestServices; var factory = services.GetRequiredService(); @@ -30,9 +33,24 @@ internal static Task UnsupportedApiVersion( HttpContext context, int statusCode context.Response.StatusCode = statusCode; + if ( options.ReportApiVersions && + context.Features.Get() is ApiVersionPolicyFeature feature ) + { + var reporter = services.GetRequiredService(); + var model = feature.Metadata.Map( reporter.Mapping ); + context.Response.OnStarting( ReportApiVersions, (reporter, context.Response, model) ); + } + return context.Response.WriteAsJsonAsync( problem, options: default, contentType: ProblemDetailsDefaults.MediaType.Json ); } + + private static Task ReportApiVersions( object state ) + { + var (reporter, response, model) = ((IReportApiVersions, HttpResponse, ApiVersionModel)) state; + reporter.Report( response, model ); + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs index f871400a..73f0c1bd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs @@ -11,4 +11,5 @@ internal enum EndpointType UnsupportedMediaType, AssumeDefault, NotAcceptable, + Unsupported, } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs index f6b4cdbe..51731b1e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs @@ -9,8 +9,12 @@ internal sealed class NotAcceptableEndpoint : Endpoint { private const string Name = "406 HTTP Not Acceptable"; - internal NotAcceptableEndpoint() : base( OnExecute, Empty, Name ) { } - - private static Task OnExecute( HttpContext context ) => - EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status406NotAcceptable ); + internal NotAcceptableEndpoint( ApiVersioningOptions options ) + : base( + context => EndpointProblem.UnsupportedApiVersion( + context, + options, + StatusCodes.Status406NotAcceptable ), + Empty, + Name ) { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs index bda9c9d2..39dcbb64 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs @@ -8,6 +8,7 @@ internal struct RouteDestination public int Malformed; public int Ambiguous; public int Unspecified; + public int Unsupported; public int UnsupportedMediaType; public int AssumeDefault; public int NotAcceptable; @@ -18,6 +19,7 @@ public RouteDestination( int exit ) Malformed = exit; Ambiguous = exit; Unspecified = exit; + Unsupported = exit; UnsupportedMediaType = exit; AssumeDefault = exit; NotAcceptable = exit; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs index 8a1661ab..1cd83a42 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs @@ -7,10 +7,15 @@ namespace Asp.Versioning.Routing; internal sealed class UnsupportedApiVersionEndpoint : Endpoint { - private const string Name = "400 Unsupported API Version"; + private const string Name = " Unsupported API Version"; - internal UnsupportedApiVersionEndpoint() : base( OnExecute, Empty, Name ) { } - - private static Task OnExecute( HttpContext context ) => - EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status400BadRequest ); + internal UnsupportedApiVersionEndpoint( ApiVersioningOptions options ) + : base( + context => EndpointProblem.UnsupportedApiVersion( + context, + options, + options.UnsupportedApiVersionStatusCode ), + Empty, + options.UnsupportedApiVersionStatusCode + Name ) + { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs index b164bf20..1e7492e6 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs @@ -9,8 +9,12 @@ internal sealed class UnsupportedMediaTypeEndpoint : Endpoint { private const string Name = "415 HTTP Unsupported Media Type"; - internal UnsupportedMediaTypeEndpoint() : base( OnExecute, Empty, Name ) { } - - private static Task OnExecute( HttpContext context ) => - EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status415UnsupportedMediaType ); + internal UnsupportedMediaTypeEndpoint( ApiVersioningOptions options ) + : base( + context => EndpointProblem.UnsupportedApiVersion( + context, + options, + StatusCodes.Status415UnsupportedMediaType ), + Empty, + Name ) { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs index 301385cd..a42ee32c 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs @@ -8,6 +8,7 @@ namespace Asp.Versioning; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; public class ReportApiVersionsAttributeTest { @@ -85,7 +86,10 @@ private static ActionExecutingContext CreateContext( versioningFeature.SetupProperty( f => f.RequestedApiVersion, new ApiVersion( 1.0 ) ); features.Set( endpointFeature.Object ); features.Set( versioningFeature.Object ); - serviceProvider.Setup( sp => sp.GetService( typeof( IReportApiVersions ) ) ).Returns( new DefaultApiVersionReporter() ); + serviceProvider.Setup( sp => sp.GetService( typeof( IReportApiVersions ) ) ) + .Returns( new DefaultApiVersionReporter() ); + serviceProvider.Setup( sp => sp.GetService( typeof( ISunsetPolicyManager ) ) ) + .Returns( new SunsetPolicyManager( Options.Create( new ApiVersioningOptions() ) ) ); response.SetupGet( r => r.Headers ).Returns( headers ); response.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object ); response.Setup( r => r.OnStarting( It.IsAny>(), It.IsAny() ) ) From e6801da0c142b191ba1a7e258d9f7c3f03895ef8 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Nov 2022 16:54:15 -0800 Subject: [PATCH 13/35] Update version and release notes --- .../Asp.Versioning.WebApi.OData.ApiExplorer.csproj | 4 ++-- .../Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt | 2 +- .../Asp.Versioning.WebApi.OData.csproj | 4 ++-- .../OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt | 2 +- .../Asp.Versioning.WebApi.ApiExplorer.csproj | 4 ++-- .../src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt | 2 +- .../src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj | 4 ++-- src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt | 2 +- .../Asp.Versioning.OData.ApiExplorer.csproj | 4 ++-- .../src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt | 2 +- .../src/Asp.Versioning.OData/Asp.Versioning.OData.csproj | 4 ++-- .../OData/src/Asp.Versioning.OData/ReleaseNotes.txt | 2 +- .../WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj | 4 ++-- .../WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt | 2 +- .../Asp.Versioning.Mvc.ApiExplorer.csproj | 4 ++-- .../src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt | 2 +- .../WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj | 4 ++-- src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt | 2 +- 18 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj index 44cd9e06..2160674c 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net45;net472 Asp.Versioning ASP.NET Web API Versioning API Explorer for OData v4.0 diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj index 504275fe..81b9d9aa 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net45;net472 Asp.Versioning API Versioning for ASP.NET Web API with OData v4.0 diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj index c04f1fe3..f983cef6 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net45;net472 ASP.NET Web API Versioning API Explorer The API Explorer extensions for ASP.NET Web API Versioning. diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj index b2f49ecd..0b37b2ca 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net45;net472 ASP.NET Web API Versioning A service API versioning library for Microsoft ASP.NET Web API. diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj index 9d30402c..955a0d05 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning API Explorer for OData v4.0 diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj index db8d2f56..23be8a44 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning with OData v4.0 diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj index 0224ba9c..dce50659 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj @@ -1,8 +1,8 @@  - 6.2.2 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt index 696f3a30..5f282702 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt @@ -1 +1 @@ -[Fix #911](https://github.com/dotnet/aspnet-api-versioning/issues/911) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj index 6e9c4dd0..c8d61eec 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning.ApiExplorer ASP.NET Core API Versioning API Explorer diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj index df0ec3fb..3ed03b13 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 6.3.0 + 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file From faf18a51e36e07cff8bcb57ddc6d07881527289f Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Nov 2022 18:00:23 -0800 Subject: [PATCH 14/35] Revert CA suppression; required for .NET 6 SDK --- .../Dispatcher/HttpResponseExceptionFactory.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs index 4afb0774..f79875b9 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable CA2000 // Dispose objects before losing scope + namespace Asp.Versioning.Dispatcher; using System.Globalization; From c86d2a467e611ca4266b317ff86a467c856fd42b Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 21 Nov 2022 17:51:56 -0800 Subject: [PATCH 15/35] Correct regression from custom group names. Fixes #923 --- .../VersionedApiDescriptionProvider.cs | 4 +- .../TestActionDescriptorCollectionProvider.cs | 20 +++++++ .../VersionedApiDescriptionProviderTest.cs | 57 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs index e71c67d0..c84f9d06 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs @@ -156,11 +156,11 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) var groupResult = result.Clone(); var metadata = action.GetApiVersionMetadata(); - if ( string.IsNullOrEmpty( groupResult.GroupName ) || formatGroupName is null ) + if ( string.IsNullOrEmpty( groupResult.GroupName ) ) { groupResult.GroupName = formattedVersion; } - else + else if ( formatGroupName is not null ) { groupResult.GroupName = formatGroupName( groupResult.GroupName, formattedVersion ); } diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs index 3361f93e..0ccce57b 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs @@ -9,6 +9,26 @@ internal sealed class TestActionDescriptorCollectionProvider : IActionDescriptor { private readonly Lazy collection = new( CreateActionDescriptors ); + public TestActionDescriptorCollectionProvider() { } + + public TestActionDescriptorCollectionProvider( ActionDescriptor action, params ActionDescriptor[] otherActions ) + { + ActionDescriptor[] actions; + + if ( otherActions.Length == 0 ) + { + actions = new ActionDescriptor[] { action }; + } + else + { + actions = new ActionDescriptor[otherActions.Length]; + actions[0] = action; + Array.Copy( otherActions, 0, actions, 1, otherActions.Length ); + } + + collection = new( () => new( actions, 0 ) ); + } + public ActionDescriptorCollection ActionDescriptors => collection.Value; private static ActionDescriptorCollection CreateActionDescriptors() diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs index f5eec3d5..45a50e05 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs @@ -2,6 +2,7 @@ namespace Asp.Versioning.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; @@ -92,6 +93,62 @@ public void versioned_api_explorer_should_apply_sunset_policy() .BeTrue(); } + [Fact] + public void versioned_api_explorer_should_preserve_group_name() + { + // arrange + var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( ApiVersion.Default ) ); + var descriptor = new ActionDescriptor() { EndpointMetadata = new[] { metadata } }; + var actionProvider = new TestActionDescriptorCollectionProvider( descriptor ); + var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); + var apiExplorer = new VersionedApiDescriptionProvider( + Mock.Of(), + NewModelMetadataProvider(), + Options.Create( new ApiExplorerOptions() ) ); + + context.Results.Add( new() + { + ActionDescriptor = descriptor, + GroupName = "Test", + } ); + + // act + apiExplorer.OnProvidersExecuted( context ); + + // assert + context.Results.Single().GroupName.Should().Be( "Test" ); + } + + [Fact] + public void versioned_api_explorer_should_use_custom_group_name() + { + // arrange + var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( ApiVersion.Default ) ); + var descriptor = new ActionDescriptor() { EndpointMetadata = new[] { metadata } }; + var actionProvider = new TestActionDescriptorCollectionProvider( descriptor ); + var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); + var options = new ApiExplorerOptions() + { + FormatGroupName = ( group, version ) => $"{group}-{version}", + }; + var apiExplorer = new VersionedApiDescriptionProvider( + Mock.Of(), + NewModelMetadataProvider(), + Options.Create( options ) ); + + context.Results.Add( new() + { + ActionDescriptor = descriptor, + GroupName = "Test", + } ); + + // act + apiExplorer.OnProvidersExecuted( context ); + + // assert + context.Results.Single().GroupName.Should().Be( "Test-1.0" ); + } + private static IModelMetadataProvider NewModelMetadataProvider() { var provider = new Mock(); From a37a143021108d668335112b72b6d635723148e0 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 26 Nov 2022 11:08:04 -0800 Subject: [PATCH 16/35] Refactor to support API version metadata collation via DI. Fixes #922 --- .../ApiVersionMetadataCollationCollection.cs | 135 ++++++++++ .../ApiVersionMetadataCollationContext.cs | 15 ++ ...ointApiVersionMetadataCollationProvider.cs | 59 +++++ .../IApiVersionMetadataCollationProvider.cs | 21 ++ .../IServiceCollectionExtensions.cs | 12 +- .../Routing/ApiVersionMatcherPolicy.cs | 123 ++++++++- .../DefaultApiVersionDescriptionProvider.cs | 206 +++------------ .../GroupedApiVersionDescriptionProvider.cs | 236 +++--------------- ...tionApiVersionMetadataCollationProvider.cs | 65 +++++ .../Asp.Versioning.Mvc/ApiVersionCollator.cs | 2 - .../IApiVersioningBuilderExtensions.cs | 2 + 11 files changed, 497 insertions(+), 379 deletions(-) create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionMetadataCollationProvider.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs new file mode 100644 index 00000000..ee58aed9 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using System.Collections; + +/// +/// Represents a collection of collated API version metadata. +/// +public class ApiVersionMetadataCollationCollection : IList, IReadOnlyList +{ + private readonly List items; + private readonly List groups; + + /// + /// Initializes a new instance of the class. + /// + public ApiVersionMetadataCollationCollection() + { + items = new(); + groups = new(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial capacity of the collection. + public ApiVersionMetadataCollationCollection( int capacity ) + { + items = new( capacity ); + groups = new( capacity ); + } + + /// + /// Gets the item in the list at the specified index. + /// + /// The zero-based index of the item to retrieve. + /// The item at the specified index. + public ApiVersionMetadata this[int index] => items[index]; + + ApiVersionMetadata IList.this[int index] + { + get => items[index]; + set => throw new NotSupportedException(); + } + + /// + public int Count => items.Count; + +#pragma warning disable CA1033 // Interface methods should be callable by child types + bool ICollection.IsReadOnly => ( (ICollection) items ).IsReadOnly; +#pragma warning restore CA1033 // Interface methods should be callable by child types + + /// + public void Add( ApiVersionMetadata item ) => Insert( Count, item, default ); + + /// + /// Adds an item to the collection. + /// + /// The item to add. + /// The associated group name, if any. + public void Add( ApiVersionMetadata item, string? groupName ) => Insert( Count, item, groupName ); + + /// + public void Clear() + { + items.Clear(); + groups.Clear(); + } + + /// + public bool Contains( ApiVersionMetadata item ) => item != null && items.Contains( item ); + + /// + public void CopyTo( ApiVersionMetadata[] array, int arrayIndex ) => items.CopyTo( array, arrayIndex ); + + /// + public IEnumerator GetEnumerator() => items.GetEnumerator(); + + /// + public int IndexOf( ApiVersionMetadata item ) => item == null ? -1 : items.IndexOf( item ); + + /// + public void Insert( int index, ApiVersionMetadata item ) => Insert( index, item, default ); + + /// + /// Inserts an item into the collection. + /// + /// The zero-based index where insertion takes place. + /// The item to insert. + /// The associated group name, if any. + public void Insert( int index, ApiVersionMetadata item, string? groupName ) + { + items.Insert( index, item ?? throw new ArgumentNullException( nameof( item ) ) ); + groups.Insert( index, groupName ); + } + + /// + public bool Remove( ApiVersionMetadata item ) + { + if ( item == null ) + { + return false; + } + + var index = items.IndexOf( item ); + + if ( index < 0 ) + { + return false; + } + + RemoveAt( index ); + return true; + } + + /// + public void RemoveAt( int index ) + { + items.RemoveAt( index ); + groups.RemoveAt( index ); + } + + IEnumerator IEnumerable.GetEnumerator() => ( (IEnumerable) items ).GetEnumerator(); + + /// + /// Gets the group name for the item at the specified index. + /// + /// The zero-based index of the item to get the group name for. + /// The associated group name or null. + /// If the specified is out of range, null + /// is returned. + public string? GroupName( int index ) => + index < 0 || index >= groups.Count ? default : groups[index]; +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs new file mode 100644 index 00000000..dc1dfa25 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +/// +/// Represents the context used during API version metadata collation. +/// +public class ApiVersionMetadataCollationContext +{ + /// + /// Gets the read-only list of collation results. + /// + /// The read-only list of collation results. + public ApiVersionMetadataCollationCollection Results { get; } = new(); +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs new file mode 100644 index 00000000..370d4f39 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; + +/// +/// Represents the API version metadata collection provider for endpoints. +/// +[CLSCompliant( false )] +public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider +{ + private readonly EndpointDataSource endpointDataSource; + private int version; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying endpoint data source. + public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource ) + { + this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) ); + ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version ); + } + + /// + public int Version => version; + + /// + public void Execute( ApiVersionMetadataCollationContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + var endpoints = endpointDataSource.Endpoints; + + for ( var i = 0; i < endpoints.Count; i++ ) + { + var endpoint = endpoints[i]; + + if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item ) + { + continue; + } + +#if NETCOREAPP3_1 + // this code path doesn't appear to exist for netcoreapp3.1 + // REF: https://github.com/dotnet/aspnetcore/blob/release/3.1/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs#L74 + context.Results.Add( item ); +#else + var groupName = endpoint.Metadata.OfType().LastOrDefault()?.EndpointGroupName; + context.Results.Add( item, groupName ); +#endif + } + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionMetadataCollationProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionMetadataCollationProvider.cs new file mode 100644 index 00000000..04a386a6 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionMetadataCollationProvider.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +/// +/// Defines the behavior of an API version metadata collation provider. +/// +public interface IApiVersionMetadataCollationProvider +{ + /// + /// Gets version of the underlying provider results. + /// + /// The version of the provider results. This can be used to detect changes. + int Version { get; } + + /// + /// Executes the provider using the given context. + /// + /// The collation context. + void Execute( ApiVersionMetadataCollationContext context ); +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs index 48da8a2b..a31dfe04 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -3,12 +3,14 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; +using Asp.Versioning.ApiExplorer; #if !NETCOREAPP3_1 using Asp.Versioning.Builder; #endif using Asp.Versioning.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; @@ -61,7 +63,15 @@ private static void AddApiVersioningServices( IServiceCollection services ) services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddEnumerable( Transient, ApiVersioningRouteOptionsSetup>() ); - services.TryAddEnumerable( Singleton() ); + //// UNDONE: explicit constructor choice to avoid breaking change; revert in next major release + services.TryAddEnumerable( Singleton( + sp => new ApiVersionMatcherPolicy( + sp.GetRequiredService(), + sp.GetServices(), + sp.GetRequiredService>(), + sp.GetRequiredService>() ) ) ); + + services.TryAddEnumerable( Singleton() ); services.Replace( WithLinkGeneratorDecorator( services ) ); } diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs index 46b93a13..78a9cf4d 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs @@ -2,6 +2,7 @@ namespace Asp.Versioning.Routing; +using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; @@ -22,6 +23,7 @@ public sealed class ApiVersionMatcherPolicy : MatcherPolicy, IEndpointSelectorPo private readonly IOptions options; private readonly IApiVersionParser apiVersionParser; private readonly ILogger logger; + private readonly ApiVersionCollator? collator; /// /// Initializes a new instance of the class. @@ -39,6 +41,20 @@ public ApiVersionMatcherPolicy( this.logger = logger ?? throw new ArgumentNullException( nameof( logger ) ); } + // TODO: avoid a breaking change or surface area change in 6.3; unify and make it public in 7.0 + // the functionality is still achievable for extenders + internal ApiVersionMatcherPolicy( + IApiVersionParser apiVersionParser, + IEnumerable providers, + IOptions options, + ILogger logger ) + { + this.apiVersionParser = apiVersionParser; + this.options = options; + this.logger = logger; + collator = new( providers, options ); + } + /// public override int Order { get; } = BeforeDefaultMatcherPolicy(); @@ -219,17 +235,21 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints builder.Add( endpoint, version, metadata ); } } + } - if ( neutralEndpoints is null ) - { - continue; - } + if ( neutralEndpoints != null && collator != null ) + { + var allVersions = collator.Items; // add an edge for all known versions because version-neutral endpoints can map to any api version - for ( var j = 0; j < neutralEndpoints.Count; j++ ) + for ( var i = 0; i < neutralEndpoints.Count; i++ ) { - var (endpoint, metadata) = neutralEndpoints[j]; - builder.Add( endpoint, version, metadata ); + var (endpoint, metadata) = neutralEndpoints[i]; + + for ( var j = 0; j < allVersions.Count; j++ ) + { + builder.Add( endpoint, allVersions[j], metadata ); + } } } @@ -498,4 +518,93 @@ internal int CompareTo( in Match other ) return result == 0 ? IsExplicit.CompareTo( other.IsExplicit ) : result; } } + + private sealed class ApiVersionCollator + { + private readonly IApiVersionMetadataCollationProvider[] providers; + private readonly IOptions options; + private readonly object syncRoot = new(); + private IReadOnlyList? items; + private int version; + + internal ApiVersionCollator( + IEnumerable providers, + IOptions options ) + { + this.providers = providers.ToArray(); + this.options = options; + } + + public IReadOnlyList Items + { + get + { + if ( items is not null && version == ComputeVersion() ) + { + return items; + } + + lock ( syncRoot ) + { + var currentVersion = ComputeVersion(); + + if ( items is not null && version == currentVersion ) + { + return items; + } + + var context = new ApiVersionMetadataCollationContext(); + + for ( var i = 0; i < providers.Length; i++ ) + { + providers[i].Execute( context ); + } + + var results = context.Results; + var versions = new SortedSet(); + + for ( var i = 0; i < results.Count; i++ ) + { + var model = results[i].Map( Explicit | Implicit ); + var declared = model.DeclaredApiVersions; + + for ( var j = 0; j < declared.Count; j++ ) + { + versions.Add( declared[j] ); + } + } + + if ( versions.Count == 0 ) + { + versions.Add( options.Value.DefaultApiVersion ); + } + + items = versions.ToArray(); + version = currentVersion; + } + + return items; + } + } + + private int ComputeVersion() => + providers.Length switch + { + 0 => 0, + 1 => providers[0].Version, + _ => ComputeVersion( providers ), + }; + + private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers ) + { + var hash = default( HashCode ); + + for ( var i = 0; i < providers.Length; i++ ) + { + hash.Add( providers[i].Version ); + } + + return hash.ToHashCode(); + } + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs index 1edc2155..a3921e13 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs @@ -3,11 +3,9 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using static Asp.Versioning.ApiVersionMapping; using static System.Globalization.CultureInfo; @@ -35,7 +33,16 @@ public DefaultApiVersionDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, endpointDataSource, actionDescriptorCollectionProvider ); + var collators = new IApiVersionMetadataCollationProvider[] + { + new EndpointApiVersionMetadataCollationProvider( + endpointDataSource ?? + throw new ArgumentNullException( nameof( endpointDataSource ) ) ), + new ActionApiVersionMetadataCollationProvider( + actionDescriptorCollectionProvider ?? + throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ) ), + }; + collection = new( this, collators ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -138,56 +145,45 @@ private void AppendDescriptions( ICollection descriptions private sealed class ApiVersionDescriptionCollection { private readonly object syncRoot = new(); - private readonly DefaultApiVersionDescriptionProvider apiVersionDescriptionProvider; - private readonly EndpointApiVersionMetadataCollection endpoints; - private readonly ActionApiVersionMetadataCollection actions; + private readonly DefaultApiVersionDescriptionProvider provider; + private readonly IApiVersionMetadataCollationProvider[] collators; private IReadOnlyList? items; - private long version; + private int version; public ApiVersionDescriptionCollection( - DefaultApiVersionDescriptionProvider apiVersionDescriptionProvider, - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) + DefaultApiVersionDescriptionProvider provider, + IEnumerable collators ) { - this.apiVersionDescriptionProvider = apiVersionDescriptionProvider; - endpoints = new( endpointDataSource ); - actions = new( actionDescriptorCollectionProvider ); + this.provider = provider; + this.collators = collators.ToArray(); } public IReadOnlyList Items { get { - if ( items is not null && version == CurrentVersion ) + if ( items is not null && version == ComputeVersion() ) { return items; } lock ( syncRoot ) { - var (items1, version1) = endpoints; - var (items2, version2) = actions; - var currentVersion = ComputeVersion( version1, version2 ); + var currentVersion = ComputeVersion(); if ( items is not null && version == currentVersion ) { return items; } - var capacity = items1.Count + items2.Count; - var metadata = new List( capacity ); - - for ( var i = 0; i < items1.Count; i++ ) - { - metadata.Add( items1[i] ); - } + var context = new ApiVersionMetadataCollationContext(); - for ( var i = 0; i < items2.Count; i++ ) + for ( var i = 0; i < collators.Length; i++ ) { - metadata.Add( items2[i] ); + collators[i].Execute( context ); } - items = apiVersionDescriptionProvider.Describe( metadata ); + items = provider.Describe( context.Results ); version = currentVersion; } @@ -195,158 +191,24 @@ public IReadOnlyList Items } } - private long CurrentVersion - { - get + private int ComputeVersion() => + collators.Length switch { - lock ( syncRoot ) - { - return ComputeVersion( endpoints.Version, actions.Version ); - } - } - } + 0 => 0, + 1 => collators[0].Version, + _ => ComputeVersion( collators ), + }; - private static long ComputeVersion( int version1, int version2 ) => ( ( (long) version1 ) << 32 ) | (long) version2; - } - - private sealed class EndpointApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly EndpointDataSource endpointDataSource; - private List? list; - private int version; - private int currentVersion; - - public EndpointApiVersionMetadataCollection( EndpointDataSource endpointDataSource ) + private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers ) { - this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) ); - ChangeToken.OnChange( endpointDataSource.GetChangeToken, IncrementVersion ); - } - - public int Version => version; + var hash = default( HashCode ); - public IReadOnlyList Items - { - get + for ( var i = 0; i < providers.Length; i++ ) { - if ( list is not null && version == currentVersion ) - { - return list; - } - - lock ( syncRoot ) - { - if ( list is not null && version == currentVersion ) - { - return list; - } - - var endpoints = endpointDataSource.Endpoints; - - if ( list == null ) - { - list = new( capacity: endpoints.Count ); - } - else - { - list.Clear(); - list.Capacity = endpoints.Count; - } - - for ( var i = 0; i < endpoints.Count; i++ ) - { - if ( endpoints[i].Metadata.GetMetadata() is ApiVersionMetadata item ) - { - list.Add( item ); - } - } - - version = currentVersion; - } - - return list; + hash.Add( providers[i].Version ); } - } - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } - } - - private void IncrementVersion() - { - lock ( syncRoot ) - { - currentVersion++; - } - } - } - - private sealed class ActionApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly IActionDescriptorCollectionProvider provider; - private List? list; - private int version; - - public ActionApiVersionMetadataCollection( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) => - provider = actionDescriptorCollectionProvider ?? throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ); - - public int Version => version; - - public IReadOnlyList Items - { - get - { - var collection = provider.ActionDescriptors; - - if ( list is not null && collection.Version == version ) - { - return list; - } - - lock ( syncRoot ) - { - if ( list is not null && collection.Version == version ) - { - return list; - } - - var actions = collection.Items; - - if ( list == null ) - { - list = new( capacity: actions.Count ); - } - else - { - list.Clear(); - list.Capacity = actions.Count; - } - - for ( var i = 0; i < actions.Count; i++ ) - { - list.Add( actions[i].GetApiVersionMetadata() ); - } - - version = collection.Version; - } - - return list; - } - } - - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } + return hash.ToHashCode(); } } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs index d15d45fb..43b66302 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs @@ -3,12 +3,9 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using System.Buffers; using static Asp.Versioning.ApiVersionMapping; using static System.Globalization.CultureInfo; @@ -37,7 +34,16 @@ public GroupedApiVersionDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, endpointDataSource, actionDescriptorCollectionProvider ); + var collators = new IApiVersionMetadataCollationProvider[] + { + new EndpointApiVersionMetadataCollationProvider( + endpointDataSource ?? + throw new ArgumentNullException( nameof( endpointDataSource ) ) ), + new ActionApiVersionMetadataCollationProvider( + actionDescriptorCollectionProvider ?? + throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ) ), + }; + collection = new( this, collators ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -157,242 +163,78 @@ private void AppendDescriptions( private sealed class ApiVersionDescriptionCollection { private readonly object syncRoot = new(); - private readonly GroupedApiVersionDescriptionProvider apiVersionDescriptionProvider; - private readonly EndpointApiVersionMetadataCollection endpoints; - private readonly ActionApiVersionMetadataCollection actions; + private readonly GroupedApiVersionDescriptionProvider provider; + private readonly IApiVersionMetadataCollationProvider[] collators; private IReadOnlyList? items; - private long version; + private int version; public ApiVersionDescriptionCollection( - GroupedApiVersionDescriptionProvider apiVersionDescriptionProvider, - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) + GroupedApiVersionDescriptionProvider provider, + IEnumerable collators ) { - this.apiVersionDescriptionProvider = apiVersionDescriptionProvider; - endpoints = new( endpointDataSource ); - actions = new( actionDescriptorCollectionProvider ); + this.provider = provider; + this.collators = collators.ToArray(); } public IReadOnlyList Items { get { - if ( items is not null && version == CurrentVersion ) + if ( items is not null && version == ComputeVersion() ) { return items; } lock ( syncRoot ) { - var (items1, version1) = endpoints; - var (items2, version2) = actions; - var currentVersion = ComputeVersion( version1, version2 ); + var currentVersion = ComputeVersion(); if ( items is not null && version == currentVersion ) { return items; } - var capacity = items1.Count + items2.Count; - var metadata = new List( capacity ); - - for ( var i = 0; i < items1.Count; i++ ) - { - metadata.Add( items1[i] ); - } - - for ( var i = 0; i < items2.Count; i++ ) - { - metadata.Add( items2[i] ); - } - - items = apiVersionDescriptionProvider.Describe( metadata ); - version = currentVersion; - } - - return items; - } - } - - private long CurrentVersion - { - get - { - lock ( syncRoot ) - { - return ComputeVersion( endpoints.Version, actions.Version ); - } - } - } - - private static long ComputeVersion( int version1, int version2 ) => ( ( (long) version1 ) << 32 ) | (long) version2; - } - - private sealed class EndpointApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly EndpointDataSource endpointDataSource; - private List? list; - private int version; - private int currentVersion; - - public EndpointApiVersionMetadataCollection( EndpointDataSource endpointDataSource ) - { - this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) ); - ChangeToken.OnChange( endpointDataSource.GetChangeToken, IncrementVersion ); - } - - public int Version => version; - - public IReadOnlyList Items - { - get - { - if ( list is not null && version == currentVersion ) - { - return list; - } + var context = new ApiVersionMetadataCollationContext(); - lock ( syncRoot ) - { - if ( list is not null && version == currentVersion ) + for ( var i = 0; i < collators.Length; i++ ) { - return list; + collators[i].Execute( context ); } - var endpoints = endpointDataSource.Endpoints; + var results = context.Results; + var metadata = new GroupedApiVersionMetadata[results.Count]; - if ( list == null ) + for ( var i = 0; i < metadata.Length; i++ ) { - list = new( capacity: endpoints.Count ); - } - else - { - list.Clear(); - list.Capacity = endpoints.Count; - } - - for ( var i = 0; i < endpoints.Count; i++ ) - { - var metadata = endpoints[i].Metadata; - - if ( metadata.GetMetadata() is ApiVersionMetadata item ) - { -#if NETCOREAPP3_1 - // this code path doesn't appear to exist for netcoreapp3.1 - // REF: https://github.com/dotnet/aspnetcore/blob/release/3.1/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs#L74 - list.Add( new( default, item ) ); -#else - var groupName = metadata.OfType().LastOrDefault()?.EndpointGroupName; - list.Add( new( groupName, item ) ); -#endif - } + metadata[i] = new( context.Results.GroupName( i ), results[i] ); } + items = provider.Describe( metadata ); version = currentVersion; } - return list; - } - } - - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } - } - - private void IncrementVersion() - { - lock ( syncRoot ) - { - currentVersion++; + return items; } } - } - - private sealed class ActionApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly IActionDescriptorCollectionProvider provider; - private List? list; - private int version; - public ActionApiVersionMetadataCollection( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) => - provider = actionDescriptorCollectionProvider ?? throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ); - - public int Version => version; - - public IReadOnlyList Items - { - get + private int ComputeVersion() => + collators.Length switch { - var collection = provider.ActionDescriptors; - - if ( list is not null && collection.Version == version ) - { - return list; - } - - lock ( syncRoot ) - { - if ( list is not null && collection.Version == version ) - { - return list; - } - - var actions = collection.Items; - - if ( list == null ) - { - list = new( capacity: actions.Count ); - } - else - { - list.Clear(); - list.Capacity = actions.Count; - } - - for ( var i = 0; i < actions.Count; i++ ) - { - var action = actions[i]; - list.Add( new( GetGroupName( action ), action.GetApiVersionMetadata() ) ); - } + 0 => 0, + 1 => collators[0].Version, + _ => ComputeVersion( collators ), + }; - version = collection.Version; - } - - return list; - } - } - - // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs - private static string? GetGroupName( ActionDescriptor action ) + private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers ) { -#if NETCOREAPP3_1 - return action.GetProperty()?.GroupName; -#else - var endpointGroupName = action.EndpointMetadata.OfType().LastOrDefault(); + var hash = default( HashCode ); - if ( endpointGroupName is null ) + for ( var i = 0; i < providers.Length; i++ ) { - return action.GetProperty()?.GroupName; + hash.Add( providers[i].Version ); } - return endpointGroupName.EndpointGroupName; -#endif - } - - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } + return hash.ToHashCode(); } } diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs new file mode 100644 index 00000000..81a06c74 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; + +/// +/// Represents an API version metadata collection provider for controller actions. +/// +[CLSCompliant( false )] +public sealed class ActionApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider +{ + private readonly IActionDescriptorCollectionProvider provider; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying + /// action descriptor collection provider. + public ActionApiVersionMetadataCollationProvider( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) => + provider = actionDescriptorCollectionProvider ?? throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ); + + /// + public int Version => provider.ActionDescriptors.Version; + + /// + public void Execute( ApiVersionMetadataCollationContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + var actions = provider.ActionDescriptors.Items; + + for ( var i = 0; i < actions.Count; i++ ) + { + var action = actions[i]; + var item = action.GetApiVersionMetadata(); + var groupName = GetGroupName( action ); + + context.Results.Add( item, groupName ); + } + } + + // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs + private static string? GetGroupName( ActionDescriptor action ) + { +#if NETCOREAPP3_1 + return action.GetProperty()?.GroupName; +#else + var endpointGroupName = action.EndpointMetadata.OfType().LastOrDefault(); + + if ( endpointGroupName is null ) + { + return action.GetProperty()?.GroupName; + } + + return endpointGroupName.EndpointGroupName; +#endif + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs index f0e2b64c..bbbbcd71 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs @@ -2,10 +2,8 @@ namespace Asp.Versioning; -using Asp.Versioning.ApplicationModels; using Asp.Versioning.Conventions; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using System.Runtime.CompilerServices; using static Asp.Versioning.ApiVersionMapping; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs index 348c5a76..afa02343 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -3,6 +3,7 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; +using Asp.Versioning.ApiExplorer; using Asp.Versioning.ApplicationModels; using Asp.Versioning.Conventions; using Asp.Versioning.Routing; @@ -73,6 +74,7 @@ private static void AddServices( IServiceCollection services ) services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Transient() ); + services.TryAddEnumerable( Singleton() ); services.Replace( WithUrlHelperFactoryDecorator( services ) ); services.TryReplace( typeof( DefaultProblemDetailsFactory ), Singleton() ); } From e923a62bf9b3f98bc674a60997b91803ae6bc003 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 26 Nov 2022 11:10:13 -0800 Subject: [PATCH 17/35] Provide workaround for OData/AspNetCoreOData#753 --- .../OData/VersionedODataTemplateTranslator.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs index 84ef5d76..5d58b2b5 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs @@ -6,6 +6,7 @@ namespace Asp.Versioning.OData; using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; using Microsoft.OData.UriParser; +using System.Runtime.CompilerServices; /// /// Represents a versioned OData template translator. @@ -30,9 +31,7 @@ public sealed class VersionedODataTemplateTranslator : IODataTemplateTranslator if ( apiVersion == null ) { - var metadata = context.Endpoint.Metadata.GetMetadata(); - - if ( metadata == null || !metadata.IsApiVersionNeutral ) + if ( !IsVersionNeutral( context ) ) { return default; } @@ -42,7 +41,13 @@ public sealed class VersionedODataTemplateTranslator : IODataTemplateTranslator var model = context.Model; var otherApiVersion = model.GetAnnotationValue( model )?.ApiVersion; - if ( !apiVersion.Equals( otherApiVersion ) ) + // HACK: a version-neutral endpoint can fail to match here because odata tries to match the + // first endpoint metadata when there could be multiple. such an endpoint is expected to be + // the same in all versions so allow it to flow through. revisit if/when odata fixes this. + // + // REF: https://github.com/OData/AspNetCoreOData/issues/753 + // REF: https://github.com/OData/AspNetCoreOData/blob/main/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMatcherPolicy.cs#L86 + if ( !apiVersion.Equals( otherApiVersion ) && !IsVersionNeutral( context ) ) { return default; } @@ -58,4 +63,9 @@ public sealed class VersionedODataTemplateTranslator : IODataTemplateTranslator return new( context.Segments ); } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool IsVersionNeutral( ODataTemplateTranslateContext context ) => + context.Endpoint.Metadata.GetMetadata() is ApiVersionMetadata metadata + && metadata.IsApiVersionNeutral; } \ No newline at end of file From c5abd2e91818cda55cd5effc864837e8351d6464 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 26 Nov 2022 11:50:46 -0800 Subject: [PATCH 18/35] Update versions and release notes --- .../Asp.Versioning.OData.ApiExplorer.csproj | 2 +- .../OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj | 2 +- src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt | 2 +- .../WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj | 2 +- src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt | 2 +- .../Asp.Versioning.Mvc.ApiExplorer.csproj | 2 +- .../WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt | 3 ++- .../WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj | 2 +- src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt | 2 +- 9 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj index 955a0d05..43ec7680 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj @@ -1,7 +1,7 @@  - 6.3.0 + 6.3.1 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj index 23be8a44..9ea5d6db 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj @@ -1,7 +1,7 @@  - 6.3.0 + 6.3.1 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt index 5f282702..d980a834 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Added workaround for [OData #753](https://github.com/OData/AspNetCoreOData/issues/753) \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj index dce50659..e94f1972 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj @@ -1,7 +1,7 @@  - 6.3.0 + 6.3.1 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt index 5f282702..e508e614 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +[Fixed #922](https://github.com/dotnet/aspnet-api-versioning/discussions/922) \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj index c8d61eec..ff2d9ae4 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj @@ -1,7 +1,7 @@  - 6.3.0 + 6.3.1 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning.ApiExplorer diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt index 5f282702..7bc8848b 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt @@ -1 +1,2 @@ - \ No newline at end of file +[Fixed #922](https://github.com/dotnet/aspnet-api-versioning/discussions/922) +[Fixed #923](https://github.com/dotnet/aspnet-api-versioning/issues/923) \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj index 3ed03b13..483d4ea7 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj @@ -1,7 +1,7 @@  - 6.3.0 + 6.3.1 6.3.0.0 net6.0;netcoreapp3.1 Asp.Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt index 5f282702..e508e614 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +[Fixed #922](https://github.com/dotnet/aspnet-api-versioning/discussions/922) \ No newline at end of file From 271df63561e4820bc9bd87294d68a4edc18282ed Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 6 Dec 2022 15:47:49 -0800 Subject: [PATCH 19/35] Call base implementation. Fixes #932 --- .../AdvertiseApiVersionsAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs index eed5d62c..9d879940 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs @@ -97,5 +97,5 @@ public bool Deprecated } /// - public override int GetHashCode() => HashCode.Combine( GetHashCode(), Deprecated ); + public override int GetHashCode() => HashCode.Combine( base.GetHashCode(), Deprecated ); } \ No newline at end of file From 177d61141ebbd1c6decd89867f2731c4427db90f Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 6 Dec 2022 15:48:17 -0800 Subject: [PATCH 20/35] Run tests under 'en-US' locale --- .../ApiVersionFormatProviderTest.cs | 16 ++++++++ .../AssumeCultureAttribute.cs | 40 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/Abstractions/test/Asp.Versioning.Abstractions.Tests/AssumeCultureAttribute.cs diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs index 64b440a8..7b8d2245 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs @@ -41,6 +41,7 @@ public void get_format_should_return_expected_format_provider() } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_allow_null_or_empty_format_string( ApiVersionFormatProvider provider ) { @@ -56,6 +57,7 @@ public void format_should_allow_null_or_empty_format_string( ApiVersionFormatPro } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_full_formatted_string_without_optional_components( ApiVersionFormatProvider provider ) { @@ -70,6 +72,7 @@ public void format_should_return_full_formatted_string_without_optional_componen } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_full_formatted_string_with_optional_components( ApiVersionFormatProvider provider ) { @@ -84,6 +87,7 @@ public void format_should_return_full_formatted_string_with_optional_components( } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_original_string_format_when_argument_cannot_be_formatted( ApiVersionFormatProvider provider ) { @@ -113,6 +117,7 @@ public void format_should_not_allow_malformed_literal_string( ApiVersionFormatPr } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( GroupVersionFormatData ) )] public void format_should_return_formatted_group_version_string( ApiVersionFormatProvider provider, string format ) { @@ -129,6 +134,7 @@ public void format_should_return_formatted_group_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_minor_version_string( ApiVersionFormatProvider provider ) { @@ -143,6 +149,7 @@ public void format_should_return_formatted_minor_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_major_version_string( ApiVersionFormatProvider provider ) { @@ -157,6 +164,7 @@ public void format_should_return_formatted_major_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_major_and_minor_version_string( ApiVersionFormatProvider provider ) { @@ -171,6 +179,7 @@ public void format_should_return_formatted_major_and_minor_version_string( ApiVe } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_short_version_string( ApiVersionFormatProvider provider ) { @@ -185,6 +194,7 @@ public void format_should_return_formatted_short_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_long_version_string( ApiVersionFormatProvider provider ) { @@ -199,6 +209,7 @@ public void format_should_return_formatted_long_version_string( ApiVersionFormat } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_status_string( ApiVersionFormatProvider provider ) { @@ -213,6 +224,7 @@ public void format_should_return_formatted_status_string( ApiVersionFormatProvid } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( PaddedMinorVersionFormatData ) )] public void format_should_return_formatted_minor_version_with_padding_string( ApiVersionFormatProvider provider, string format ) { @@ -233,6 +245,7 @@ public void format_should_return_formatted_minor_version_with_padding_string( Ap } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( PaddedMajorVersionFormatData ) )] public void format_should_return_formatted_major_version_with_padding_string( ApiVersionFormatProvider provider, string format ) { @@ -253,6 +266,7 @@ public void format_should_return_formatted_major_version_with_padding_string( Ap } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( CustomFormatData ) )] public void format_should_return_custom_format_string( Func format, string expected ) { @@ -268,6 +282,7 @@ public void format_should_return_custom_format_string( Func } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( MultipleFormatParameterData ) )] public void format_should_return_formatted_string_with_multiple_parameters( ApiVersionFormatProvider provider, string format, object secondArgument, string expected ) { @@ -284,6 +299,7 @@ public void format_should_return_formatted_string_with_multiple_parameters( ApiV } [Fact] + [AssumeCulture( "en-us" )] public void format_should_return_formatted_string_with_escape_sequence() { // arrange diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/AssumeCultureAttribute.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/AssumeCultureAttribute.cs new file mode 100644 index 00000000..9c956d87 --- /dev/null +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/AssumeCultureAttribute.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Globalization; +using System.Reflection; +using Xunit.Sdk; +using static System.AttributeTargets; +using static System.Threading.Thread; + +/// +/// Allows a test method to assume that it is running in a specific locale. +/// +[AttributeUsage( Class | Method, AllowMultiple = false, Inherited = true )] +public sealed class AssumeCultureAttribute : BeforeAfterTestAttribute +{ + private CultureInfo originalCulture; + private CultureInfo originalUICulture; + + public AssumeCultureAttribute( string name ) => Name = name; + + public string Name { get; } + + public override void Before( MethodInfo methodUnderTest ) + { + originalCulture = CurrentThread.CurrentCulture; + originalUICulture = CurrentThread.CurrentUICulture; + + var culture = CultureInfo.CreateSpecificCulture( Name ); + + CurrentThread.CurrentCulture = culture; + CurrentThread.CurrentUICulture = culture; + } + + public override void After( MethodInfo methodUnderTest ) + { + CurrentThread.CurrentCulture = originalCulture; + CurrentThread.CurrentUICulture = originalUICulture; + } +} \ No newline at end of file From a2347a8fbd09620b2f0cbb30196570bd81801c49 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 6 Dec 2022 15:49:16 -0800 Subject: [PATCH 21/35] Bump version and add release notes --- .../Asp.Versioning.Abstractions.csproj | 2 +- .../src/Asp.Versioning.Abstractions/ReleaseNotes.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj b/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj index cf2411e7..fdfb8220 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj @@ -1,7 +1,7 @@  - 6.2.0 + 6.2.1 6.2.0.0 netstandard1.0;netstandard2.0;net6.0 API Versioning Abstractions diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt b/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt index 5f282702..40820b51 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +[Fixed #932](https://github.com/dotnet/aspnet-api-versioning/issues/932) \ No newline at end of file From f8d334baed5a665fdbd7e04e5213416d08621eb9 Mon Sep 17 00:00:00 2001 From: Marius W Nilsen Date: Wed, 7 Dec 2022 10:14:09 +0100 Subject: [PATCH 22/35] AssumeCulture en-us for month formatting test --- .../Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs index b650303b..1d027d3e 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs @@ -6,6 +6,7 @@ public partial class ApiVersionTest { [Theory] [MemberData( nameof( FormatData ) )] + [AssumeCulture("en-us")] public void try_format_format_should_return_expected_string( string format, string text, string formattedString ) { // arrange From 1679a090edc914d018bbbb1e8d9784e33ec6bb29 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Wed, 7 Dec 2022 17:03:50 -0800 Subject: [PATCH 23/35] Fix spacing --- .../net6.0/ApiVersionTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs index 1d027d3e..7620bc38 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs @@ -6,7 +6,7 @@ public partial class ApiVersionTest { [Theory] [MemberData( nameof( FormatData ) )] - [AssumeCulture("en-us")] + [AssumeCulture( "en-us" )] public void try_format_format_should_return_expected_string( string format, string text, string formattedString ) { // arrange @@ -20,4 +20,4 @@ public void try_format_format_should_return_expected_string( string format, stri result.Should().BeTrue(); buffer[..written].ToString().Should().Be( formattedString ); } -} \ No newline at end of file +} From 64ac13266ed34363341a610eb0f4e57e95b92269 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 23 Dec 2022 14:48:39 -0800 Subject: [PATCH 24/35] Remove frivolous line --- .../Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs index 7620bc38..b1b6f794 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net6.0/ApiVersionTest.cs @@ -20,4 +20,4 @@ public void try_format_format_should_return_expected_string( string format, stri result.Should().BeTrue(); buffer[..written].ToString().Should().Be( formattedString ); } -} +} \ No newline at end of file From 207ffceb47b1e22c1133f23b952158c322c7b266 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 5 Dec 2022 09:47:19 -0800 Subject: [PATCH 25/35] Refactor to make registering model configurations public --- .../IApiVersioningBuilderExtensions.cs | 42 +--------- .../IServiceCollectionExtensions.cs | 76 +++++++++++++++++++ ...ODataMultiModelApplicationModelProvider.cs | 1 - 3 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs index 79b1ad0e..40ac7a78 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -9,7 +9,6 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.AspNetCore.Routing; @@ -69,8 +68,7 @@ private static void AddServices( IServiceCollection services ) services.TryRemoveODataService( typeof( IApplicationModelProvider ), ODataRoutingApplicationModelProviderType ); var partManager = services.GetOrCreateApplicationPartManager(); - - ConfigureDefaultFeatureProviders( partManager ); + var configured = partManager.ConfigureDefaultFeatureProviders(); services.AddHttpContextAccessor(); services.TryAddSingleton(); @@ -87,24 +85,11 @@ private static void AddServices( IServiceCollection services ) services.TryAddEnumerable( Singleton() ); services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Transient() ); - services.AddModelConfigurationsAsServices( partManager ); - } - - private static T GetService( this IServiceCollection services ) => - (T) services.LastOrDefault( d => d.ServiceType == typeof( T ) )?.ImplementationInstance!; - - private static ApplicationPartManager GetOrCreateApplicationPartManager( this IServiceCollection services ) - { - var partManager = services.GetService(); - if ( partManager == null ) + if ( configured ) { - partManager = new ApplicationPartManager(); - services.TryAddSingleton( partManager ); + services.AddModelConfigurationsAsServices( partManager ); } - - partManager.ApplicationParts.Add( new AssemblyPart( typeof( ODataApiVersioningOptions ).Assembly ) ); - return partManager; } [MethodImpl( MethodImplOptions.AggressiveInlining )] @@ -151,27 +136,6 @@ private static void TryReplaceODataService( } } - private static void AddModelConfigurationsAsServices( this IServiceCollection services, ApplicationPartManager partManager ) - { - var feature = new ModelConfigurationFeature(); - var modelConfigurationType = typeof( IModelConfiguration ); - - partManager.PopulateFeature( feature ); - - foreach ( var modelConfiguration in feature.ModelConfigurations ) - { - services.TryAddEnumerable( Transient( modelConfigurationType, modelConfiguration ) ); - } - } - - private static void ConfigureDefaultFeatureProviders( ApplicationPartManager partManager ) - { - if ( !partManager.FeatureProviders.OfType().Any() ) - { - partManager.FeatureProviders.Add( new ModelConfigurationFeatureProvider() ); - } - } - private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor ) { if ( descriptor.ImplementationInstance != null ) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs new file mode 100644 index 00000000..cb86af84 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Microsoft.Extensions.DependencyInjection; + +using Asp.Versioning.OData; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Runtime.CompilerServices; +using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; + +/// +/// Provides extension methods for . +/// +public static class IServiceCollectionExtensions +{ + [MethodImpl( MethodImplOptions.AggressiveInlining )] + internal static T GetService( this IServiceCollection services ) => + (T) services.LastOrDefault( d => d.ServiceType == typeof( T ) )?.ImplementationInstance!; + + internal static ApplicationPartManager GetOrCreateApplicationPartManager( this IServiceCollection services ) + { + var partManager = services.GetService(); + + if ( partManager == null ) + { + partManager = new ApplicationPartManager(); + services.TryAddSingleton( partManager ); + } + + partManager.ApplicationParts.Add( new AssemblyPart( typeof( ODataApiVersioningOptions ).Assembly ) ); + return partManager; + } + + internal static void AddModelConfigurationsAsServices( this IServiceCollection services, ApplicationPartManager partManager ) + { + var feature = new ModelConfigurationFeature(); + var modelConfigurationType = typeof( IModelConfiguration ); + + partManager.PopulateFeature( feature ); + + foreach ( var modelConfiguration in feature.ModelConfigurations ) + { + services.TryAddEnumerable( Transient( modelConfigurationType, modelConfiguration ) ); + } + } + + internal static bool ConfigureDefaultFeatureProviders( this ApplicationPartManager partManager ) + { + if ( partManager.FeatureProviders.OfType().Any() ) + { + return false; + } + + partManager.FeatureProviders.Add( new ModelConfigurationFeatureProvider() ); + return true; + } + + /// + /// Registers discovered model configurations as services in the . + /// + /// The extended . + public static void AddModelConfigurationsAsServices( this IServiceCollection services ) + { + if ( services == null ) + { + throw new ArgumentNullException( nameof( services ) ); + } + + var partManager = services.GetOrCreateApplicationPartManager(); + + if ( ConfigureDefaultFeatureProviders( partManager ) ) + { + services.AddModelConfigurationsAsServices( partManager ); + } + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs index c8fc3fe0..f0189cb0 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs @@ -4,7 +4,6 @@ namespace Asp.Versioning.OData; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Routing.Conventions; From 0cdfc1dbe57692ae820f52a3c317460c529962ac Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 5 Dec 2022 19:22:40 -0800 Subject: [PATCH 26/35] Fix NETFX reference assemblies --- asp.sln | 3 --- build/test.targets | 4 ++-- .../Asp.Versioning.Abstractions.Tests.csproj | 2 +- src/AspNet/Directory.Build.targets | 10 ---------- src/Directory.Build.targets | 4 ++++ 5 files changed, 7 insertions(+), 16 deletions(-) delete mode 100644 src/AspNet/Directory.Build.targets diff --git a/asp.sln b/asp.sln index 57868d94..0df9e4ed 100644 --- a/asp.sln +++ b/asp.sln @@ -26,9 +26,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abstractions", "Abstractions", "{7B0FA6C2-47BA-4C34-90E0-B75DF44F2124}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{34A0373F-12C9-44A8-9A1C-5EEE7218C877}" - ProjectSection(SolutionItems) = preProject - src\AspNet\Directory.Build.targets = src\AspNet\Directory.Build.targets - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCore", "AspNetCore", "{EBC9F217-E8BC-4DCE-9C67-F22150959EAF}" EndProject diff --git a/build/test.targets b/build/test.targets index c20ca11e..38ae7080 100644 --- a/build/test.targets +++ b/build/test.targets @@ -2,8 +2,8 @@ - 6.7.0 - 4.18.2 + 6.8.0 + 4.18.3 2.4.5 6.0.8-* diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj index b1d118a0..e15d19f1 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj @@ -1,6 +1,6 @@  - net6.0;net472;net452 + net6.0;net452;net472 Asp.Versioning diff --git a/src/AspNet/Directory.Build.targets b/src/AspNet/Directory.Build.targets deleted file mode 100644 index 6a993f01..00000000 --- a/src/AspNet/Directory.Build.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 15a2e2dc..468f1f01 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -21,6 +21,10 @@ + + + + From 0b9a2997958fbd5fe6280cbae636f0e20e212be9 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 5 Dec 2022 19:26:47 -0800 Subject: [PATCH 27/35] Add ad hoc annotation and resolve annotations via extension method --- .../OData/EdmModelSelector.cs | 6 +- .../OData/VersionedODataModelBuilderTest.cs | 2 +- .../ODataApiDescriptionProvider.cs | 2 +- .../ODataQueryOptionDescriptionContext.cs | 2 +- ...ODataMultiModelApplicationModelProvider.cs | 6 +- .../OData/VersionedODataTemplateTranslator.cs | 2 +- .../VersionedAttributeRoutingConvention.cs | 2 +- .../OData/VersionedODataModelBuilderTest.cs | 2 +- .../OData/DefaultModelTypeBuilder.cs | 55 ++++++++++++++----- .../OData/TypeExtensions.cs | 33 +++++------ .../OData/TypeSubstitutionContext.cs | 2 +- .../IEdmModelExtensions.cs | 28 ++++++++++ .../src/Common.OData/OData/AdHocAnnotation.cs | 15 +++++ 13 files changed, 113 insertions(+), 44 deletions(-) create mode 100644 src/Common/src/Common.OData/Microsoft.OData.Edm/IEdmModelExtensions.cs create mode 100644 src/Common/src/Common.OData/OData/AdHocAnnotation.cs diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs index cba6623f..f8f7e17b 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs @@ -130,16 +130,12 @@ public EdmModelSelector( IEnumerable models, IApiVersionSelector apiV private static void AddVersionFromModel( IEdmModel model, IList versions, IDictionary collection ) { - var annotation = model.GetAnnotationValue( model ); - - if ( annotation == null ) + if ( model.GetApiVersion() is not ApiVersion version ) { var message = string.Format( CultureInfo.CurrentCulture, SR.MissingAnnotation, typeof( ApiVersionAnnotation ).Name ); throw new ArgumentException( message ); } - var version = annotation.ApiVersion; - collection.Add( version, model ); versions.Add( version ); } diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs index 7bd0781e..31845519 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs @@ -34,7 +34,7 @@ public void get_edm_models_should_return_expected_results() var model = builder.GetEdmModels().Single(); // assert - model.GetAnnotationValue( model ).ApiVersion.Should().Be( apiVersion ); + model.GetApiVersion().Should().Be( apiVersion ); modelCreated.Verify( f => f( It.IsAny(), model ), Times.Once() ); } diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs index 0084cd01..06611d5b 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs @@ -221,7 +221,7 @@ private static bool TryMatchModelVersion( for ( var i = 0; i < items.Count; i++ ) { var item = items[i]; - var otherApiVersion = item.Model.GetAnnotationValue( item.Model )?.ApiVersion; + var otherApiVersion = item.Model.GetApiVersion(); if ( apiVersion.Equals( otherApiVersion ) ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs index aef2fdbf..ae3ad97b 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs @@ -34,7 +34,7 @@ public partial class ODataQueryOptionDescriptionContext foreach ( var item in items ) { var model = item.Model; - var otherVersion = model.GetAnnotationValue( model )?.ApiVersion; + var otherVersion = model.GetApiVersion(); if ( version.Equals( otherVersion ) ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs index f0189cb0..7fb591e2 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs @@ -152,7 +152,11 @@ private void AddRouteComponents( for ( var i = 0; i < models.Count; i++ ) { var model = models[i]; - var version = model.GetAnnotationValue( model ).ApiVersion; + + if ( model.GetApiVersion() is not ApiVersion version ) + { + continue; + } if ( !mappings.TryGetValue( version, out var options ) ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs index 5d58b2b5..8f29de7b 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs @@ -39,7 +39,7 @@ public sealed class VersionedODataTemplateTranslator : IODataTemplateTranslator else { var model = context.Model; - var otherApiVersion = model.GetAnnotationValue( model )?.ApiVersion; + var otherApiVersion = model.GetApiVersion(); // HACK: a version-neutral endpoint can fail to match here because odata tries to match the // first endpoint metadata when there could be multiple. such an endpoint is expected to be diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs index 5dd11990..8ea4c5a2 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs @@ -55,7 +55,7 @@ public override bool AppliesToAction( ODataControllerActionContext context ) return false; } - var apiVersion = edm.GetAnnotationValue( edm )?.ApiVersion; + var apiVersion = edm.GetApiVersion(); if ( apiVersion == null || !metadata.IsMappedTo( apiVersion ) ) { diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs index 29be43a2..b40917a9 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs @@ -29,7 +29,7 @@ public void get_edm_models_should_return_expected_results() var model = builder.GetEdmModels().Single(); // assert - model.GetAnnotationValue( model ).ApiVersion.Should().Be( apiVersion ); + model.GetApiVersion().Should().Be( apiVersion ); modelCreated.Verify( f => f( It.IsAny(), model ), Once() ); } diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs index 461d6cf0..62df945a 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs @@ -25,10 +25,19 @@ namespace Asp.Versioning.OData; public sealed class DefaultModelTypeBuilder : IModelTypeBuilder { private static Type? ienumerableOfT; + private readonly bool adHoc; + private DefaultModelTypeBuilder? adHocBuilder; private ConcurrentDictionary? modules; private ConcurrentDictionary>? generatedEdmTypesPerVersion; private ConcurrentDictionary>? generatedActionParamsPerVersion; + private DefaultModelTypeBuilder( bool adHoc ) => this.adHoc = adHoc; + + /// + /// Initializes a new instance of the class. + /// + public DefaultModelTypeBuilder() { } + /// public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) { @@ -37,6 +46,12 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp throw new ArgumentNullException( nameof( model ) ); } + if ( !adHoc && model.IsAdHoc() ) + { + adHocBuilder ??= new( adHoc: true ); + return adHocBuilder.NewStructuredType( model, structuredType, clrType, apiVersion ); + } + if ( structuredType == null ) { throw new ArgumentNullException( nameof( structuredType ) ); @@ -54,10 +69,9 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp generatedEdmTypesPerVersion ??= new(); - var typeKey = new EdmTypeKey( structuredType, apiVersion ); var edmTypes = generatedEdmTypesPerVersion.GetOrAdd( apiVersion, key => GenerateTypesForEdmModel( model, key ) ); - return edmTypes[typeKey]; + return edmTypes[new( structuredType, apiVersion )]; } /// @@ -68,6 +82,12 @@ public Type NewActionParameters( IEdmModel model, IEdmAction action, string cont throw new ArgumentNullException( nameof( model ) ); } + if ( !adHoc && model.IsAdHoc() ) + { + adHocBuilder ??= new( adHoc: true ); + return adHocBuilder.NewActionParameters( model, action, controllerName, apiVersion ); + } + if ( action == null ) { throw new ArgumentNullException( nameof( action ) ); @@ -85,7 +105,7 @@ public Type NewActionParameters( IEdmModel model, IEdmAction action, string cont generatedActionParamsPerVersion ??= new(); - var paramTypes = generatedActionParamsPerVersion.GetOrAdd( apiVersion, _ => new ConcurrentDictionary() ); + var paramTypes = generatedActionParamsPerVersion.GetOrAdd( apiVersion, _ => new() ); var fullTypeName = $"{controllerName}.{action.Namespace}.{controllerName}{action.Name}Parameters"; var key = new EdmTypeKey( fullTypeName, apiVersion ); var type = paramTypes.GetOrAdd( key, _ => @@ -322,8 +342,7 @@ private static Type ResolveType( } [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static Type MakeEnumerable( Type itemType ) => - ( ienumerableOfT ??= typeof( IEnumerable<> ) ).MakeGenericType( itemType ); + private static Type MakeEnumerable( Type itemType ) => ( ienumerableOfT ??= typeof( IEnumerable<> ) ).MakeGenericType( itemType ); [MethodImpl( MethodImplOptions.AggressiveInlining )] private static Type CreateTypeFromSignature( ModuleBuilder moduleBuilder, ClassSignature @class ) => @@ -389,7 +408,11 @@ private static IDictionary ResolveDependencies( BuilderContext return edmTypes; } - private static PropertyBuilder AddProperty( TypeBuilder addTo, Type shouldBeAdded, string name, IReadOnlyList customAttributes ) + private static PropertyBuilder AddProperty( + TypeBuilder addTo, + Type shouldBeAdded, + string name, + IReadOnlyList customAttributes ) { const MethodAttributes propertyMethodAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; var field = addTo.DefineField( "_" + name, shouldBeAdded, FieldAttributes.Private ); @@ -418,7 +441,7 @@ private static PropertyBuilder AddProperty( TypeBuilder addTo, Type shouldBeAdde return propertyBuilder; } - private static AssemblyName NewAssemblyName(ApiVersion apiVersion) + private static AssemblyName NewAssemblyName( ApiVersion apiVersion, bool adHoc ) { // this is not strictly necessary, but it makes debugging a bit easier as each // assembly-qualified type name provides visibility as to which api version a @@ -455,20 +478,26 @@ private static AssemblyName NewAssemblyName(ApiVersion apiVersion) } name.Insert( 0, 'V' ) - .Append( NewGuid().ToString( "n", InvariantCulture ) ) - .Append( ".DynamicModels" ); + .Append( NewGuid().ToString( "n", InvariantCulture ) ); + + if ( adHoc ) + { + name.Append( ".AdHoc" ); + } + + name.Append( ".DynamicModels" ); return new( name.ToString() ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static ModuleBuilder CreateModuleForApiVersion( ApiVersion apiVersion ) + private ModuleBuilder CreateModuleForApiVersion( ApiVersion apiVersion ) { - var name = NewAssemblyName( apiVersion ); + var assemblyName = NewAssemblyName( apiVersion, adHoc ); #if NETFRAMEWORK - var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( name, Run ); + var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( assemblyName, Run ); #else - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( name, Run ); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( assemblyName, Run ); #endif return assemblyBuilder.DefineDynamicModule( "" ); } diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs index fb13bbe8..bca4a961 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs @@ -3,11 +3,13 @@ namespace Asp.Versioning.OData; #if NETFRAMEWORK +using Microsoft.OData.Edm; using System.Net.Http; using System.Web.Http; #else using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Results; +using Microsoft.OData.Edm; #endif using System.Reflection; using System.Reflection.Emit; @@ -54,12 +56,11 @@ public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContex var openTypes = new Stack(); var apiVersion = context.ApiVersion; var resolver = new StructuredTypeResolver( context.Model ); + IEdmStructuredType? structuredType; if ( IsSubstitutableGeneric( type, openTypes, out var innerType ) ) { - var structuredType = resolver.GetStructuredType( innerType! ); - - if ( structuredType == null ) + if ( ( structuredType = resolver.GetStructuredType( innerType! ) ) == null ) { return type; } @@ -74,14 +75,9 @@ public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContex return CloseGeneric( openTypes, newType ); } - if ( CanBeSubstituted( type ) ) + if ( CanBeSubstituted( type ) && ( structuredType = resolver.GetStructuredType( type ) ) != null ) { - var structuredType = resolver.GetStructuredType( type ); - - if ( structuredType != null ) - { - type = context.ModelTypeBuilder.NewStructuredType( context.Model, structuredType, type, apiVersion ); - } + type = context.ModelTypeBuilder.NewStructuredType( context.Model, structuredType, type, apiVersion ); } return type; @@ -242,16 +238,15 @@ private static Type CloseGeneric( Stack openTypes, Type innerType ) return type; } - private static bool CanBeSubstituted( Type type ) - { - return Type.GetTypeCode( type ) == TypeCode.Object && - !type.IsValueType && - !type.Equals( ActionResultType ) && + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool CanBeSubstituted( Type type ) => + Type.GetTypeCode( type ) == TypeCode.Object && + !type.IsValueType && + !type.Equals( ActionResultType ) && #if NETFRAMEWORK - !type.Equals( HttpResponseType ) && + !type.Equals( HttpResponseType ) && #endif - !type.IsODataActionParameters(); - } + !type.IsODataActionParameters(); internal static bool IsEnumerable( this Type type, @@ -295,6 +290,7 @@ internal static bool IsEnumerable( return false; } + [MethodImpl( MethodImplOptions.AggressiveInlining )] private static bool IsSingleResult( this Type type ) => type.Is( SingleResultOfT ); private static bool IsODataValue( this Type? type ) @@ -323,6 +319,7 @@ private static bool IsODataValue( this Type? type ) private static bool Is( this Type type, Type typeDefinition ) => type.IsGenericType && type.GetGenericTypeDefinition().Equals( typeDefinition ); + [MethodImpl( MethodImplOptions.AggressiveInlining )] private static bool ShouldExtractInnerType( this Type type ) => type.IsDelta() || #if !NETFRAMEWORK diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs b/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs index baad2af6..b47ae724 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs @@ -36,7 +36,7 @@ public class TypeSubstitutionContext /// Gets API version associated with the source model. /// /// The associated API version. - public ApiVersion ApiVersion => apiVersion ??= Model.GetAnnotationValue( Model )?.ApiVersion ?? ApiVersion.Neutral; + public ApiVersion ApiVersion => apiVersion ??= Model.GetApiVersion() ?? ApiVersion.Neutral; /// /// Gets the model type builder used to create substitution types. diff --git a/src/Common/src/Common.OData/Microsoft.OData.Edm/IEdmModelExtensions.cs b/src/Common/src/Common.OData/Microsoft.OData.Edm/IEdmModelExtensions.cs new file mode 100644 index 00000000..04d14dd6 --- /dev/null +++ b/src/Common/src/Common.OData/Microsoft.OData.Edm/IEdmModelExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Microsoft.OData.Edm; + +using Asp.Versioning; +using Asp.Versioning.OData; + +/// +/// Provides extension methods for . +/// +public static class IEdmModelExtensions +{ + /// + /// Gets the API version associated with the Entity Data Model (EDM). + /// + /// The extended EDM. + /// The associated API version or null. + public static ApiVersion? GetApiVersion( this IEdmModel model ) => + model.GetAnnotationValue( model )?.ApiVersion; + + /// + /// Gets a value indicating whether the Entity Data Model (EDM) is for defined ad hoc usage. + /// + /// The extended EDM. + /// True if the EDM is defined for ad hoc usage; otherwise, false. + public static bool IsAdHoc( this IEdmModel model ) => + model.GetAnnotationValue( model ) is not null; +} \ No newline at end of file diff --git a/src/Common/src/Common.OData/OData/AdHocAnnotation.cs b/src/Common/src/Common.OData/OData/AdHocAnnotation.cs new file mode 100644 index 00000000..c787bacc --- /dev/null +++ b/src/Common/src/Common.OData/OData/AdHocAnnotation.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.OData; + +/// +/// Represents an annotation for ad hoc usage. +/// +public sealed class AdHocAnnotation +{ + /// + /// Gets a singleton instance of the annotation. + /// + /// A singleton annotation instance. + public static AdHocAnnotation Instance { get; } = new(); +} \ No newline at end of file From 639b9588c9dd1f7d7d001eaeec2aa07741440349 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 6 Dec 2022 10:52:30 -0800 Subject: [PATCH 28/35] Define option to opt out of ad hoc model type substitution --- .../OData/DefaultModelTypeBuilder.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs index 62df945a..37c3bebc 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs @@ -24,6 +24,18 @@ namespace Asp.Versioning.OData; /// public sealed class DefaultModelTypeBuilder : IModelTypeBuilder { + /* design: there is typically a 1:1 relationship between an edm and api version. odata model bound settings + * are realized as an annotation in the edm. this can result in two sets of pairs where one edm is the + * standard mapping and the other is ad hoc for the purposes of query option settings. aside for the bucket + * they land in, there is no difference in how types will be mapped; however if the wrong edm from the + * incorrect bucket is picked, then the type mapping will fail. the model type builder detects if a model + * is ad hoc. if it is, then it will recursively create a private instance of itself to handle the ad hoc + * bucket. normal odata cannot opt out of this process because the explored type must match the edm. a type + * mapped via an ad hoc edm is not really odata so it can opt out if desired. the opt out process is more + * of a failsafe and optimization. if the ad hoc edm wasn't customized, then the meta model and type should + * be exactly the same, which will result in no substitution. + */ + private static Type? ienumerableOfT; private readonly bool adHoc; private DefaultModelTypeBuilder? adHocBuilder; @@ -38,6 +50,14 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder /// public DefaultModelTypeBuilder() { } + /// + /// Gets or sets a value indicating whether types from an ad hoc Entity Data Model + /// (EDM) should be excluded. + /// + /// True if types from an ad hoc EDM are excluded; otherwise, false. The + /// default value is false. + public bool ExcludeAdHocModels { get; set; } + /// public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) { @@ -46,10 +66,17 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp throw new ArgumentNullException( nameof( model ) ); } - if ( !adHoc && model.IsAdHoc() ) + if ( model.IsAdHoc() ) { - adHocBuilder ??= new( adHoc: true ); - return adHocBuilder.NewStructuredType( model, structuredType, clrType, apiVersion ); + if ( ExcludeAdHocModels ) + { + return clrType; + } + else if ( !adHoc ) + { + adHocBuilder ??= new( adHoc: true ); + return adHocBuilder.NewStructuredType( model, structuredType, clrType, apiVersion ); + } } if ( structuredType == null ) From 8ec6e0a015ff5d838ea15ef7a25697e9576ae05e Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 23 Dec 2022 17:08:56 -0800 Subject: [PATCH 29/35] Fix code analysis warning --- .../DependencyInjection/IServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs index cb86af84..66e5f8ad 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Provides extension methods for . /// +[CLSCompliant( false )] public static class IServiceCollectionExtensions { [MethodImpl( MethodImplOptions.AggressiveInlining )] From 8d3f6e882f8c50a3f473098b7f868e944852b1c8 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 27 Dec 2022 09:50:21 -0800 Subject: [PATCH 30/35] Add support for exploring OData query options using an ad hoc EDM. Related #928 --- .../ApiExplorer/AdHocEdmScope.cs | 100 ++++++++ .../ApiExplorer/ODataApiExplorer.cs | 58 ++++- .../ApiExplorer/ODataApiExplorerOptions.cs | 3 +- .../ImplicitModelBoundSettingsConvention.cs | 31 +++ .../ODataQueryOptionsConventionBuilder.cs | 17 -- .../Description/ApiDescriptionExtensions.cs | 154 ++++++------ .../Description/ODataApiExplorerTest.cs | 34 +++ .../Simulators/Models/Book.cs | 3 + .../Simulators/V1/BooksController.cs | 3 + .../ApiExplorer/ApiDescriptionExtensions.cs | 21 ++ .../ApiExplorer/ODataApiExplorerOptions.cs | 39 +++ .../ODataApiExplorerOptionsFactory.cs | 84 +++++++ .../PartialODataDescriptionProvider.cs | 230 ++++++++++++++++++ .../ImplicitModelBoundSettingsConvention.cs | 49 ++++ .../IApiVersioningBuilderExtensions.cs | 9 +- .../ODataApiDescriptionProviderTest.cs | 27 ++ .../Simulators/Models/Book.cs | 3 + .../ApiExplorerOptionsFactory{T}.cs | 20 +- .../ApiExplorer/ODataApiExplorerOptions.cs | 10 + .../ImplicitModelBoundSettingsConvention.cs | 84 +++++++ .../ODataQueryOptionsConventionBuilder.cs | 2 +- 21 files changed, 872 insertions(+), 109 deletions(-) create mode 100644 src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/AdHocEdmScope.cs create mode 100644 src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs create mode 100644 src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ApiDescriptionExtensions.cs create mode 100644 src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs create mode 100644 src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs create mode 100644 src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs create mode 100644 src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs create mode 100644 src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/AdHocEdmScope.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/AdHocEdmScope.cs new file mode 100644 index 00000000..2bcf1549 --- /dev/null +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/AdHocEdmScope.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning.Conventions; +using Asp.Versioning.Description; +using Asp.Versioning.OData; +using Microsoft.OData.Edm; +using System.Collections.Generic; +using System.Web.Http; +using System.Web.Http.Description; + +internal sealed class AdHocEdmScope : IDisposable +{ + private readonly IReadOnlyList results; + private bool disposed; + + internal AdHocEdmScope( + IReadOnlyList apiDescriptions, + VersionedODataModelBuilder builder ) + { + var conventions = builder.ModelConfigurations.OfType().ToArray(); + + results = FilterResults( apiDescriptions, conventions ); + + if ( results.Count > 0 ) + { + ApplyAdHocEdm( builder.GetEdmModels(), results ); + } + } + + public void Dispose() + { + if ( disposed ) + { + return; + } + + disposed = true; + + for ( var i = 0; i < results.Count; i++ ) + { + results[i].SetProperty( default( IEdmModel ) ); + } + } + + private static IReadOnlyList FilterResults( + IReadOnlyList apiDescriptions, + IReadOnlyList conventions ) + { + if ( conventions.Count == 0 ) + { + return Array.Empty(); + } + + var results = default( List ); + + for ( var i = 0; i < apiDescriptions.Count; i++ ) + { + var apiDescription = apiDescriptions[i]; + + if ( apiDescription.EdmModel() != null || !apiDescription.IsODataLike() ) + { + continue; + } + + results ??= new(); + results.Add( apiDescription ); + + for ( var j = 0; j < conventions.Count; j++ ) + { + conventions[j].ApplyTo( apiDescription ); + } + } + + return results?.ToArray() ?? Array.Empty(); + } + + private static void ApplyAdHocEdm( + IReadOnlyList models, + IReadOnlyList results ) + { + for ( var i = 0; i < models.Count; i++ ) + { + var model = models[i]; + var version = model.GetApiVersion(); + + for ( var j = 0; j < results.Count; j++ ) + { + var result = results[j]; + var metadata = result.ActionDescriptor.GetApiVersionMetadata(); + + if ( metadata.IsMappedTo( version ) ) + { + result.SetProperty( model ); + } + } + } + } +} \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs index f6fda7f9..4c45eb70 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs @@ -7,6 +7,7 @@ namespace Asp.Versioning.ApiExplorer; using Asp.Versioning.OData; using Asp.Versioning.Routing; using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Formatter; using Microsoft.AspNet.OData.Routing; @@ -16,6 +17,7 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.OData.UriParser; using System.Collections.ObjectModel; using System.Net.Http.Formatting; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Web.Http; using System.Web.Http.Controllers; @@ -50,7 +52,11 @@ public ODataApiExplorer( HttpConfiguration configuration ) /// The current HTTP configuration. /// The associated API explorer options. public ODataApiExplorer( HttpConfiguration configuration, ODataApiExplorerOptions options ) - : base( configuration, options ) => this.options = options; + : base( configuration, options ) + { + this.options = options; + options.AdHocModelBuilder.OnModelCreated += MarkAsAdHoc; + } /// /// Gets the options associated with the API explorer. @@ -172,7 +178,20 @@ protected override Collection ExploreRouteControllers( if ( route is not ODataRoute ) { apiDescriptions = base.ExploreRouteControllers( controllerMappings, route, apiVersion ); - return ExploreQueryOptions( route, apiDescriptions ); + + if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + else if ( apiDescriptions.Count > 0 ) + { + using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + } + + return apiDescriptions; } apiDescriptions = new(); @@ -199,7 +218,8 @@ protected override Collection ExploreRouteControllers( } } - return ExploreQueryOptions( route, apiDescriptions ); + ExploreQueryOptions( route, apiDescriptions ); + return apiDescriptions; } /// @@ -210,7 +230,25 @@ protected override Collection ExploreDirectRouteControl ApiVersion apiVersion ) { var apiDescriptions = base.ExploreDirectRouteControllers( controllerDescriptor, candidateActionDescriptors, route, apiVersion ); - return ExploreQueryOptions( route, apiDescriptions ); + + if ( apiDescriptions.Count == 0 ) + { + return apiDescriptions; + } + + if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + else if ( apiDescriptions.Count > 0 ) + { + using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + } + + return apiDescriptions; } /// @@ -238,20 +276,20 @@ protected virtual void ExploreQueryOptions( queryOptions.ApplyTo( apiDescriptions, settings ); } - private Collection ExploreQueryOptions( - IHttpRoute route, - Collection apiDescriptions ) + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) => + model.SetAnnotationValue( model, AdHocAnnotation.Instance ); + + private void ExploreQueryOptions( IHttpRoute route, Collection apiDescriptions ) { if ( apiDescriptions.Count == 0 ) { - return apiDescriptions; + return; } var uriResolver = Configuration.GetODataRootContainer( route ).GetRequiredService(); ExploreQueryOptions( apiDescriptions, uriResolver ); - - return apiDescriptions; } private ResponseDescription CreateResponseDescriptionWithRoute( diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs index b9d34d6b..4bf5b113 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs @@ -15,7 +15,8 @@ public partial class ODataApiExplorerOptions : ApiExplorerOptions /// Initializes a new instance of the class. /// /// The current configuration associated with the options. - public ODataApiExplorerOptions( HttpConfiguration configuration ) : base( configuration ) { } + public ODataApiExplorerOptions( HttpConfiguration configuration ) + : base( configuration ) => AdHocModelBuilder = new( configuration ); /// /// Gets or sets a value indicating whether the API explorer settings are honored. diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs new file mode 100644 index 00000000..771af4de --- /dev/null +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Conventions; + +using Asp.Versioning.OData; +using System.Web.Http.Description; + +/// +/// Provides additional implementation specific to ASP.NET Web API. +/// +public partial class ImplicitModelBoundSettingsConvention : IModelConfiguration, IODataQueryOptionsConvention +{ + /// + public void ApplyTo( ApiDescription apiDescription ) + { + var response = apiDescription.ResponseDescription; + var type = response.ResponseType ?? response.DeclaredType; + + if ( type == null ) + { + return; + } + + if ( type.IsEnumerable( out var itemType ) ) + { + type = itemType; + } + + types.Add( type! ); + } +} \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs index 04f8cf6d..8b297ebb 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs @@ -2,7 +2,6 @@ namespace Asp.Versioning.Conventions; -using Microsoft.AspNet.OData; using System.Runtime.CompilerServices; using System.Web.Http.Description; @@ -14,20 +13,4 @@ public partial class ODataQueryOptionsConventionBuilder [MethodImpl( MethodImplOptions.AggressiveInlining )] private static Type GetController( ApiDescription apiDescription ) => apiDescription.ActionDescriptor.ControllerDescriptor.ControllerType; - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static bool IsODataLike( ApiDescription description ) - { - var parameters = description.ParameterDescriptions; - - for ( var i = 0; i < parameters.Count; i++ ) - { - if ( parameters[i].ParameterDescriptor.ParameterType.IsODataQueryOptions() ) - { - return true; - } - } - - return false; - } } \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs index 3c19916d..cba8d13e 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs @@ -1,100 +1,114 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -namespace System.Web.Http.Description -{ - using Asp.Versioning.Description; - using Microsoft.AspNet.OData.Routing; - using Microsoft.OData.Edm; +namespace System.Web.Http.Description; + +using Asp.Versioning.Description; +using Microsoft.AspNet.OData.Routing; +using Microsoft.OData.Edm; +/// +/// Provides extension methods for the class. +/// +public static class ApiDescriptionExtensions +{ /// - /// Provides extension methods for the class. + /// Gets the entity data model (EDM) associated with the API description. /// - public static class ApiDescriptionExtensions + /// The API description to get the model for. + /// The associated EDM model or null if there is no associated model. + public static IEdmModel? EdmModel( this ApiDescription apiDescription ) { - /// - /// Gets the entity data model (EDM) associated with the API description. - /// - /// The API description to get the model for. - /// The associated EDM model or null if there is no associated model. - public static IEdmModel? EdmModel( this ApiDescription apiDescription ) + if ( apiDescription is VersionedApiDescription description ) { - if ( apiDescription is VersionedApiDescription description ) - { - return description.GetProperty(); - } + return description.GetProperty(); + } + return default; + } + + /// + /// Gets the entity set associated with the API description. + /// + /// The API description to get the entity set for. + /// The associated entity set or null if there is no associated entity set. + public static IEdmEntitySet? EntitySet( this ApiDescription apiDescription ) + { + if ( apiDescription is not VersionedApiDescription description ) + { return default; } - /// - /// Gets the entity set associated with the API description. - /// - /// The API description to get the entity set for. - /// The associated entity set or null if there is no associated entity set. - public static IEdmEntitySet? EntitySet( this ApiDescription apiDescription ) + var key = typeof( IEdmEntitySet ); + + if ( description.Properties.TryGetValue( key, out var value ) ) { - if ( apiDescription is not VersionedApiDescription description ) - { - return default; - } + return (IEdmEntitySet) value; + } - var key = typeof( IEdmEntitySet ); + var container = description.EdmModel()?.EntityContainer; - if ( description.Properties.TryGetValue( key, out var value ) ) - { - return (IEdmEntitySet) value; - } + if ( container == null ) + { + return default; + } - var container = description.EdmModel()?.EntityContainer; + var entitySetName = description.ActionDescriptor.ControllerDescriptor.ControllerName; + var entitySet = container.FindEntitySet( entitySetName ); - if ( container == null ) - { - return default; - } + description.Properties[key] = entitySet; - var entitySetName = description.ActionDescriptor.ControllerDescriptor.ControllerName; - var entitySet = container.FindEntitySet( entitySetName ); + return entitySet; + } - description.Properties[key] = entitySet; + /// + /// Gets the entity type associated with the API description. + /// + /// The API description to get the entity type for. + /// The associated entity type or null if there is no associated entity type. + public static IEdmEntityType? EntityType( this ApiDescription apiDescription ) => apiDescription.EntitySet()?.EntityType(); - return entitySet; + /// + /// Gets the operation associated with the API description. + /// + /// The API description to get the operation for. + /// The associated EDM operation or null if there is no associated operation. + public static IEdmOperation? Operation( this ApiDescription apiDescription ) + { + if ( apiDescription is VersionedApiDescription description ) + { + return description.GetProperty(); } - /// - /// Gets the entity type associated with the API description. - /// - /// The API description to get the entity type for. - /// The associated entity type or null if there is no associated entity type. - public static IEdmEntityType? EntityType( this ApiDescription apiDescription ) => apiDescription.EntitySet()?.EntityType(); - - /// - /// Gets the operation associated with the API description. - /// - /// The API description to get the operation for. - /// The associated EDM operation or null if there is no associated operation. - public static IEdmOperation? Operation( this ApiDescription apiDescription ) - { - if ( apiDescription is VersionedApiDescription description ) - { - return description.GetProperty(); - } + return default; + } - return default; + /// + /// Gets the route prefix associated with the API description. + /// + /// The API description to get the route prefix for. + /// The associated route prefix or null. + public static string? RoutePrefix( this ApiDescription apiDescription ) + { + if ( apiDescription == null ) + { + throw new ArgumentNullException( nameof( apiDescription ) ); } - /// - /// Gets the route prefix associated with the API description. - /// - /// The API description to get the route prefix for. - /// The associated route prefix or null. - public static string? RoutePrefix( this ApiDescription apiDescription ) + return apiDescription.Route is ODataRoute route ? route.RoutePrefix : default; + } + + internal static bool IsODataLike( this ApiDescription description ) + { + var parameters = description.ParameterDescriptions; + + for ( var i = 0; i < parameters.Count; i++ ) { - if ( apiDescription == null ) + if ( parameters[i].ParameterDescriptor.ParameterType.IsODataQueryOptions() ) { - throw new ArgumentNullException( nameof( apiDescription ) ); + return true; } - - return apiDescription.Route is ODataRoute route ? route.RoutePrefix : default; } + + return false; } } \ No newline at end of file diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs index 534d10a3..34540a00 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs @@ -3,9 +3,13 @@ namespace Asp.Versioning.Description; using Asp.Versioning.ApiExplorer; +using Asp.Versioning.Controllers; +using Asp.Versioning.Conventions; using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; using System.Net.Http; using System.Web.Http; +using System.Web.Http.Dispatcher; using static System.Net.Http.HttpMethod; public class ODataApiExplorerTest @@ -210,4 +214,34 @@ public void api_description_group_should_explore_navigation_properties() }, options => options.ExcludingMissingMembers() ); } + + [Fact] + public void api_description_group_should_explore_model_bound_settings() + { + // arrange + var configuration = new HttpConfiguration(); + var controllerTypeResolver = new ControllerTypeCollection( + typeof( VersionedMetadataController ), + typeof( Simulators.V1.BooksController ) ); + + configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver ); + configuration.EnableDependencyInjection(); + configuration.AddApiVersioning(); + configuration.MapHttpAttributeRoutes(); + + var apiVersion = new ApiVersion( 1.0 ); + var options = new ODataApiExplorerOptions( configuration ); + var apiExplorer = new ODataApiExplorer( configuration, options ); + + options.AdHocModelBuilder.ModelConfigurations.Add( new ImplicitModelBoundSettingsConvention() ); + + // act + var descriptionGroup = apiExplorer.ApiDescriptions[apiVersion]; + var description = descriptionGroup.ApiDescriptions[0]; + + // assert + var parameter = description.ParameterDescriptions.Single( p => p.Name == "$filter" ); + + parameter.Documentation.Should().EndWith( "author, published." ); + } } \ No newline at end of file diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs index 404bf071..72b12f9b 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs @@ -2,6 +2,9 @@ namespace Asp.Versioning.Simulators.Models; +using Microsoft.AspNet.OData.Query; + +[Filter( "author", "published" )] public class Book { public string Id { get; set; } diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs index e6e24820..24d7a24e 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs @@ -14,6 +14,7 @@ namespace Asp.Versioning.Simulators.V1; /// Represents a RESTful service of books. /// [ApiVersion( 1.0 )] +[RoutePrefix( "api/books" )] public class BooksController : ApiController { private static readonly Book[] books = new Book[] @@ -33,6 +34,7 @@ public class BooksController : ApiController /// All available books. /// The successfully retrieved books. [HttpGet] + [Route] [ResponseType( typeof( IEnumerable ) )] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( options.ApplyTo( books.AsQueryable() ) ); @@ -46,6 +48,7 @@ public IHttpActionResult Get( ODataQueryOptions options ) => /// The book was successfully retrieved. /// The book does not exist. [HttpGet] + [Route( "{id}" )] [ResponseType( typeof( Book ) )] public IHttpActionResult Get( string id, ODataQueryOptions options ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ApiDescriptionExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ApiDescriptionExtensions.cs new file mode 100644 index 00000000..29520295 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ApiDescriptionExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Microsoft.AspNetCore.Mvc.ApiExplorer; + +internal static class ApiDescriptionExtensions +{ + internal static bool IsODataLike( this ApiDescription description ) + { + var parameters = description.ActionDescriptor.Parameters; + + for ( var i = 0; i < parameters.Count; i++ ) + { + if ( parameters[i].ParameterType.IsODataQueryOptions() ) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs new file mode 100644 index 00000000..44a737e0 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning.OData; +using Microsoft.Extensions.Options; +using System.ComponentModel; + +/// +/// Provides additional implementation specific to ASP.NET Core. +/// +public partial class ODataApiExplorerOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// This constructor is only intended to satisfy the parameterless constructor + /// requirement on the type constraint. + [EditorBrowsable( EditorBrowsableState.Never )] + public ODataApiExplorerOptions() => + AdHocModelBuilder = new( ODataApiVersionCollectionProvider.Empty, Enumerable.Empty() ); + + /// + /// Initializes a new instance of the class. + /// + /// The associated model builder. + [CLSCompliant( false )] + public ODataApiExplorerOptions( VersionedODataModelBuilder modelBuilder ) => + AdHocModelBuilder = modelBuilder ?? throw new ArgumentNullException( nameof( modelBuilder ) ); + + private sealed class ODataApiVersionCollectionProvider : IODataApiVersionCollectionProvider + { + private ODataApiVersionCollectionProvider() { } + + internal static ODataApiVersionCollectionProvider Empty { get; } = new(); + + public IReadOnlyList ApiVersions { get; set; } = Array.Empty(); + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs new file mode 100644 index 00000000..1d758b5a --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning.OData; +using Microsoft.Extensions.Options; +using static Asp.Versioning.ApiVersionMapping; + +/// +/// Represents a factory to create OData API explorer options. +/// +[CLSCompliant( false )] +public class ODataApiExplorerOptionsFactory : ApiExplorerOptionsFactory +{ + private readonly IApiVersionMetadataCollationProvider[] providers; + private readonly IEnumerable modelConfigurations; + + /// + /// Initializes a new instance of the class. + /// + /// A sequence of + /// providers used to collate API version metadata. + /// A sequence of + /// configurations used to configure Entity Data Models. + /// The API versioning options + /// used to create API explorer options. + /// The sequence of + /// configuration actions to run. + /// The sequence of + /// initialization actions to run. + public ODataApiExplorerOptionsFactory( + IEnumerable providers, + IEnumerable modelConfigurations, + IOptions options, + IEnumerable> setups, + IEnumerable> postConfigures ) + : base( options, setups, postConfigures ) + { + this.providers = ( providers ?? throw new ArgumentNullException( nameof( providers ) ) ).ToArray(); + this.modelConfigurations = modelConfigurations ?? throw new ArgumentNullException( nameof( modelConfigurations ) ); + } + + /// + protected override ODataApiExplorerOptions CreateInstance( string name ) => + new( new( CollateApiVersions( providers, Options ), modelConfigurations ) ); + + private static ODataApiVersionCollectionProvider CollateApiVersions( + IApiVersionMetadataCollationProvider[] providers, + ApiVersioningOptions options ) + { + var context = new ApiVersionMetadataCollationContext(); + + for ( var i = 0; i < providers.Length; i++ ) + { + providers[i].Execute( context ); + } + + var results = context.Results; + var versions = new SortedSet(); + + for ( var i = 0; i < results.Count; i++ ) + { + var model = results[i].Map( Implicit ); + var declared = model.DeclaredApiVersions; + + for ( var j = 0; j < declared.Count; j++ ) + { + versions.Add( declared[j] ); + } + } + + if ( versions.Count == 0 ) + { + versions.Add( options.DefaultApiVersion ); + } + + return new() { ApiVersions = versions.ToArray() }; + } + + private sealed class ODataApiVersionCollectionProvider : IODataApiVersionCollectionProvider + { + public IReadOnlyList ApiVersions { get; set; } = Array.Empty(); + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs new file mode 100644 index 00000000..c90ab9b7 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs @@ -0,0 +1,230 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning; +using Asp.Versioning.Conventions; +using Asp.Versioning.OData; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Routing; +using Microsoft.AspNetCore.OData.Routing.Template; +using Microsoft.Extensions.Options; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using System.Runtime.CompilerServices; +using Opts = Microsoft.Extensions.Options.Options; + +/// +/// Represents an API description provider for partial OData support. +/// +[CLSCompliant( false )] +public class PartialODataDescriptionProvider : IApiDescriptionProvider +{ + private static int? beforeOData; + private readonly IOptionsFactory odataOptionsFactory; + private readonly IOptions options; + private bool markedAdHoc; + private IODataQueryOptionsConvention[]? conventions; + + /// + /// Initializes a new instance of the class. + /// + /// The factory used to create + /// OData options. + /// The container of configured + /// API explorer options. + public PartialODataDescriptionProvider( + IOptionsFactory odataOptionsFactory, + IOptions options ) + { + this.odataOptionsFactory = odataOptionsFactory ?? throw new ArgumentNullException( nameof( odataOptionsFactory ) ); + this.options = options ?? throw new ArgumentNullException( nameof( options ) ); + beforeOData ??= ODataOrder( odataOptionsFactory, options ) + 10; + Order = beforeOData.Value; + } + + /// + /// Gets the associated OData API explorer options. + /// + /// The current OData API explorer options. + protected ODataApiExplorerOptions Options + { + get + { + var value = options.Value; + + if ( !markedAdHoc ) + { + value.AdHocModelBuilder.OnModelCreated += MarkAsAdHoc; + markedAdHoc = true; + } + + return value; + } + } + + /// + /// Gets the builder used to create ad hoc Entity Data Models (EDMs). + /// + /// The associated model builder. + protected VersionedODataModelBuilder ModelBuilder => Options.AdHocModelBuilder; + + /// + /// Gets associated the OData query option conventions. + /// + /// A read-only list of + /// OData query option conventions. + protected IReadOnlyList Conventions => + conventions ??= Options.AdHocModelBuilder.ModelConfigurations.OfType().ToArray(); + + /// + /// Gets or sets the order precedence of the current API description provider. + /// + /// The order precedence of the current API description provider. + public int Order { get; protected set; } + + /// + public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + var results = FilterResults( context.Results, Conventions ); + + if ( results.Count == 0 ) + { + return; + } + + var models = ModelBuilder.GetEdmModels(); + + for ( var i = 0; i < models.Count; i++ ) + { + var model = models[i]; + var version = model.GetApiVersion(); + var options = odataOptionsFactory.Create( Opts.DefaultName ); + + options.AddRouteComponents( model ); + + for ( var j = 0; j < results.Count; j++ ) + { + var result = results[j]; + var metadata = result.ActionDescriptor.GetApiVersionMetadata(); + + if ( metadata.IsMappedTo( version ) ) + { + result.ActionDescriptor.EndpointMetadata.Add( ODataMetadata.New( model ) ); + } + } + } + } + + /// + public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + var actions = context.Actions; + + for ( var i = 0; i < actions.Count; i++ ) + { + var metadata = actions[i].EndpointMetadata; + + for ( var j = metadata.Count - 1; j >= 0; j-- ) + { + if ( metadata[j] is IODataRoutingMetadata routing && routing.Model.IsAdHoc() ) + { + metadata.Remove( j ); + } + } + } + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static int ODataOrder( IOptionsFactory factory, IOptions options ) => + new ODataApiDescriptionProvider( + new StubModelMetadataProvider(), + new StubModelTypeBuilder(), + factory, + options ).Order; + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) => + model.SetAnnotationValue( model, AdHocAnnotation.Instance ); + + private static IReadOnlyList FilterResults( + IList results, + IReadOnlyList conventions ) + { + var filtered = default( List ); + + for ( var i = 0; i < results.Count; i++ ) + { + var result = results[i]; + var metadata = result.ActionDescriptor.EndpointMetadata; + var odata = false; + + for ( var j = 0; j < metadata.Count; j++ ) + { + if ( metadata[j] is IODataRoutingMetadata ) + { + odata = true; + break; + } + } + + if ( odata || !result.IsODataLike() ) + { + continue; + } + + filtered ??= new( capacity: results.Count ); + filtered.Add( result ); + + for ( var j = 0; j < conventions.Count; j++ ) + { + conventions[j].ApplyTo( result ); + } + } + + return filtered?.ToArray() ?? Array.Empty(); + } + + private sealed class StubModelMetadataProvider : IModelMetadataProvider + { + public IEnumerable GetMetadataForProperties( Type modelType ) => + throw new NotImplementedException(); + + public ModelMetadata GetMetadataForType( Type modelType ) => + throw new NotImplementedException(); + } + + private sealed class StubModelTypeBuilder : IModelTypeBuilder + { + public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ) => + throw new NotImplementedException(); + + public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) => + throw new NotImplementedException(); + } + + private static class ODataMetadata + { + private const string ArbitrarySegment = "52459ff8-bca1-4a26-b7f2-08c7da04472d"; + + // metadata (~/$metadata) and service (~/) doc have special handling. + // make sure we don't match the service doc + private static readonly ODataPathTemplate AdHocODataTemplate = + new( new DynamicSegmentTemplate( new( ArbitrarySegment ) ) ); + + public static ODataRoutingMetadata New( IEdmModel model ) => new( string.Empty, model, AdHocODataTemplate ); + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs new file mode 100644 index 00000000..3800afd6 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Conventions; + +using Asp.Versioning; +using Asp.Versioning.OData; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OData.ModelBuilder; + +/// +/// Provides additional implementation specific to ASP.NET Core. +/// +[CLSCompliant( false )] +public partial class ImplicitModelBoundSettingsConvention +{ + /// + public void ApplyTo( ApiDescription apiDescription ) + { + if ( apiDescription == null ) + { + throw new ArgumentNullException( nameof( apiDescription ) ); + } + + var responses = apiDescription.SupportedResponseTypes; + + for ( var j = 0; j < responses.Count; j++ ) + { + var response = responses[j]; + var notForSuccess = response.StatusCode < 200 || response.StatusCode >= 300; + + if ( notForSuccess ) + { + continue; + } + + var model = response.ModelMetadata; + var type = model == null + ? response.Type + : model.IsEnumerableType + ? model.ElementType + : model.UnderlyingOrModelType; + + if ( type != null ) + { + types.Add( type ); + } + } + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs index 27283ec7..d19c8ed5 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -4,6 +4,7 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; using Asp.Versioning.ApiExplorer; +using Asp.Versioning.Conventions; using Asp.Versioning.OData; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -55,9 +56,12 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) var services = builder.Services; builder.AddApiExplorer(); + builder.Services.AddModelConfigurationsAsServices(); services.TryAddSingleton(); - services.TryAddSingleton, ApiExplorerOptionsFactory>(); + services.TryAddSingleton, ODataApiExplorerOptionsFactory>(); + services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Transient() ); + services.TryAddEnumerable( Transient() ); services.Replace( Singleton, ODataApiExplorerOptionsAdapter>() ); } @@ -67,8 +71,7 @@ private sealed class ODataApiExplorerOptionsAdapter : IOptionsFactory factory; - public ODataApiExplorerOptionsAdapter( IOptionsFactory factory ) => - this.factory = factory; + public ODataApiExplorerOptionsAdapter( IOptionsFactory factory ) => this.factory = factory; public ApiExplorerOptions Create( string name ) => factory.Create( name ); } diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs index 3c3f3136..511513b4 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs @@ -9,6 +9,7 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.OData; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Buffers; public class ODataApiDescriptionProviderTest { @@ -152,6 +153,8 @@ private void AssertVersion1( ApiDescriptionGroup group ) new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" }, }, options => options.ExcludingMissingMembers() ); + + AssertQueryOptionWithoutOData( items[0], "filter", "author", "published" ); } private void AssertVersion2( ApiDescriptionGroup group ) @@ -229,6 +232,30 @@ private void AssertVersion3( ApiDescriptionGroup group ) items.Should().BeEquivalentTo( expected, options => options.ExcludingMissingMembers() ); } + private static void AssertQueryOptionWithoutOData( ApiDescription description, string name, string property, params string[] otherProperties ) + { + var parameter = description.ParameterDescriptions.Single( p => p.Name == name ); + var count = otherProperties.Length + 1; + string suffix; + + if ( count == 1 ) + { + suffix = property; + } + else + { + var pool = ArrayPool.Shared; + var properties = pool.Rent( count ); + + properties[0] = property; + Array.Copy( otherProperties, 0, properties, 1, count - 1 ); + + suffix = string.Join( ", ", properties, 0, count ); + } + + parameter.ModelMetadata.Description.Should().EndWith( suffix + '.' ); + } + private void PrintGroup( IReadOnlyList items ) { for ( var i = 0; i < items.Count; i++ ) diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs index 00fec7f1..97610da6 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs @@ -2,9 +2,12 @@ namespace Asp.Versioning.Simulators.Models; +using Microsoft.OData.ModelBuilder; + /// /// Represents a book. /// +[Filter( "author", "published" )] public class Book { /// diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs index 810f3a25..ebe3a05a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs @@ -53,17 +53,23 @@ public ApiExplorerOptionsFactory( /// initialization actions to run. protected IEnumerable> PostConfigures { get; } + /// + /// Creates and returns a new options instance. + /// + /// The name associated with the options instance. + /// A new options instance. + protected virtual T CreateInstance( string name ) => new(); + /// public virtual T Create( string name ) { var apiVersioningOptions = Options; - var options = new T() - { - AssumeDefaultVersionWhenUnspecified = apiVersioningOptions.AssumeDefaultVersionWhenUnspecified, - ApiVersionParameterSource = apiVersioningOptions.ApiVersionReader, - DefaultApiVersion = apiVersioningOptions.DefaultApiVersion, - RouteConstraintName = apiVersioningOptions.RouteConstraintName, - }; + var options = CreateInstance( name ); + + options.AssumeDefaultVersionWhenUnspecified = apiVersioningOptions.AssumeDefaultVersionWhenUnspecified; + options.ApiVersionParameterSource = apiVersioningOptions.ApiVersionReader; + options.DefaultApiVersion = apiVersioningOptions.DefaultApiVersion; + options.RouteConstraintName = apiVersioningOptions.RouteConstraintName; foreach ( var setup in Setups ) { diff --git a/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs b/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs index c90cafb5..45acaa44 100644 --- a/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs @@ -3,6 +3,7 @@ namespace Asp.Versioning.ApiExplorer; using Asp.Versioning.Conventions; +using Asp.Versioning.OData; /// /// Represents the possible API versioning options for an OData API explorer. @@ -44,4 +45,13 @@ public ODataQueryOptionsConventionBuilder QueryOptions /// /// One or more values. public ODataMetadataOptions MetadataOptions { get; set; } = ODataMetadataOptions.None; + + /// + /// Gets the builder used to create ad hoc versioned Entity Data Models (EDMs). + /// + /// The associated model builder. +#if !NETFRAMEWORK + [CLSCompliant( false )] +#endif + public VersionedODataModelBuilder AdHocModelBuilder { get; } } \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs new file mode 100644 index 00000000..39dd9f3b --- /dev/null +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Conventions; + +using Asp.Versioning; +using Asp.Versioning.OData; +#if NETFRAMEWORK +using Microsoft.AspNet.OData.Builder; +#else +using Microsoft.OData.ModelBuilder; +#endif + +/// +/// Represents an OData model bound settings model configuration +/// that is also an OData query options convention. +/// +public sealed partial class ImplicitModelBoundSettingsConvention : IModelConfiguration, IODataQueryOptionsConvention +{ + private readonly HashSet types = new(); + + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix ) + { + if ( builder == null ) + { + throw new ArgumentNullException( nameof( builder ) ); + } + + if ( types.Count == 0 ) + { + return; + } + + if ( GetExistingTypes( builder ) is HashSet existingTypes ) + { + types.ExceptWith( existingTypes ); + } + + if ( types.Count == 0 ) + { + return; + } + + // model configurations are applied unordered, which could matter. + // defer implicit registrations in the model until all other model + // configurations have been applied, if possible + if ( builder is ODataConventionModelBuilder modelBuilder ) + { + modelBuilder.OnModelCreating += OnModelCreating; + } + else + { + OnModelCreating( builder ); + } + } + + private static HashSet? GetExistingTypes( ODataModelBuilder builder ) + { + var types = default( HashSet ); + + foreach ( var entitySet in builder.EntitySets ) + { + types ??= new(); + types.Add( entitySet.ClrType ); + } + + foreach ( var singleton in builder.Singletons ) + { + types ??= new(); + types.Add( singleton.ClrType ); + } + + return types; + } + + private void OnModelCreating( ODataModelBuilder builder ) + { + foreach ( var type in types ) + { + var entityType = builder.AddEntityType( type ); + builder.AddEntitySet( entityType.Name, entityType ); + } + } +} \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs index bf2c4b6b..054f5641 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs @@ -135,7 +135,7 @@ public virtual void ApplyTo( IEnumerable apiDescriptions, ODataQ { var controller = GetController( description ); - if ( !controller.IsODataController() && !IsODataLike( description ) ) + if ( !controller.IsODataController() && !description.IsODataLike() ) { continue; } From 7e4c2520bd7085a15c3a22b868c08ade467c544b Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Thu, 8 Dec 2022 11:42:51 -0800 Subject: [PATCH 31/35] Default to excluding ad hoc model substitution (more common) --- .../OData/DefaultModelTypeBuilder.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs index 37c3bebc..4b969183 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs @@ -31,32 +31,32 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder * incorrect bucket is picked, then the type mapping will fail. the model type builder detects if a model * is ad hoc. if it is, then it will recursively create a private instance of itself to handle the ad hoc * bucket. normal odata cannot opt out of this process because the explored type must match the edm. a type - * mapped via an ad hoc edm is not really odata so it can opt out if desired. the opt out process is more - * of a failsafe and optimization. if the ad hoc edm wasn't customized, then the meta model and type should - * be exactly the same, which will result in no substitution. + * mapped via an ad hoc edm is not really odata so it should opt out by default because without an edm + * there is not away to control member serialization/deserialization easily. such cases will typically + * create a type-per-version, as is common for non-odata, which negates the need for model substitution. + * a user can opt into ad hoc model substitution if they have a way to deal with member filtering. */ private static Type? ienumerableOfT; private readonly bool adHoc; + private readonly bool excludeAdHocModels; private DefaultModelTypeBuilder? adHocBuilder; private ConcurrentDictionary? modules; private ConcurrentDictionary>? generatedEdmTypesPerVersion; private ConcurrentDictionary>? generatedActionParamsPerVersion; - private DefaultModelTypeBuilder( bool adHoc ) => this.adHoc = adHoc; + private DefaultModelTypeBuilder( bool excludeAdHocModels, bool adHoc ) + { + this.adHoc = adHoc; + this.excludeAdHocModels = excludeAdHocModels; + } /// /// Initializes a new instance of the class. /// - public DefaultModelTypeBuilder() { } - - /// - /// Gets or sets a value indicating whether types from an ad hoc Entity Data Model - /// (EDM) should be excluded. - /// - /// True if types from an ad hoc EDM are excluded; otherwise, false. The - /// default value is false. - public bool ExcludeAdHocModels { get; set; } + /// Indicates whether types from an ad hoc Entity + /// Data Model (EDM) should be included. + public DefaultModelTypeBuilder( bool includeAdHocModels = false ) => excludeAdHocModels = !includeAdHocModels; /// public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) @@ -68,13 +68,13 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp if ( model.IsAdHoc() ) { - if ( ExcludeAdHocModels ) + if ( excludeAdHocModels ) { return clrType; } else if ( !adHoc ) { - adHocBuilder ??= new( adHoc: true ); + adHocBuilder ??= new( excludeAdHocModels, adHoc: true ); return adHocBuilder.NewStructuredType( model, structuredType, clrType, apiVersion ); } } @@ -111,7 +111,7 @@ public Type NewActionParameters( IEdmModel model, IEdmAction action, string cont if ( !adHoc && model.IsAdHoc() ) { - adHocBuilder ??= new( adHoc: true ); + adHocBuilder ??= new( excludeAdHocModels, adHoc: true ); return adHocBuilder.NewActionParameters( model, action, controllerName, apiVersion ); } From aa8c832ef86fa49612ea7eb7691901a6b4a2485f Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 27 Dec 2022 11:35:59 -0800 Subject: [PATCH 32/35] Fix empty model check; misses only complex types --- .../src/Common.OData/OData/VersionedODataModelBuilder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs b/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs index c54ad30f..f1fa3c43 100644 --- a/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs +++ b/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs @@ -110,11 +110,10 @@ private void BuildModelPerApiVersion( configurations[j].Apply( builder, apiVersion, routePrefix ); } + const int EntityContainerOnly = 1; var model = builder.GetEdmModel(); var container = model.EntityContainer; - var empty = !container.EntitySets().Any() && - !container.Singletons().Any() && - !container.OperationImports().Any(); + var empty = model.SchemaElements.Count() == EntityContainerOnly; if ( empty ) { From 336ef8073495ec3b67601ca10e910e2a968118b2 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 27 Dec 2022 11:36:49 -0800 Subject: [PATCH 33/35] Switch ad hoc models to be complex types instead of entities; skips validation --- .../ImplicitModelBoundSettingsConvention.cs | 3 - .../ImplicitModelBoundSettingsConvention.cs | 56 ++++++++++++++++--- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs index 3800afd6..1e7fe4e0 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -2,10 +2,7 @@ namespace Asp.Versioning.Conventions; -using Asp.Versioning; -using Asp.Versioning.OData; using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OData.ModelBuilder; /// /// Provides additional implementation specific to ASP.NET Core. diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs index 39dd9f3b..555fd9bc 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -8,6 +8,7 @@ namespace Asp.Versioning.Conventions; using Microsoft.AspNet.OData.Builder; #else using Microsoft.OData.ModelBuilder; +using System.Buffers; #endif /// @@ -56,18 +57,56 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? rou private static HashSet? GetExistingTypes( ODataModelBuilder builder ) { - var types = default( HashSet ); + HashSet types; - foreach ( var entitySet in builder.EntitySets ) + if ( builder.StructuralTypes is ICollection collection ) { - types ??= new(); - types.Add( entitySet.ClrType ); + var count = collection.Count; + + if ( count == 0 ) + { + return default; + } + +#if NETFRAMEWORK + var array = new StructuralTypeConfiguration[count]; + types = new(); +#else + var pool = ArrayPool.Shared; + var array = pool.Rent( count ); + + types = new( capacity: count ); +#endif + + collection.CopyTo( array, 0 ); + + for ( var i = 0; i < count; i++ ) + { + types.Add( array[i].ClrType ); + } + +#if !NETFRAMEWORK + pool.Return( array, clearArray: true ); +#endif + + return types; + } + + using var structuralTypes = builder.StructuralTypes.GetEnumerator(); + + if ( !structuralTypes.MoveNext() ) + { + return default; } - foreach ( var singleton in builder.Singletons ) + types = new HashSet() + { + structuralTypes.Current.ClrType, + }; + + while ( structuralTypes.MoveNext() ) { - types ??= new(); - types.Add( singleton.ClrType ); + types.Add( structuralTypes.Current.ClrType ); } return types; @@ -77,8 +116,7 @@ private void OnModelCreating( ODataModelBuilder builder ) { foreach ( var type in types ) { - var entityType = builder.AddEntityType( type ); - builder.AddEntitySet( entityType.Name, entityType ); + builder.AddComplexType( type ); } } } \ No newline at end of file From 9ae3dd557f5b9cff54b27bc580c75e1f633268ff Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 27 Dec 2022 13:06:44 -0800 Subject: [PATCH 34/35] Update versions and release notes --- .../Asp.Versioning.WebApi.OData.ApiExplorer.csproj | 4 ++-- .../Asp.Versioning.WebApi.OData.csproj | 4 ++-- .../Asp.Versioning.OData.ApiExplorer.csproj | 4 ++-- .../src/Asp.Versioning.OData/Asp.Versioning.OData.csproj | 4 ++-- .../OData/src/Asp.Versioning.OData/ReleaseNotes.txt | 2 +- .../WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj | 4 ++-- .../WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt | 2 +- .../Asp.Versioning.Mvc.ApiExplorer.csproj | 4 ++-- .../src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt | 3 +-- .../WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj | 4 ++-- src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt | 2 +- 11 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj index 2160674c..a3983eab 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.3.0 - 6.3.0.0 + 6.4.0 + 6.4.0.0 net45;net472 Asp.Versioning ASP.NET Web API Versioning API Explorer for OData v4.0 diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj index 81b9d9aa..311c3340 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj @@ -1,8 +1,8 @@  - 6.3.0 - 6.3.0.0 + 6.4.0 + 6.4.0.0 net45;net472 Asp.Versioning API Versioning for ASP.NET Web API with OData v4.0 diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj index 43ec7680..78a1e926 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.3.1 - 6.3.0.0 + 6.4.0 + 6.4.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning API Explorer for OData v4.0 diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj index 9ea5d6db..bf5a891c 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj @@ -1,8 +1,8 @@  - 6.3.1 - 6.3.0.0 + 6.4.0 + 6.4.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning with OData v4.0 diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt index d980a834..5f282702 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt @@ -1 +1 @@ -Added workaround for [OData #753](https://github.com/OData/AspNetCoreOData/issues/753) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj index e94f1972..982d8a77 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj @@ -1,8 +1,8 @@  - 6.3.1 - 6.3.0.0 + 6.4.0 + 6.4.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt index e508e614..5f282702 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ReleaseNotes.txt @@ -1 +1 @@ -[Fixed #922](https://github.com/dotnet/aspnet-api-versioning/discussions/922) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj index ff2d9ae4..e3947a5e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.3.1 - 6.3.0.0 + 6.4.0 + 6.4.0.0 net6.0;netcoreapp3.1 Asp.Versioning.ApiExplorer ASP.NET Core API Versioning API Explorer diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt index 7bc8848b..5f282702 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ReleaseNotes.txt @@ -1,2 +1 @@ -[Fixed #922](https://github.com/dotnet/aspnet-api-versioning/discussions/922) -[Fixed #923](https://github.com/dotnet/aspnet-api-versioning/issues/923) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj index 483d4ea7..8ad4d30d 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj @@ -1,8 +1,8 @@  - 6.3.1 - 6.3.0.0 + 6.4.0 + 6.4.0.0 net6.0;netcoreapp3.1 Asp.Versioning ASP.NET Core API Versioning diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt index e508e614..5f282702 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReleaseNotes.txt @@ -1 +1 @@ -[Fixed #922](https://github.com/dotnet/aspnet-api-versioning/discussions/922) \ No newline at end of file + \ No newline at end of file From 52cb28da785574f8c41f561b22a574eff5ef0a37 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 27 Dec 2022 14:03:02 -0800 Subject: [PATCH 35/35] Fix CodeQL violations --- .../PartialODataDescriptionProvider.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs index c90ab9b7..77825e95 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs @@ -23,7 +23,7 @@ namespace Asp.Versioning.ApiExplorer; [CLSCompliant( false )] public class PartialODataDescriptionProvider : IApiDescriptionProvider { - private static int? beforeOData; + private static readonly int BeforeOData = ODataOrder() + 10; private readonly IOptionsFactory odataOptionsFactory; private readonly IOptions options; private bool markedAdHoc; @@ -42,8 +42,6 @@ public PartialODataDescriptionProvider( { this.odataOptionsFactory = odataOptionsFactory ?? throw new ArgumentNullException( nameof( odataOptionsFactory ) ); this.options = options ?? throw new ArgumentNullException( nameof( options ) ); - beforeOData ??= ODataOrder( odataOptionsFactory, options ) + 10; - Order = beforeOData.Value; } /// @@ -84,7 +82,7 @@ protected ODataApiExplorerOptions Options /// Gets or sets the order precedence of the current API description provider. /// /// The order precedence of the current API description provider. - public int Order { get; protected set; } + public int Order { get; protected set; } = BeforeOData; /// public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context ) @@ -107,9 +105,9 @@ public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context { var model = models[i]; var version = model.GetApiVersion(); - var options = odataOptionsFactory.Create( Opts.DefaultName ); + var odata = odataOptionsFactory.Create( Opts.DefaultName ); - options.AddRouteComponents( model ); + odata.AddRouteComponents( model ); for ( var j = 0; j < results.Count; j++ ) { @@ -149,12 +147,14 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) } [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static int ODataOrder( IOptionsFactory factory, IOptions options ) => + private static int ODataOrder() => new ODataApiDescriptionProvider( new StubModelMetadataProvider(), new StubModelTypeBuilder(), - factory, - options ).Order; + new OptionsFactory( + Enumerable.Empty>(), + Enumerable.Empty>() ), + Opts.Create( new ODataApiExplorerOptions() ) ).Order; [MethodImpl( MethodImplOptions.AggressiveInlining )] private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) =>