From ef0aa0872872569d1e25084a20638b707c3b4fcd Mon Sep 17 00:00:00 2001 From: Xavier Date: Tue, 9 Jan 2024 11:02:24 -0800 Subject: [PATCH 01/41] Move .net core packages to the top. --- README.md | 58 +++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c0284787..d6828747 100644 --- a/README.md +++ b/README.md @@ -23,22 +23,6 @@ versioning in the past or supported API versioning with semantics that are diffe The supported flavors of ASP.NET are: -* **ASP.NET Web API** -
Adds API versioning to your Web API applications
- - [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi) - [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi) - [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/New-Services-Quick-Start#aspnet-web-api) - [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/WebApi) - -* **ASP.NET Web API and OData** -
Adds API versioning to your Web API applications using OData v4.0
- - [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.OData.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData) - [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.OData.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData) - [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/New-Services-Quick-Start#aspnet-web-api-with-odata-v40) - [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/OData) - * **ASP.NET Core**
Adds API versioning to your ASP.NET Core Minimal API applications
@@ -63,23 +47,23 @@ The supported flavors of ASP.NET are: [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/New-Services-Quick-Start#aspnet-core-with-odata-v40) [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNetCore/OData) -This is also the home of the ASP.NET API versioning API explorers that you can use to easily document your REST APIs with OpenAPI: +* **ASP.NET Web API** +
Adds API versioning to your Web API applications
-* **ASP.NET Web API Versioned API Explorer** -
Replaces the default API explorer in your Web API applications
+ [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi) + [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi) + [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/New-Services-Quick-Start#aspnet-web-api) + [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/WebApi) - [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.ApiExplorer.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer) - [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.ApiExplorer.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer) - [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/API-Documentation#aspnet-web-api) - [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/WebApi/OpenApiWebApiSample) +* **ASP.NET Web API and OData** +
Adds API versioning to your Web API applications using OData v4.0
-* **ASP.NET Web API with OData API Explorer** -
Adds an API explorer to your Web API applications using OData v4.0
+ [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.OData.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData) + [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.OData.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData) + [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/New-Services-Quick-Start#aspnet-web-api-with-odata-v40) + [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/OData) - [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.OData.ApiExplorer.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer) - [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.OData.ApiExplorer.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer) - [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/API-Documentation#aspnet-web-api-with-odata) - [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/OData/OpenApiODataWebApiSample) +This is also the home of the ASP.NET API versioning API explorers that you can use to easily document your REST APIs with OpenAPI: * **ASP.NET Core Versioned API Explorer**
Adds additional API explorer support to your ASP.NET Core applications
@@ -97,6 +81,22 @@ This is also the home of the ASP.NET API versioning API explorers that you can u [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/API-Documentation#aspnet-core-with-odata) [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNetCore/OData/OpenApiODataSample) +* **ASP.NET Web API Versioned API Explorer** +
Replaces the default API explorer in your Web API applications
+ + [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.ApiExplorer.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer) + [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.ApiExplorer.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer) + [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/API-Documentation#aspnet-web-api) + [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/WebApi/OpenApiWebApiSample) + +* **ASP.NET Web API with OData API Explorer** +
Adds an API explorer to your Web API applications using OData v4.0
+ + [![NuGet Package](https://img.shields.io/nuget/v/Asp.Versioning.WebApi.OData.ApiExplorer.svg)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer) + [![NuGet Downloads](https://img.shields.io/nuget/dt/Asp.Versioning.WebApi.OData.ApiExplorer.svg?color=green)](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer) + [![Quick Start](https://img.shields.io/badge/quick-start-9B6CD1)](../../wiki/API-Documentation#aspnet-web-api-with-odata) + [![Examples](https://img.shields.io/badge/example-code-2B91AF)](../../tree/main/examples/AspNet/OData/OpenApiODataWebApiSample) + The client-side libraries make it simple to create API version-aware HTTP clients. * **HTTP Client API Versioning Extensions** From 1c136280aab973a70315ae3826873f4de67df6a7 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Wed, 10 Jan 2024 15:44:05 -0800 Subject: [PATCH 02/41] Correct sunset policy resolution when falling back. Fixes #1064 --- .../Asp.Versioning.Abstractions.csproj | 2 +- .../ISunsetPolicyManagerExtensions.cs | 7 ++++--- .../src/Asp.Versioning.Abstractions/ReleaseNotes.txt | 2 +- .../ISunsetPolicyManagerExtensionsTest.cs | 5 ++--- src/Common/src/Common/DefaultApiVersionReporter.cs | 4 +--- 5 files changed, 9 insertions(+), 11 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 00dd4809..6fb59849 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 @@  - 8.0.0 + 8.0.1 8.0.0.0 $(DefaultTargetFramework);netstandard1.0;netstandard2.0 API Versioning Abstractions diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs index 37be6965..f28f9cca 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs @@ -46,7 +46,7 @@ public static bool TryGetPolicy( /// The name of the API. /// The API version to get the policy for. /// The applicable sunset policy, if any. - /// The resolution or is as follows: + /// The resolution order is as follows: /// /// and /// only @@ -76,7 +76,7 @@ public static bool TryGetPolicy( /// The API version to get the policy for. /// /// The applicable sunset policy, if any. /// True if the sunset policy was retrieved; otherwise, false. - /// The resolution or is as follows: + /// The resolution order is as follows: /// /// and /// only @@ -102,7 +102,8 @@ public static bool TryResolvePolicy( return true; } } - else if ( apiVersion != null && policyManager.TryGetPolicy( apiVersion, out sunsetPolicy ) ) + + if ( apiVersion != null && policyManager.TryGetPolicy( apiVersion, out sunsetPolicy ) ) { return true; } diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt b/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt index 5f282702..d6943ffb 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 +TryResolvePolicy should correctly fall back ([#1064](https://github.com/dotnet/aspnet-api-versioning/issues/1064)) \ No newline at end of file diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs index ac1cb2c0..9cc9b693 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs @@ -65,11 +65,10 @@ public void resolve_policy_should_fall_back_to_global_result() var expected = new SunsetPolicy(); var other = new SunsetPolicy(); - manager.Setup( m => m.TryGetPolicy( "Test", new ApiVersion( 1.0, null ), out other ) ).Returns( true ); - manager.Setup( m => m.TryGetPolicy( default, new ApiVersion( 1.0, null ), out expected ) ).Returns( true ); + manager.Setup( m => m.TryGetPolicy( It.IsAny(), new ApiVersion( 1.0, null ), out expected ) ).Returns( true ); // act - var policy = manager.Object.ResolvePolicyOrDefault( default, new ApiVersion( 1.0 ) ); + var policy = manager.Object.ResolvePolicyOrDefault( "Test", new ApiVersion( 1.0 ) ); // assert policy.Should().BeSameAs( expected ); diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs index 95e4d017..72ed1408 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -91,9 +91,7 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) #endif var name = metadata.Name; - if ( sunsetPolicyManager.TryGetPolicy( name, version, out var policy ) || - ( !string.IsNullOrEmpty( name ) && sunsetPolicyManager.TryGetPolicy( name, out policy ) ) || - ( version != null && sunsetPolicyManager.TryGetPolicy( version, out policy ) ) ) + if ( sunsetPolicyManager.TryResolvePolicy( name, version, out var policy ) ) { response.WriteSunsetPolicy( policy ); } From 9d18108b8758dcf190ead2d9f579b7bce921e078 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 12 Jan 2024 15:16:21 -0800 Subject: [PATCH 03/41] Added IEndpointInspector so that controller action endpoints are not processed by min api endpoint collators. Related #1066 --- .../ApiExplorer/DefaultEndpointInspector.cs | 15 ++++++++ ...ointApiVersionMetadataCollationProvider.cs | 19 ++++++++-- .../ApiExplorer/IEndpointInspector.cs | 19 ++++++++++ .../IServiceCollectionExtensions.cs | 1 + .../ApiVersionDescriptionProviderFactory.cs | 5 ++- .../IApiVersioningBuilderExtensions.cs | 36 +++++++++---------- .../ApiExplorer/MvcEndpointInspector.cs | 21 +++++++++++ .../IApiVersioningBuilderExtensions.cs | 18 ++++++++++ 8 files changed, 112 insertions(+), 22 deletions(-) create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IEndpointInspector.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs new file mode 100644 index 00000000..7109d87f --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Http; + +/// +/// Represents the default endpoint inspector. +/// +[CLSCompliant(false)] +public sealed class DefaultEndpointInspector : IEndpointInspector +{ + /// + public bool IsControllerAction( Endpoint endpoint ) => false; +} \ 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 index 45c14725..e5ce20fb 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs @@ -12,15 +12,29 @@ namespace Asp.Versioning.ApiExplorer; public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider { private readonly EndpointDataSource endpointDataSource; + private readonly IEndpointInspector endpointInspector; private int version; /// /// Initializes a new instance of the class. /// /// The underlying endpoint data source. + [Obsolete( "Use the constructor that accepts IEndpointInspector. This constructor will be removed in a future version." )] public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource ) + : this( endpointDataSource, new DefaultEndpointInspector() ) { } + + /// + /// Initializes a new instance of the class. + /// + /// The underlying endpoint data source. + /// The endpoint inspector used to inspect endpoints. + public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource, IEndpointInspector endpointInspector ) { - this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) ); + ArgumentNullException.ThrowIfNull( endpointDataSource ); + ArgumentNullException.ThrowIfNull( endpointInspector ); + + this.endpointDataSource = endpointDataSource; + this.endpointInspector = endpointInspector; ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version ); } @@ -38,7 +52,8 @@ public void Execute( ApiVersionMetadataCollationContext context ) { var endpoint = endpoints[i]; - if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item ) + if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item || + endpointInspector.IsControllerAction( endpoint ) ) { continue; } diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IEndpointInspector.cs new file mode 100644 index 00000000..900edf94 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IEndpointInspector.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Http; + +/// +/// Defines the behavior of an endpoint inspector. +/// +[CLSCompliant( false )] +public interface IEndpointInspector +{ + /// + /// Determines whether the specified endpoint is a controller action. + /// + /// The endpoint to inspect. + /// True if the is for a controller action; otherwise, false. + bool IsControllerAction( Endpoint endpoint ); +} \ 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 07911329..14606936 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -89,6 +89,7 @@ private static void AddApiVersioningServices( IServiceCollection services ) services.TryAddEnumerable( Transient, ApiVersioningRouteOptionsSetup>() ); services.TryAddEnumerable( Singleton() ); services.TryAddEnumerable( Singleton() ); + services.TryAddTransient(); services.Replace( WithLinkGeneratorDecorator( services ) ); TryAddProblemDetailsRfc7231Compliance( services ); TryAddErrorObjectJsonOptions( services ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs index 05b1cba1..75b99644 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs @@ -16,6 +16,7 @@ internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescript { private readonly ISunsetPolicyManager sunsetPolicyManager; private readonly IApiVersionMetadataCollationProvider[] providers; + private readonly IEndpointInspector endpointInspector; private readonly IOptions options; private readonly Activator activator; @@ -23,11 +24,13 @@ public ApiVersionDescriptionProviderFactory( Activator activator, ISunsetPolicyManager sunsetPolicyManager, IEnumerable providers, + IEndpointInspector endpointInspector, IOptions options ) { this.activator = activator; this.sunsetPolicyManager = sunsetPolicyManager; this.providers = providers.ToArray(); + this.endpointInspector = endpointInspector; this.options = options; } @@ -35,7 +38,7 @@ public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSou { var collators = new List( capacity: providers.Length + 1 ) { - new EndpointApiVersionMetadataCollationProvider( endpointDataSource ), + new EndpointApiVersionMetadataCollationProvider( endpointDataSource, endpointInspector ), }; collators.AddRange( providers ); 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 1c1d2e63..29f0fba9 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 @@ -5,11 +5,13 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using static ServiceDescriptor; /// @@ -70,40 +72,36 @@ private static IApiVersionDescriptionProviderFactory ResolveApiVersionDescriptio { var sunsetPolicyManager = serviceProvider.GetRequiredService(); var providers = serviceProvider.GetServices(); + var inspector = serviceProvider.GetRequiredService(); var options = serviceProvider.GetRequiredService>(); - var mightUseCustomGroups = options.Value.FormatGroupName is not null; return new ApiVersionDescriptionProviderFactory( - mightUseCustomGroups ? NewGroupedProvider : NewDefaultProvider, + NewDefaultProvider, sunsetPolicyManager, providers, + inspector, options ); - static IApiVersionDescriptionProvider NewDefaultProvider( + static DefaultApiVersionDescriptionProvider NewDefaultProvider( IEnumerable providers, ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) => - new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions ); - - static IApiVersionDescriptionProvider NewGroupedProvider( - IEnumerable providers, - ISunsetPolicyManager sunsetPolicyManager, - IOptions apiExplorerOptions ) => - new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions ); + new( providers, sunsetPolicyManager, apiExplorerOptions ); } private static IApiVersionDescriptionProvider ResolveApiVersionDescriptionProvider( IServiceProvider serviceProvider ) { - var providers = serviceProvider.GetServices(); - var sunsetPolicyManager = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>(); - var mightUseCustomGroups = options.Value.FormatGroupName is not null; + var factory = serviceProvider.GetRequiredService(); + var endpointDataSource = new EmptyEndpointDataSource(); + return factory.Create( endpointDataSource ); + } + + private sealed class EmptyEndpointDataSource : EndpointDataSource + { + public override IReadOnlyList Endpoints => Array.Empty(); - if ( mightUseCustomGroups ) - { - return new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, options ); - } + public override IChangeToken GetChangeToken() => new CancellationChangeToken( CancellationToken.None ); - return new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, options ); + public override IReadOnlyList GetGroupedEndpoints( RouteGroupContext context ) => Array.Empty(); } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs new file mode 100644 index 00000000..8729791c --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +/// +/// Represents the inspector that understands +/// endpoints defined by MVC controllers. +/// +[CLSCompliant(false)] +public sealed class MvcEndpointInspector : IEndpointInspector +{ + /// + public bool IsControllerAction( Endpoint endpoint ) + { + ArgumentNullException.ThrowIfNull( endpoint ); + return endpoint.Metadata.Any( static attribute => attribute is ControllerAttribute ); + } +} \ No newline at end of file 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 7796ca96..5f895312 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -67,6 +67,7 @@ private static void AddServices( IServiceCollection services ) services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Singleton() ); services.Replace( WithUrlHelperFactoryDecorator( services ) ); + services.TryReplace(); } private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor ) @@ -84,6 +85,23 @@ private static object CreateInstance( this IServiceProvider services, ServiceDes return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType! ); } + private static void TryReplace( this IServiceCollection services ) + { + var serviceType = typeof( TService ); + var implementationType = typeof( TImplementation ); + + for ( var i = services.Count - 1; i >= 0; i-- ) + { + var service = services[i]; + + if ( service.ServiceType == serviceType && service.ImplementationType == implementationType ) + { + services[i] = Describe( serviceType, typeof( TReplacement ), service.Lifetime ); + break; + } + } + } + [SkipLocalsInit] private static DecoratedServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection services ) { From 59ff9e687b4f5cc7bcf1f48f701117b694a50da9 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 12 Jan 2024 15:17:24 -0800 Subject: [PATCH 04/41] Refactor and unify IApiVersionDescriptionProvider implementations. Explicit group names can only be determined at runtime. Related #1066 --- .../DefaultApiVersionDescriptionProvider.cs | 147 +++---------- .../GroupedApiVersionDescriptionProvider.cs | 200 ++---------------- .../ApiVersionDescriptionCollection.cs | 76 +++++++ .../Internal/ApiVersionDescriptionComparer.cs | 28 +++ .../Internal/DescriptionProvider.cs | 107 ++++++++++ .../Internal/GroupedApiVersion.cs | 5 + .../Internal/IGroupedApiVersionMetadata.cs | 20 ++ .../IGroupedApiVersionMetadataFactory.cs | 9 + 8 files changed, 291 insertions(+), 301 deletions(-) create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs 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 63408cad..22151e11 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs @@ -2,9 +2,8 @@ namespace Asp.Versioning.ApiExplorer; +using Asp.Versioning.ApiExplorer.Internal; using Microsoft.Extensions.Options; -using static Asp.Versioning.ApiVersionMapping; -using static System.Globalization.CultureInfo; /// /// Represents the default implementation of an object that discovers and describes the API version information within an application. @@ -12,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer; [CLSCompliant( false )] public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider { - private readonly ApiVersionDescriptionCollection collection; + private readonly ApiVersionDescriptionCollection collection; private readonly IOptions options; /// @@ -28,7 +27,7 @@ public DefaultApiVersionDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); + collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -58,133 +57,53 @@ protected virtual IReadOnlyList Describe( IReadOnlyList( capacity: metadata.Count ); - var supported = new HashSet(); - var deprecated = new HashSet(); - - BucketizeApiVersions( metadata, supported, deprecated ); - AppendDescriptions( descriptions, supported, deprecated: false ); - AppendDescriptions( descriptions, deprecated, deprecated: true ); - - return descriptions.OrderBy( d => d.ApiVersion ).ToArray(); - } - - private void BucketizeApiVersions( IReadOnlyList metadata, HashSet supported, HashSet deprecated ) - { - var declared = new HashSet(); - var advertisedSupported = new HashSet(); - var advertisedDeprecated = new HashSet(); - - for ( var i = 0; i < metadata.Count; i++ ) + // TODO: consider refactoring and removing GroupedApiVersionDescriptionProvider as both implementations are now + // effectively the same. this cast is safe as an internal implementation detail. if this method is + // overridden, then this code doesn't even run + // + // REF: https://github.com/dotnet/aspnet-api-versioning/issues/1066 + if ( metadata is GroupedApiVersionMetadata[] groupedMetadata ) { - var model = metadata[i].Map( Explicit | Implicit ); - var versions = model.DeclaredApiVersions; - - for ( var j = 0; j < versions.Count; j++ ) - { - declared.Add( versions[j] ); - } - - versions = model.SupportedApiVersions; - - for ( var j = 0; j < versions.Count; j++ ) - { - var version = versions[j]; - supported.Add( version ); - advertisedSupported.Add( version ); - } - - versions = model.DeprecatedApiVersions; - - for ( var j = 0; j < versions.Count; j++ ) - { - var version = versions[j]; - deprecated.Add( version ); - advertisedDeprecated.Add( version ); - } + return DescriptionProvider.Describe( groupedMetadata, SunsetPolicyManager, Options ); } - advertisedSupported.ExceptWith( declared ); - advertisedDeprecated.ExceptWith( declared ); - supported.ExceptWith( advertisedSupported ); - deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) ); - - if ( supported.Count == 0 && deprecated.Count == 0 ) - { - supported.Add( Options.DefaultApiVersion ); - } + return Array.Empty(); } - private void AppendDescriptions( List descriptions, IEnumerable versions, bool deprecated ) + private sealed class GroupedApiVersionMetadata : + ApiVersionMetadata, + IEquatable, + IGroupedApiVersionMetadata, + IGroupedApiVersionMetadataFactory { - foreach ( var version in versions ) - { - var groupName = version.ToString( Options.GroupNameFormat, CurrentCulture ); - var sunsetPolicy = SunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default; - descriptions.Add( new( version, groupName, deprecated, sunsetPolicy ) ); - } - } + private GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata ) + : base( metadata ) => GroupName = groupName; - private sealed class ApiVersionDescriptionCollection( - DefaultApiVersionDescriptionProvider provider, - IEnumerable collators ) - { - private readonly object syncRoot = new(); - private readonly DefaultApiVersionDescriptionProvider provider = provider; - private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray(); - private IReadOnlyList? items; - private int version; - - public IReadOnlyList Items - { - get - { - if ( items is not null && version == ComputeVersion() ) - { - return items; - } + public string? GroupName { get; } - lock ( syncRoot ) - { - var currentVersion = ComputeVersion(); + static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New( + string? groupName, + ApiVersionMetadata metadata ) => new( groupName, metadata ); - if ( items is not null && version == currentVersion ) - { - return items; - } + public bool Equals( GroupedApiVersionMetadata? other ) => + other is not null && other.GetHashCode() == GetHashCode(); - var context = new ApiVersionMetadataCollationContext(); + public override bool Equals( object? obj ) => + obj is not null && + GetType().Equals( obj.GetType() ) && + GetHashCode() == obj.GetHashCode(); - for ( var i = 0; i < collators.Length; i++ ) - { - collators[i].Execute( context ); - } - - items = provider.Describe( context.Results ); - version = currentVersion; - } - - return items; - } - } - - private int ComputeVersion() => - collators.Length switch - { - 0 => 0, - 1 => collators[0].Version, - _ => ComputeVersion( collators ), - }; - - private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers ) + public override int GetHashCode() { var hash = default( HashCode ); - for ( var i = 0; i < providers.Length; i++ ) + if ( !string.IsNullOrEmpty( GroupName ) ) { - hash.Add( providers[i].Version ); + hash.Add( GroupName, StringComparer.Ordinal ); } + hash.Add( base.GetHashCode() ); + return hash.ToHashCode(); } } 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 ca31264b..294db52c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs @@ -2,10 +2,8 @@ namespace Asp.Versioning.ApiExplorer; +using Asp.Versioning.ApiExplorer.Internal; using Microsoft.Extensions.Options; -using System.Buffers; -using static Asp.Versioning.ApiVersionMapping; -using static System.Globalization.CultureInfo; /// /// Represents the default implementation of an object that discovers and describes the API version information within an application. @@ -13,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer; [CLSCompliant( false )] public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvider { - private readonly ApiVersionDescriptionCollection collection; + private readonly ApiVersionDescriptionCollection collection; private readonly IOptions options; /// @@ -29,7 +27,7 @@ public GroupedApiVersionDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); + collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -59,191 +57,17 @@ public GroupedApiVersionDescriptionProvider( protected virtual IReadOnlyList Describe( IReadOnlyList metadata ) { ArgumentNullException.ThrowIfNull( metadata ); - - var descriptions = new SortedSet( new ApiVersionDescriptionComparer() ); - var supported = new HashSet(); - var deprecated = new HashSet(); - - BucketizeApiVersions( metadata, supported, deprecated ); - AppendDescriptions( descriptions, supported, deprecated: false ); - AppendDescriptions( descriptions, deprecated, deprecated: true ); - - return descriptions.ToArray(); - } - - private void BucketizeApiVersions( - IReadOnlyList list, - ISet supported, - ISet deprecated ) - { - var declared = new HashSet(); - var advertisedSupported = new HashSet(); - var advertisedDeprecated = new HashSet(); - - for ( var i = 0; i < list.Count; i++ ) - { - var metadata = list[i]; - var groupName = metadata.GroupName; - var model = metadata.Map( Explicit | Implicit ); - var versions = model.DeclaredApiVersions; - - for ( var j = 0; j < versions.Count; j++ ) - { - declared.Add( new( groupName, versions[j] ) ); - } - - versions = model.SupportedApiVersions; - - for ( var j = 0; j < versions.Count; j++ ) - { - var version = versions[j]; - supported.Add( new( groupName, version ) ); - advertisedSupported.Add( new( groupName, version ) ); - } - - versions = model.DeprecatedApiVersions; - - for ( var j = 0; j < versions.Count; j++ ) - { - var version = versions[j]; - deprecated.Add( new( groupName, version ) ); - advertisedDeprecated.Add( new( groupName, version ) ); - } - } - - advertisedSupported.ExceptWith( declared ); - advertisedDeprecated.ExceptWith( declared ); - supported.ExceptWith( advertisedSupported ); - deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) ); - - if ( supported.Count == 0 && deprecated.Count == 0 ) - { - supported.Add( new( default, Options.DefaultApiVersion ) ); - } - } - - private void AppendDescriptions( - ICollection descriptions, - IEnumerable versions, - bool deprecated ) - { - var format = Options.GroupNameFormat; - var formatGroupName = Options.FormatGroupName; - - foreach ( var (groupName, version) in versions ) - { - var formattedVersion = version.ToString( format, CurrentCulture ); - var formattedGroupName = - string.IsNullOrEmpty( groupName ) || formatGroupName is null - ? formattedVersion - : formatGroupName( groupName, formattedVersion ); - - var sunsetPolicy = SunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default; - descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy ) ); - } - } - - private sealed class ApiVersionDescriptionCollection( - GroupedApiVersionDescriptionProvider provider, - IEnumerable collators ) - { - private readonly object syncRoot = new(); - private readonly GroupedApiVersionDescriptionProvider provider = provider; - private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray(); - private IReadOnlyList? items; - private int version; - - 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 < collators.Length; i++ ) - { - collators[i].Execute( context ); - } - - var results = context.Results; - var metadata = new GroupedApiVersionMetadata[results.Count]; - - for ( var i = 0; i < metadata.Length; i++ ) - { - metadata[i] = new( context.Results.GroupName( i ), results[i] ); - } - - items = provider.Describe( metadata ); - version = currentVersion; - } - - return items; - } - } - - private int ComputeVersion() => - collators.Length switch - { - 0 => 0, - 1 => collators[0].Version, - _ => ComputeVersion( collators ), - }; - - 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(); - } - } - - private sealed class ApiVersionDescriptionComparer : IComparer - { - public int Compare( ApiVersionDescription? x, ApiVersionDescription? y ) - { - if ( x is null ) - { - return y is null ? 0 : -1; - } - - if ( y is null ) - { - return 1; - } - - var result = x.ApiVersion.CompareTo( y.ApiVersion ); - - if ( result == 0 ) - { - result = StringComparer.Ordinal.Compare( x.GroupName, y.GroupName ); - } - - return result; - } + return DescriptionProvider.Describe( metadata, SunsetPolicyManager, Options ); } /// /// Represents the API version metadata applied to an endpoint with an optional group name. /// - protected class GroupedApiVersionMetadata : ApiVersionMetadata, IEquatable + protected class GroupedApiVersionMetadata : + ApiVersionMetadata, + IEquatable, + IGroupedApiVersionMetadata, + IGroupedApiVersionMetadataFactory { /// /// Initializes a new instance of the class. @@ -259,6 +83,10 @@ public GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata /// The associated group name, if any. public string? GroupName { get; } + static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New( + string? groupName, + ApiVersionMetadata metadata ) => new( groupName, metadata ); + /// public bool Equals( GroupedApiVersionMetadata? other ) => other is not null && other.GetHashCode() == GetHashCode(); @@ -284,6 +112,4 @@ public override int GetHashCode() return hash.ToHashCode(); } } - - private record struct GroupedApiVersion( string? GroupName, ApiVersion ApiVersion ); } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs new file mode 100644 index 00000000..f5847dd0 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer.Internal; + +internal sealed class ApiVersionDescriptionCollection( + Func, IReadOnlyList> describe, + IEnumerable collators ) + where T : IGroupedApiVersionMetadata, IGroupedApiVersionMetadataFactory +{ + private readonly object syncRoot = new(); + private readonly Func, IReadOnlyList> describe = describe; + private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray(); + private IReadOnlyList? items; + private int version; + + 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 < collators.Length; i++ ) + { + collators[i].Execute( context ); + } + + var results = context.Results; + var metadata = new T[results.Count]; + + for ( var i = 0; i < metadata.Length; i++ ) + { + metadata[i] = T.New( context.Results.GroupName( i ), results[i] ); + } + + items = describe( metadata ); + version = currentVersion; + } + + return items; + } + } + + private int ComputeVersion() => + collators.Length switch + { + 0 => 0, + 1 => collators[0].Version, + _ => ComputeVersion( collators ), + }; + + 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/Internal/ApiVersionDescriptionComparer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs new file mode 100644 index 00000000..3fb73385 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer.Internal; + +internal sealed class ApiVersionDescriptionComparer : IComparer +{ + public int Compare( ApiVersionDescription? x, ApiVersionDescription? y ) + { + if ( x is null ) + { + return y is null ? 0 : -1; + } + + if ( y is null ) + { + return 1; + } + + var result = x.ApiVersion.CompareTo( y.ApiVersion ); + + if ( result == 0 ) + { + result = StringComparer.Ordinal.Compare( x.GroupName, y.GroupName ); + } + + return result; + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs new file mode 100644 index 00000000..ce3a0dbd --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer.Internal; + +using static Asp.Versioning.ApiVersionMapping; +using static System.Globalization.CultureInfo; + +internal static class DescriptionProvider +{ + internal static ApiVersionDescription[] Describe( + IReadOnlyList metadata, + ISunsetPolicyManager sunsetPolicyManager, + ApiExplorerOptions options ) + where T : IGroupedApiVersionMetadata, IEquatable + { + var descriptions = new SortedSet( new ApiVersionDescriptionComparer() ); + var supported = new HashSet(); + var deprecated = new HashSet(); + + BucketizeApiVersions( metadata, supported, deprecated, options ); + AppendDescriptions( descriptions, supported, sunsetPolicyManager, options, deprecated: false ); + AppendDescriptions( descriptions, deprecated, sunsetPolicyManager, options, deprecated: true ); + + return [.. descriptions]; + } + + private static void BucketizeApiVersions( + IReadOnlyList list, + HashSet supported, + HashSet deprecated, + ApiExplorerOptions options ) + where T : IGroupedApiVersionMetadata + { + var declared = new HashSet(); + var advertisedSupported = new HashSet(); + var advertisedDeprecated = new HashSet(); + + for ( var i = 0; i < list.Count; i++ ) + { + var metadata = list[i]; + var groupName = metadata.GroupName; + var model = metadata.Map( Explicit | Implicit ); + var versions = model.DeclaredApiVersions; + + for ( var j = 0; j < versions.Count; j++ ) + { + declared.Add( new( groupName, versions[j] ) ); + } + + versions = model.SupportedApiVersions; + + for ( var j = 0; j < versions.Count; j++ ) + { + var version = versions[j]; + supported.Add( new( groupName, version ) ); + advertisedSupported.Add( new( groupName, version ) ); + } + + versions = model.DeprecatedApiVersions; + + for ( var j = 0; j < versions.Count; j++ ) + { + var version = versions[j]; + deprecated.Add( new( groupName, version ) ); + advertisedDeprecated.Add( new( groupName, version ) ); + } + } + + advertisedSupported.ExceptWith( declared ); + advertisedDeprecated.ExceptWith( declared ); + supported.ExceptWith( advertisedSupported ); + deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) ); + + if ( supported.Count == 0 && deprecated.Count == 0 ) + { + supported.Add( new( default, options.DefaultApiVersion ) ); + } + } + + private static void AppendDescriptions( + SortedSet descriptions, + HashSet versions, + ISunsetPolicyManager sunsetPolicyManager, + ApiExplorerOptions options, + bool deprecated ) + { + var format = options.GroupNameFormat; + var formatGroupName = options.FormatGroupName; + + foreach ( var (groupName, version) in versions ) + { + var formattedGroupName = groupName; + + if ( string.IsNullOrEmpty( formattedGroupName ) ) + { + formattedGroupName = version.ToString( format, CurrentCulture ); + } + else if ( formatGroupName is not null ) + { + formattedGroupName = formatGroupName( formattedGroupName, version.ToString( format, CurrentCulture ) ); + } + + var sunsetPolicy = sunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default; + descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy ) ); + } + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs new file mode 100644 index 00000000..8d276e60 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs @@ -0,0 +1,5 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer.Internal; + +internal record struct GroupedApiVersion( string? GroupName, ApiVersion ApiVersion ); \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs new file mode 100644 index 00000000..ec0c13e3 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer.Internal; + +internal interface IGroupedApiVersionMetadata +{ + string? GroupName { get; } + + string Name { get; } + + bool IsApiVersionNeutral { get; } + + ApiVersionModel Map( ApiVersionMapping mapping ); + + ApiVersionMapping MappingTo( ApiVersion? apiVersion ); + + bool IsMappedTo( ApiVersion? apiVersion ); + + void Deconstruct( out ApiVersionModel apiModel, out ApiVersionModel endpointModel ); +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs new file mode 100644 index 00000000..ac9d885f --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer.Internal; + +internal interface IGroupedApiVersionMetadataFactory + where T : IGroupedApiVersionMetadata +{ + static abstract T New( string? groupName, ApiVersionMetadata metadata ); +} \ No newline at end of file From 8ac43c59ad6aaadeb53d7d1f910d767e8a792bff Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 22 Mar 2024 10:09:45 -0700 Subject: [PATCH 05/41] Parse versions single header CSVs. Fixes #1070 --- .../ApiVersionEnumerator.cs | 73 ++++++++--- .../ApiVersionEnumeratorTest.cs | 116 ++++++++++++++++++ 2 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs index 49139052..6f940aee 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs @@ -5,14 +5,20 @@ namespace Asp.Versioning.Http; +#if NET +using System.Buffers; +#endif using System.Collections; +#if NET +using static System.StringSplitOptions; +#endif /// /// Represents an enumerator of API versions from a HTTP header. /// public readonly struct ApiVersionEnumerator : IEnumerable { - private readonly IEnumerable values; + private readonly string[] values; private readonly IApiVersionParser parser; /// @@ -29,37 +35,72 @@ public ApiVersionEnumerator( ArgumentNullException.ThrowIfNull( response ); ArgumentException.ThrowIfNullOrEmpty( headerName ); - this.values = - response.Headers.TryGetValues( headerName, out var values ) - ? values - : Enumerable.Empty(); - + this.values = response.Headers.TryGetValues( headerName, out var values ) ? values.ToArray() : []; this.parser = parser ?? ApiVersionParser.Default; } /// public IEnumerator GetEnumerator() { - using var iterator = values.GetEnumerator(); +#if NETSTANDARD + for ( var i = 0; i < values.Length; i++ ) + { + var items = values[i].Split( ',' ); - if ( !iterator.MoveNext() ) + for ( var j = 0; j < items.Length; j++ ) + { + var item = items[j].Trim(); + + if ( item.Length > 0 && parser.TryParse( item, out var result ) ) + { + yield return result!; + } + } + } +#else + for ( var i = 0; i < values.Length; i++ ) { - yield break; + var (count, versions) = ParseVersions( values[i] ); + + for ( var j = 0; j < count; j++ ) + { + yield return versions[j]; + } } +#endif + } - if ( parser.TryParse( iterator.Current, out var value ) ) + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +#if NET + private (int Count, ApiVersion[] Results) ParseVersions( ReadOnlySpan value ) + { + var pool = ArrayPool.Shared; + var ranges = pool.Rent( 5 ); + var length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries ); + + while ( length >= ranges.Length ) { - yield return value!; + pool.Return( ranges ); + length <<= 1; + ranges = pool.Rent( length ); + length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries ); } - while ( iterator.MoveNext() ) + var results = new ApiVersion[length]; + var count = 0; + + for ( var i = 0; i < length; i++ ) { - if ( parser.TryParse( iterator.Current, out value ) ) + var text = value[ranges[i]]; + + if ( text.Length > 0 && parser.TryParse( text, out var result ) ) { - yield return value!; + results[count++] = result; } } - } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + pool.Return( ranges ); + return (count, results); + } +#endif } \ No newline at end of file diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs new file mode 100644 index 00000000..864a8d92 --- /dev/null +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Http; + +public class ApiVersionEnumeratorTest +{ + [Fact] + public void enumerator_should_process_single_header_value() + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", "1.0" ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( [new ApiVersion( 1.0 )] ); + } + + [Fact] + public void enumerator_should_process_multiple_header_values() + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", ["1.0", "2.0"] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( new ApiVersion[] { new( 1.0 ), new( 2.0 ) } ); + } + + [Theory] + [InlineData( "1.0,2.0" )] + [InlineData( "1.0, 2.0" )] + [InlineData( "1.0,,2.0" )] + [InlineData( "1.0, abc, 2.0" )] + public void enumerator_should_process_single_header_comma_separated_values( string value ) + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", [value] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( new ApiVersion[] { new( 1.0 ), new( 2.0 ) } ); + } + + [Fact] + public void enumerator_should_process_many_header_comma_separated_values() + { + // arrange + const string Value = "1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0"; + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", [Value] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( + new ApiVersion[] + { + new( 1.0 ), + new( 2.0 ), + new( 3.0 ), + new( 4.0 ), + new( 5.0 ), + new( 6.0 ), + new( 7.0 ), + new( 8.0 ), + new( 9.0 ), + new( 10.0 ), + } ); + } + + [Fact] + public void enumerator_should_process_multiple_header_comma_separated_values() + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", ["1.0, 2.0", "3.0, 4.0"] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( + new ApiVersion[] + { + new( 1.0 ), + new( 2.0 ), + new( 3.0 ), + new( 4.0 ), + } ); + } +} \ No newline at end of file From 0b50d4541881437df5a1a7011d44121d0ad6fb1c Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 22 Mar 2024 10:10:05 -0700 Subject: [PATCH 06/41] Bump version and add release notes --- .../Asp.Versioning.Http.Client.csproj | 2 +- src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj index 75416005..e508c993 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj +++ b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj @@ -1,7 +1,7 @@  - 8.0.0 + 8.0.1 8.0.0.0 $(DefaultTargetFramework);netstandard1.1;netstandard2.0 Asp.Versioning.Http diff --git a/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt b/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt index 5f282702..0cf60623 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt +++ b/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Parse single header CSVs ([#1070](https://github.com/dotnet/aspnet-api-versioning/issues/1070)) \ No newline at end of file From f9aa66da1b9a479fe79a8a3de7fcfc031b6f5b1f Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 24 Mar 2024 10:57:36 -0700 Subject: [PATCH 07/41] Expose JsonSerializerContext for extenders. Related to #1072 --- .../Asp.Versioning.Http/ErrorObjectWriter.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs index 82460917..b02d1260 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +// Ignore Spelling: Serializer namespace Asp.Versioning; using Microsoft.AspNetCore.Http; @@ -30,6 +31,19 @@ public partial class ErrorObjectWriter : IProblemDetailsWriter public ErrorObjectWriter( IOptions options ) => this.options = ( options ?? throw new ArgumentNullException( nameof( options ) ) ).Value.SerializerOptions; + /// + /// Gets the associated, default . + /// + /// The associated, default . + public static JsonSerializerContext DefaultJsonSerializerContext => ErrorObjectJsonContext.Default; + + /// + /// Creates and returns a new associated with the writer. + /// + /// The JSON serializer options to use. + /// A new . + public static JsonSerializerContext NewJsonSerializerContext( JsonSerializerOptions options ) => new ErrorObjectJsonContext( options ); + /// public virtual bool CanWrite( ProblemDetailsContext context ) { @@ -89,6 +103,7 @@ internal ErrorObject( ProblemDetails problemDetails ) => /// protected internal readonly partial struct ErrorDetail { + private const string CodeProperty = "code"; private readonly ProblemDetails problemDetails; private readonly InnerError? innerError; private readonly Dictionary extensions = []; @@ -103,23 +118,21 @@ internal ErrorDetail( ProblemDetails problemDetails ) /// Gets or sets one of a server-defined set of error codes. /// /// A server-defined error code. - [JsonPropertyName( "code" )] + [JsonPropertyName( CodeProperty )] [JsonIgnore( Condition = WhenWritingNull )] public string? Code { - get => problemDetails.Extensions.TryGetValue( "code", out var value ) && - value is string code ? - code : - default; + get => problemDetails.Extensions.TryGetValue( CodeProperty, out var value ) && + value is string code ? code : default; set { if ( value is null ) { - problemDetails.Extensions.Remove( "code" ); + problemDetails.Extensions.Remove( CodeProperty ); } else { - problemDetails.Extensions["code"] = value; + problemDetails.Extensions[CodeProperty] = value; } } } From 06d3d628ab5c57b6038a23b1ca5492bf6c7c6e06 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 24 Mar 2024 10:58:10 -0700 Subject: [PATCH 08/41] Simplify the setup of Error Objects. Related to #1072 --- .../IServiceCollectionExtensions.cs | 96 +++++++++++++++++-- .../ErrorObjectJsonOptionsSetup.cs | 24 ----- 2 files changed, 90 insertions(+), 30 deletions(-) delete mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs 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 14606936..367c908c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -11,7 +11,9 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using System; +using System.Diagnostics.CodeAnalysis; using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; /// /// Provides extension methods for the interface. @@ -75,6 +77,65 @@ public static IApiVersioningBuilder EnableApiVersionBinding( this IApiVersioning return builder; } + /// + /// Adds error object support in problem details. + /// + /// The services available in the application. + /// The JSON options setup to perform, if any. + /// The original . + /// + /// + /// This method is only intended to provide backward compatibility with previous library versions by converting + /// into Error Objects that conform to the + /// Error Responses + /// in the Microsoft REST API Guidelines and + /// OData Error Responses. + /// + /// + /// This method should be called before . + /// + /// + public static IServiceCollection AddErrorObjects( this IServiceCollection services, Action? setup = default ) => + AddErrorObjects( services, setup ); + + /// + /// Adds error object support in problem details. + /// + /// The type of . + /// The services available in the application. + /// The JSON options setup to perform, if any. + /// The original . + /// + /// + /// This method is only intended to provide backward compatibility with previous library versions by converting + /// into Error Objects that conform to the + /// Error Responses + /// in the Microsoft REST API Guidelines and + /// OData Error Responses. + /// + /// + /// This method should be called before . + /// + /// + public static IServiceCollection AddErrorObjects<[DynamicallyAccessedMembers( PublicConstructors )] TWriter>( + this IServiceCollection services, + Action? setup = default ) + where TWriter : ErrorObjectWriter + { + ArgumentNullException.ThrowIfNull( services ); + + services.TryAddEnumerable( Singleton() ); + services.Configure( setup ?? DefaultErrorObjectJsonConfig ); + + // TODO: remove with TryAddErrorObjectJsonOptions in 9.0+ + services.AddTransient(); + + return services; + } + + private static void DefaultErrorObjectJsonConfig( JsonOptions options ) => + options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default ); + private static void AddApiVersioningServices( IServiceCollection services ) { ArgumentNullException.ThrowIfNull( services ); @@ -180,23 +241,46 @@ static Rfc7231ProblemDetailsWriter NewProblemDetailsWriter( IServiceProvider ser new( (IProblemDetailsWriter) serviceProvider.GetRequiredService( decoratedType ) ); } + // TODO: retain for 8.1.x back-compat, but remove in 9.0+ in favor of AddErrorObjects for perf private static void TryAddErrorObjectJsonOptions( IServiceCollection services ) { var serviceType = typeof( IProblemDetailsWriter ); var implementationType = typeof( ErrorObjectWriter ); + var markerType = typeof( ErrorObjectsAdded ); + var hasErrorObjects = false; + var hasErrorObjectsJsonConfig = false; for ( var i = 0; i < services.Count; i++ ) { var service = services[i]; - // inheritance is intentionally not considered here because it will require a user-defined - // JsonSerlizerContext and IConfigureOptions - if ( service.ServiceType == serviceType && - service.ImplementationType == implementationType ) + if ( !hasErrorObjects && + service.ServiceType == serviceType && + implementationType.IsAssignableFrom( service.ImplementationType ) ) { - services.TryAddEnumerable( Singleton, ErrorObjectJsonOptionsSetup>() ); - return; + hasErrorObjects = true; + + if ( hasErrorObjectsJsonConfig ) + { + break; + } } + else if ( service.ServiceType == markerType ) + { + hasErrorObjectsJsonConfig = true; + + if ( hasErrorObjects ) + { + break; + } + } + } + + if ( hasErrorObjects && !hasErrorObjectsJsonConfig ) + { + services.Configure( DefaultErrorObjectJsonConfig ); } } + + private sealed class ErrorObjectsAdded { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs deleted file mode 100644 index 664c4840..00000000 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -#pragma warning disable CA1812 // Avoid uninstantiated internal classes - -namespace Asp.Versioning; - -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; - -/// -/// Adds the ErrorObjectJsonContext to the current JsonSerializerOptions. -/// -/// This allows for consistent serialization behavior for ErrorObject regardless if the -/// default reflection-based serializer is used and makes it trim/NativeAOT compatible. -/// -internal sealed class ErrorObjectJsonOptionsSetup : IConfigureOptions -{ - // Always insert the ErrorObjectJsonContext to the beginning of the chain at the time this Configure - // is invoked. This JsonTypeInfoResolver will be before the default reflection-based resolver, and - // before any other resolvers currently added. If apps need to customize serialization, they can - // prepend a custom ErrorObject resolver to the chain in an IConfigureOptions registered. - public void Configure( JsonOptions options ) => - options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default ); -} \ No newline at end of file From b0c34031a330d50fe095720c350956ecd696bdf9 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 24 Mar 2024 11:45:29 -0700 Subject: [PATCH 09/41] Fix code analysis --- .../DependencyInjection/IServiceCollectionExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) 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 367c908c..46ecac55 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -282,5 +282,7 @@ private static void TryAddErrorObjectJsonOptions( IServiceCollection services ) } } +// TEMP: this is a marker class to test whether Error Objects have been explicitly added. remove in 9.0+ +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class ErrorObjectsAdded { } } \ No newline at end of file From 7aad49f5246d32f4e977c8c424fb3a822e366e8f Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 09:32:11 -0700 Subject: [PATCH 10/41] Improve string formatting --- .../ApiDescriptionExtensions.cs | 9 +++++++-- .../ApiVersionParameterDescriptionContext.cs | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs index a0cf3835..30aaf0cd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs @@ -106,9 +106,14 @@ public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDe return false; } - var token = '{' + parameter.Name + '}'; + Span token = stackalloc char[parameter.Name.Length + 2]; + + token[0] = '{'; + token[^1] = '}'; + parameter.Name.AsSpan().CopyTo( token.Slice( 1, parameter.Name.Length ) ); + var value = apiVersion.ToString( options.SubstitutionFormat, CultureInfo.InvariantCulture ); - var newRelativePath = relativePath.Replace( token, value, StringComparison.Ordinal ); + var newRelativePath = relativePath.Replace( token.ToString(), value, StringComparison.Ordinal ); if ( relativePath == newRelativePath ) { 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 136ad268..da70b954 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs @@ -9,6 +9,7 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; +using System.Runtime.CompilerServices; using static Asp.Versioning.ApiVersionParameterLocation; using static System.Linq.Enumerable; using static System.StringComparison; @@ -304,7 +305,7 @@ routeInfo.Constraints is IEnumerable constraints && continue; } - var token = $"{parameter.Name}:{constraintName}"; + var token = FormatToken( parameter.Name, constraintName ); parameterDescription.Name = parameter.Name; description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal ); @@ -375,7 +376,7 @@ routeInfo.Constraints is IEnumerable constraints && }, Source = BindingSource.Path, }; - var token = $"{parameter.Name}:{constraintName}"; + var token = FormatToken( parameter.Name!, constraintName! ); description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal ); description.ParameterDescriptions.Insert( 0, result ); @@ -457,4 +458,18 @@ private static bool FirstParameterIsOptional( return apiVersion == defaultApiVersion; } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static string FormatToken( ReadOnlySpan parameterName, ReadOnlySpan constraintName ) + { + var left = parameterName.Length; + var right = constraintName.Length; + Span token = stackalloc char[left + right + 1]; + + parameterName.CopyTo( token[..left] ); + token[left] = ':'; + constraintName.CopyTo( token.Slice( left + 1, right ) ); + + return token.ToString(); + } } \ No newline at end of file From be7021cf87385fa7dae11eba79e05a11026c2042 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 09:32:46 -0700 Subject: [PATCH 11/41] Use collection initializers --- .../ApiExplorer/PartialODataDescriptionProvider.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 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 fc1ff1b6..47eac901 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 @@ -145,14 +145,10 @@ private static int ODataOrder() => new ODataApiDescriptionProvider( new StubModelMetadataProvider(), new StubModelTypeBuilder(), - new OptionsFactory( - Enumerable.Empty>(), - Enumerable.Empty>() ), + new OptionsFactory( [], [] ), Opts.Create( new ODataApiExplorerOptions( - new( - new StubODataApiVersionCollectionProvider(), - Enumerable.Empty() ) ) ) ).Order; + new( new StubODataApiVersionCollectionProvider(), [] ) ) ) ).Order; [MethodImpl( MethodImplOptions.AggressiveInlining )] private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) => From 42607c5513ba11381d79a4313eb3e0eb8f8fc5f4 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 09:34:37 -0700 Subject: [PATCH 12/41] Support collection parameters in functions. Fixes #999 --- .../ODataApiDescriptionProvider.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) 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 5736e8f4..fdb4ff8d 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 @@ -142,6 +142,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) else { UpdateModelTypes( result, matched ); + UpdateFunctionCollectionParameters( result, matched ); } } @@ -456,6 +457,75 @@ private void UpdateModelTypes( ApiDescription description, IODataRoutingMetadata } } + private static void UpdateFunctionCollectionParameters( ApiDescription description, IODataRoutingMetadata metadata ) + { + var parameters = description.ParameterDescriptions; + + if ( parameters.Count == 0 ) + { + return; + } + + var function = default( IEdmFunction ); + var mapping = default( IDictionary ); + + for ( var i = 0; i < metadata.Template.Count; i++ ) + { + var segment = metadata.Template[i]; + + if ( segment is FunctionSegmentTemplate func ) + { + function = func.Function; + mapping = func.ParameterMappings; + break; + } + else if ( segment is FunctionImportSegmentTemplate import ) + { + function = import.FunctionImport.Function; + mapping = import.ParameterMappings; + break; + } + } + + if ( function is null || mapping is null ) + { + return; + } + + var name = default( string ); + + foreach ( var parameter in function.Parameters ) + { + if ( parameter.Type.IsCollection() && + mapping.TryGetValue( parameter.Name, out name ) && + parameters.SingleOrDefault( p => p.Name == name ) is { } param ) + { + param.Source = BindingSource.Path; + break; + } + } + + var path = description.RelativePath; + + if ( string.IsNullOrEmpty( name ) || string.IsNullOrEmpty( path ) ) + { + return; + } + + var span = name.AsSpan(); + Span oldValue = stackalloc char[name.Length + 2]; + Span newValue = stackalloc char[name.Length + 4]; + + newValue[1] = oldValue[0] = '{'; + newValue[^2] = oldValue[^1] = '}'; + newValue[0] = '['; + newValue[^1] = ']'; + span.CopyTo( oldValue.Slice( 1, name.Length ) ); + span.CopyTo( newValue.Slice( 2, name.Length ) ); + + description.RelativePath = path.Replace( oldValue.ToString(), newValue.ToString(), Ordinal ); + } + private sealed class ApiDescriptionComparer : IEqualityComparer { private readonly IEqualityComparer comparer = StringComparer.OrdinalIgnoreCase; From ea90819930becbe48b40ebee977667f002e44ef1 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 18:37:59 -0700 Subject: [PATCH 13/41] Fix spacing --- .../src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs index 8729791c..28bed7bf 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/MvcEndpointInspector.cs @@ -9,7 +9,7 @@ namespace Asp.Versioning.ApiExplorer; /// Represents the inspector that understands /// endpoints defined by MVC controllers. /// -[CLSCompliant(false)] +[CLSCompliant( false )] public sealed class MvcEndpointInspector : IEndpointInspector { /// From cff65fcecc588176c9f4ebb423485f198b401eb1 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 18:39:09 -0700 Subject: [PATCH 14/41] Use static lambdas --- .../DependencyInjection/IServiceCollectionExtensions.cs | 9 ++++----- .../IApiVersioningBuilderExtensions.cs | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) 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 46ecac55..83521614 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -10,9 +10,8 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using System; using System.Diagnostics.CodeAnalysis; -using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; +using static ServiceDescriptor; using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; /// @@ -141,9 +140,9 @@ private static void AddApiVersioningServices( IServiceCollection services ) ArgumentNullException.ThrowIfNull( services ); services.TryAddSingleton(); - services.AddSingleton( sp => sp.GetRequiredService>().Value.ApiVersionReader ); - services.AddSingleton( sp => (IApiVersionParameterSource) sp.GetRequiredService>().Value.ApiVersionReader ); - services.AddSingleton( sp => sp.GetRequiredService>().Value.ApiVersionSelector ); + services.AddSingleton( static sp => sp.GetRequiredService>().Value.ApiVersionReader ); + services.AddSingleton( static sp => (IApiVersionParameterSource) sp.GetRequiredService>().Value.ApiVersionReader ); + services.AddSingleton( static sp => sp.GetRequiredService>().Value.ApiVersionSelector ); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddEnumerable( Transient, ValidateApiVersioningOptions>() ); 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 5f895312..eef6b507 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using System.Runtime.CompilerServices; -using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; +using static ServiceDescriptor; /// /// Provides ASP.NET Core MVC specific extension methods for . @@ -57,9 +57,9 @@ private static void AddServices( IServiceCollection services ) services.AddMvcCore(); services.TryAddSingleton, MvcApiVersioningOptionsFactory>(); services.TryAddSingleton(); - services.TryAddSingleton( sp => new ApiVersionConventionBuilder( sp.GetRequiredService() ) ); + services.TryAddSingleton( static sp => new ApiVersionConventionBuilder( sp.GetRequiredService() ) ); services.TryAddSingleton(); - services.TryAddSingleton( sp => new ReportApiVersionsAttribute( sp.GetRequiredService() ) ); + services.TryAddSingleton( static sp => new ReportApiVersionsAttribute( sp.GetRequiredService() ) ); services.AddSingleton(); services.TryAddEnumerable( Transient, ApiVersioningMvcOptionsSetup>() ); services.TryAddEnumerable( Transient() ); From 51033a9ba82a5140e47aabbbc3a49b452f01b726 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 18:40:29 -0700 Subject: [PATCH 15/41] Add parameterless extension method for creating IApiVersionDescriptionProvider --- ...ionDescriptionProviderFactoryExtensions.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionDescriptionProviderFactoryExtensions.cs diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionDescriptionProviderFactoryExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionDescriptionProviderFactoryExtensions.cs new file mode 100644 index 00000000..4a20e9c1 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionDescriptionProviderFactoryExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; + +/// +/// Provides extension methods for . +/// +[CLSCompliant( false )] +public static class IApiVersionDescriptionProviderFactoryExtensions +{ + /// + /// Creates and returns an API version description provider. + /// + /// The extended . + /// A new API version description provider. + public static IApiVersionDescriptionProvider Create( this IApiVersionDescriptionProviderFactory factory ) + { + ArgumentNullException.ThrowIfNull( factory ); + return factory.Create( new EmptyEndpointDataSource() ); + } + + private sealed class EmptyEndpointDataSource : EndpointDataSource + { + public override IReadOnlyList Endpoints { get; } = []; + + public override IChangeToken GetChangeToken() => new CancellationChangeToken( CancellationToken.None ); + + public override IReadOnlyList GetGroupedEndpoints( RouteGroupContext context ) => Endpoints; + } +} \ No newline at end of file From b71cdc437a02239eec3f6aa4457eb66f873700bc Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 18:41:45 -0700 Subject: [PATCH 16/41] Refactor DI so that IApiVersionDescriptionProviderFactory creates IApiVersionDescriptionProvider in all paths and can be the single source of replacement --- .../ApiVersionDescriptionProviderFactory.cs | 16 +++---- .../IApiVersioningBuilderExtensions.cs | 45 ++----------------- 2 files changed, 8 insertions(+), 53 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs index 75b99644..cec78062 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs @@ -1,16 +1,13 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable SA1135 // Using directives should be qualified -#pragma warning disable SA1200 // Using directives should be placed correctly - -using Asp.Versioning; -using Asp.Versioning.ApiExplorer; -using Microsoft.Extensions.Options; +#pragma warning disable CA1812 // Avoid uninstantiated internal classes namespace Microsoft.AspNetCore.Builder; +using Asp.Versioning; +using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Routing; -using Activator = Func, ISunsetPolicyManager, IOptions, IApiVersionDescriptionProvider>; +using Microsoft.Extensions.Options; internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescriptionProviderFactory { @@ -18,16 +15,13 @@ internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescript private readonly IApiVersionMetadataCollationProvider[] providers; private readonly IEndpointInspector endpointInspector; private readonly IOptions options; - private readonly Activator activator; public ApiVersionDescriptionProviderFactory( - Activator activator, ISunsetPolicyManager sunsetPolicyManager, IEnumerable providers, IEndpointInspector endpointInspector, IOptions options ) { - this.activator = activator; this.sunsetPolicyManager = sunsetPolicyManager; this.providers = providers.ToArray(); this.endpointInspector = endpointInspector; @@ -43,6 +37,6 @@ public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSou collators.AddRange( providers ); - return activator( collators, sunsetPolicyManager, options ); + return new DefaultApiVersionDescriptionProvider( collators, sunsetPolicyManager, options ); } } \ No newline at end of file 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 29f0fba9..c1a689fe 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 @@ -5,13 +5,11 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using static ServiceDescriptor; /// @@ -54,54 +52,17 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) services.AddMvcCore().AddApiExplorer(); services.TryAddSingleton, ApiExplorerOptionsFactory>(); - services.TryAddTransient( ResolveApiVersionDescriptionProviderFactory ); - services.TryAddSingleton( ResolveApiVersionDescriptionProvider ); + services.TryAddTransient(); + services.TryAddSingleton( static sp => sp.GetRequiredService().Create() ); // use internal constructor until ASP.NET Core fixes their bug // BUG: https://github.com/dotnet/aspnetcore/issues/41773 services.TryAddEnumerable( Transient( - sp => new( + static sp => new( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>() ) ) ); } - - private static IApiVersionDescriptionProviderFactory ResolveApiVersionDescriptionProviderFactory( IServiceProvider serviceProvider ) - { - var sunsetPolicyManager = serviceProvider.GetRequiredService(); - var providers = serviceProvider.GetServices(); - var inspector = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>(); - - return new ApiVersionDescriptionProviderFactory( - NewDefaultProvider, - sunsetPolicyManager, - providers, - inspector, - options ); - - static DefaultApiVersionDescriptionProvider NewDefaultProvider( - IEnumerable providers, - ISunsetPolicyManager sunsetPolicyManager, - IOptions apiExplorerOptions ) => - new( providers, sunsetPolicyManager, apiExplorerOptions ); - } - - private static IApiVersionDescriptionProvider ResolveApiVersionDescriptionProvider( IServiceProvider serviceProvider ) - { - var factory = serviceProvider.GetRequiredService(); - var endpointDataSource = new EmptyEndpointDataSource(); - return factory.Create( endpointDataSource ); - } - - private sealed class EmptyEndpointDataSource : EndpointDataSource - { - public override IReadOnlyList Endpoints => Array.Empty(); - - public override IChangeToken GetChangeToken() => new CancellationChangeToken( CancellationToken.None ); - - public override IReadOnlyList GetGroupedEndpoints( RouteGroupContext context ) => Array.Empty(); - } } \ No newline at end of file From 4c6091f01451c05323ac55c2f0a4f9c144bf3c9c Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 26 Mar 2024 09:44:50 -0700 Subject: [PATCH 17/41] Simplify usings --- .../DependencyInjection/IServiceCollectionExtensions.cs | 1 - 1 file changed, 1 deletion(-) 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 83521614..89d6d51c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using System.Diagnostics.CodeAnalysis; using static ServiceDescriptor; using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; From 22a775b8eae36147587faaa062d7555260c94736 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Tue, 26 Mar 2024 10:21:09 -0700 Subject: [PATCH 18/41] Bump versions and update release notes --- .../Asp.Versioning.Abstractions.csproj | 4 ++-- .../src/Asp.Versioning.Abstractions/ReleaseNotes.txt | 2 +- .../Asp.Versioning.OData.ApiExplorer.csproj | 4 ++-- .../src/Asp.Versioning.OData/Asp.Versioning.OData.csproj | 4 ++-- .../WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj | 4 ++-- .../Asp.Versioning.Mvc.ApiExplorer.csproj | 4 ++-- .../WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj | 4 ++-- .../Asp.Versioning.Http.Client.csproj | 4 ++-- src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt | 2 +- 9 files changed, 16 insertions(+), 16 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 6fb59849..68dd7991 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj @@ -1,8 +1,8 @@  - 8.0.1 - 8.0.0.0 + 8.1.0 + 8.1.0.0 $(DefaultTargetFramework);netstandard1.0;netstandard2.0 API Versioning Abstractions The abstractions library for API versioning. diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt b/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt index d6943ffb..5f282702 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ReleaseNotes.txt @@ -1 +1 @@ -TryResolvePolicy should correctly fall back ([#1064](https://github.com/dotnet/aspnet-api-versioning/issues/1064)) \ 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 d132353a..c78ccdc9 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 @@  - 8.0.0 - 8.0.0.0 + 8.1.0 + 8.1.0.0 $(DefaultTargetFramework) 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 ee194d18..cec744b0 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 @@  - 8.0.0 - 8.0.0.0 + 8.1.0 + 8.1.0.0 $(DefaultTargetFramework) Asp.Versioning ASP.NET Core API Versioning with OData v4.0 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 32e89b8d..3abd2a2c 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 @@  - 8.0.0 - 8.0.0.0 + 8.1.0 + 8.1.0.0 $(DefaultTargetFramework) Asp.Versioning ASP.NET Core API Versioning 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 35aa3746..528a203a 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 @@  - 8.0.0 - 8.0.0.0 + 8.1.0 + 8.1.0.0 $(DefaultTargetFramework) Asp.Versioning.ApiExplorer ASP.NET Core API Versioning API Explorer 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 0871cc58..52963338 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 @@  - 8.0.0 - 8.0.0.0 + 8.1.0 + 8.1.0.0 $(DefaultTargetFramework) Asp.Versioning ASP.NET Core API Versioning diff --git a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj index e508c993..b704593d 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj +++ b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj @@ -1,8 +1,8 @@  - 8.0.1 - 8.0.0.0 + 8.1.0 + 8.1.0.0 $(DefaultTargetFramework);netstandard1.1;netstandard2.0 Asp.Versioning.Http API Versioning Client Extensions diff --git a/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt b/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt index 0cf60623..5f282702 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt +++ b/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt @@ -1 +1 @@ -Parse single header CSVs ([#1070](https://github.com/dotnet/aspnet-api-versioning/issues/1070)) \ No newline at end of file + \ No newline at end of file From 4bd1c2d9a1da065af1de8af1423bf2b8fb845eed Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 14 Apr 2024 11:45:44 -0700 Subject: [PATCH 19/41] Render Swagger UI links in a fancier, more useful way --- .../OpenApiODataWebApiExample/Startup.cs | 31 +++++++++++++++--- .../SomeOpenApiODataWebApiExample/Startup.cs | 31 +++++++++++++++--- .../WebApi/OpenApiWebApiExample/Startup.cs | 32 ++++++++++++++++--- .../ConfigureSwaggerOptions.cs | 23 ++++++++++--- .../ConfigureSwaggerOptions.cs | 23 ++++++++++--- .../ConfigureSwaggerOptions.cs | 23 ++++++++++--- .../OpenApiExample/ConfigureSwaggerOptions.cs | 23 ++++++++++--- 7 files changed, 155 insertions(+), 31 deletions(-) diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs index e867f18d..e9041933 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs @@ -5,6 +5,7 @@ using Asp.Versioning.Conventions; using Asp.Versioning.OData; using Microsoft.AspNet.OData.Extensions; +using Microsoft.Extensions.Primitives; using Microsoft.OData; using Newtonsoft.Json.Serialization; using Owin; @@ -131,25 +132,45 @@ public void Configuration( IAppBuilder builder ) { description.AppendLine(); + var rendered = false; + for ( var i = 0; i < policy.Links.Count; i++ ) { var link = policy.Links[i]; if ( link.Type == "text/html" ) { - description.AppendLine(); - - if ( link.Title.HasValue ) + if ( !rendered ) { - description.Append( link.Title.Value ).Append( ": " ); + description.AppendLine(); + description.Append( "**Links**" ); + description.AppendLine(); + rendered = true; } - description.Append( link.LinkTarget.OriginalString ); + if ( StringSegment.IsNullOrEmpty( link.Title ) ) + { + if ( link.LinkTarget.IsAbsoluteUri ) + { + description.AppendLine( $"- {link.LinkTarget.OriginalString}" ); + } + else + { + description.AppendFormat( "- {0}", link.LinkTarget.OriginalString ); + description.AppendLine(); + } + } + else + { + description.AppendLine( $"- [{link.Title}]({link.LinkTarget.OriginalString})" ); + } } } } } + description.AppendLine(); + description.AppendLine( "**Additional Information**" ); info.Version( group.Name, $"Sample API {group.ApiVersion}" ) .Contact( c => c.Name( "Bill Mei" ).Email( "bill.mei@somewhere.com" ) ) .Description( description.ToString() ) diff --git a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs index dca602ca..9e223a7e 100644 --- a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs +++ b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs @@ -3,6 +3,7 @@ using Asp.Versioning; using Asp.Versioning.Conventions; using Microsoft.AspNet.OData.Extensions; +using Microsoft.Extensions.Primitives; using Microsoft.OData; using Newtonsoft.Json.Serialization; using Owin; @@ -105,25 +106,45 @@ public void Configuration( IAppBuilder builder ) { description.AppendLine(); + var rendered = false; + for ( var i = 0; i < policy.Links.Count; i++ ) { var link = policy.Links[i]; if ( link.Type == "text/html" ) { - description.AppendLine(); - - if ( link.Title.HasValue ) + if ( !rendered ) { - description.Append( link.Title.Value ).Append( ": " ); + description.AppendLine(); + description.Append( "**Links**" ); + description.AppendLine(); + rendered = true; } - description.Append( link.LinkTarget.OriginalString ); + if ( StringSegment.IsNullOrEmpty( link.Title ) ) + { + if ( link.LinkTarget.IsAbsoluteUri ) + { + description.AppendLine( $"- {link.LinkTarget.OriginalString}" ); + } + else + { + description.AppendFormat( "- {0}", link.LinkTarget.OriginalString ); + description.AppendLine(); + } + } + else + { + description.AppendLine( $"- [{link.Title}]({link.LinkTarget.OriginalString})" ); + } } } } } + description.AppendLine(); + description.AppendLine( "**Additional Information**" ); info.Version( group.Name, $"Sample API {group.ApiVersion}" ) .Contact( c => c.Name( "Bill Mei" ).Email( "bill.mei@somewhere.com" ) ) .Description( description.ToString() ) diff --git a/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs b/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs index b3651855..848f77f5 100644 --- a/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs +++ b/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs @@ -2,6 +2,7 @@ using Asp.Versioning; using Asp.Versioning.Routing; +using Microsoft.Extensions.Primitives; using Owin; using Swashbuckle.Application; using System.IO; @@ -10,6 +11,7 @@ using System.Web.Http; using System.Web.Http.Description; using System.Web.Http.Routing; +using static System.Net.Mime.MediaTypeNames; /// /// Represents the startup process for the application. @@ -88,25 +90,45 @@ public void Configuration( IAppBuilder builder ) { description.AppendLine(); + var rendered = false; + for ( var i = 0; i < policy.Links.Count; i++ ) { var link = policy.Links[i]; if ( link.Type == "text/html" ) { - description.AppendLine(); - - if ( link.Title.HasValue ) + if ( !rendered ) { - description.Append( link.Title.Value ).Append( ": " ); + description.AppendLine(); + description.Append( "**Links**" ); + description.AppendLine(); + rendered = true; } - description.Append( link.LinkTarget.OriginalString ); + if ( StringSegment.IsNullOrEmpty( link.Title ) ) + { + if ( link.LinkTarget.IsAbsoluteUri ) + { + description.AppendLine( $"- {link.LinkTarget.OriginalString}" ); + } + else + { + description.AppendFormat( "- {0}", link.LinkTarget.OriginalString ); + description.AppendLine(); + } + } + else + { + description.AppendLine( $"- [{link.Title}]({link.LinkTarget.OriginalString})" ); + } } } } } + description.AppendLine(); + description.AppendLine( "**Additional Information**" ); info.Version( group.Name, $"Example API {group.ApiVersion}" ) .Contact( c => c.Name( "Bill Mei" ).Email( "bill.mei@somewhere.com" ) ) .Description( description.ToString() ) diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs index d0575a5e..f81ed540 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -4,6 +4,7 @@ using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Text; @@ -63,25 +64,39 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri { text.AppendLine(); + var rendered = false; + for ( var i = 0; i < policy.Links.Count; i++ ) { var link = policy.Links[i]; if ( link.Type == "text/html" ) { - text.AppendLine(); - - if ( link.Title.HasValue ) + if ( !rendered ) { - text.Append( link.Title.Value ).Append( ": " ); + text.Append( "

Links

" ); + } } } + text.Append( "

Additional Information

" ); info.Description = text.ToString(); return info; diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs index 987145a1..4cad607a 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -4,6 +4,7 @@ using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Text; @@ -63,25 +64,39 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri { text.AppendLine(); + var rendered = false; + for ( var i = 0; i < policy.Links.Count; i++ ) { var link = policy.Links[i]; if ( link.Type == "text/html" ) { - text.AppendLine(); - - if ( link.Title.HasValue ) + if ( !rendered ) { - text.Append( link.Title.Value ).Append( ": " ); + text.Append( "

Links

" ); + } } } + text.Append( "

Additional Information

" ); info.Description = text.ToString(); return info; diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs index bee2b1f6..4673321d 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs @@ -4,6 +4,7 @@ using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Text; @@ -63,25 +64,39 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri { text.AppendLine(); + var rendered = false; + for ( var i = 0; i < policy.Links.Count; i++ ) { var link = policy.Links[i]; if ( link.Type == "text/html" ) { - text.AppendLine(); - - if ( link.Title.HasValue ) + if ( !rendered ) { - text.Append( link.Title.Value ).Append( ": " ); + text.Append( "

Links

" ); + } } } + text.Append( "

Additional Information

" ); info.Description = text.ToString(); return info; diff --git a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs index bee2b1f6..4673321d 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs @@ -4,6 +4,7 @@ using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Text; @@ -63,25 +64,39 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri { text.AppendLine(); + var rendered = false; + for ( var i = 0; i < policy.Links.Count; i++ ) { var link = policy.Links[i]; if ( link.Type == "text/html" ) { - text.AppendLine(); - - if ( link.Title.HasValue ) + if ( !rendered ) { - text.Append( link.Title.Value ).Append( ": " ); + text.Append( "

Links

" ); + } } } + text.Append( "

Additional Information

" ); info.Description = text.ToString(); return info; From 8eec498d29069640322b31352bc7c7f61e3d06b3 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 14 Apr 2024 11:57:14 -0700 Subject: [PATCH 20/41] Use more succinct pattern matching --- examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs | 4 ++-- .../AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs | 5 ++--- examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs | 5 ++--- .../OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs | 5 ++--- .../OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs | 5 ++--- .../WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs | 5 ++--- .../WebApi/OpenApiExample/ConfigureSwaggerOptions.cs | 5 ++--- 7 files changed, 14 insertions(+), 20 deletions(-) diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs index e9041933..2577d01c 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs @@ -119,9 +119,9 @@ public void Configuration( IAppBuilder builder ) description.Append( " This API version has been deprecated." ); } - if ( group.SunsetPolicy is SunsetPolicy policy ) + if ( group.SunsetPolicy is { } policy ) { - if ( policy.Date is DateTimeOffset when ) + if ( policy.Date is { } when ) { description.Append( " The API will be sunset on " ) .Append( when.Date.ToShortDateString() ) diff --git a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs index 9e223a7e..1ac7209a 100644 --- a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs +++ b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs @@ -1,6 +1,5 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; using Asp.Versioning.Conventions; using Microsoft.AspNet.OData.Extensions; using Microsoft.Extensions.Primitives; @@ -93,9 +92,9 @@ public void Configuration( IAppBuilder builder ) description.Append( " This API version has been deprecated." ); } - if ( group.SunsetPolicy is SunsetPolicy policy ) + if ( group.SunsetPolicy is { } policy ) { - if ( policy.Date is DateTimeOffset when ) + if ( policy.Date is { } when ) { description.Append( " The API will be sunset on " ) .Append( when.Date.ToShortDateString() ) diff --git a/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs b/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs index 848f77f5..2a060f11 100644 --- a/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs +++ b/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs @@ -11,7 +11,6 @@ using System.Web.Http; using System.Web.Http.Description; using System.Web.Http.Routing; -using static System.Net.Mime.MediaTypeNames; /// /// Represents the startup process for the application. @@ -77,9 +76,9 @@ public void Configuration( IAppBuilder builder ) description.Append( " This API version has been deprecated." ); } - if ( group.SunsetPolicy is SunsetPolicy policy ) + if ( group.SunsetPolicy is { } policy ) { - if ( policy.Date is DateTimeOffset when ) + if ( policy.Date is { } when ) { description.Append( " The API will be sunset on " ) .Append( when.Date.ToShortDateString() ) diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs index f81ed540..5384c160 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -1,6 +1,5 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -51,9 +50,9 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is SunsetPolicy policy ) + if ( description.SunsetPolicy is { } policy ) { - if ( policy.Date is DateTimeOffset when ) + if ( policy.Date is { } when ) { text.Append( " The API will be sunset on " ) .Append( when.Date.ToShortDateString() ) diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs index 4cad607a..b725ab72 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -1,6 +1,5 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -51,9 +50,9 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is SunsetPolicy policy ) + if ( description.SunsetPolicy is { } policy ) { - if ( policy.Date is DateTimeOffset when ) + if ( policy.Date is { } when ) { text.Append( " The API will be sunset on " ) .Append( when.Date.ToShortDateString() ) diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs index 4673321d..d531cea4 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs @@ -1,6 +1,5 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -51,9 +50,9 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is SunsetPolicy policy ) + if ( description.SunsetPolicy is { } policy ) { - if ( policy.Date is DateTimeOffset when ) + if ( policy.Date is { } when ) { text.Append( " The API will be sunset on " ) .Append( when.Date.ToShortDateString() ) diff --git a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs index 4673321d..d531cea4 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs @@ -1,6 +1,5 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -51,9 +50,9 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is SunsetPolicy policy ) + if ( description.SunsetPolicy is { } policy ) { - if ( policy.Date is DateTimeOffset when ) + if ( policy.Date is { } when ) { text.Append( " The API will be sunset on " ) .Append( when.Date.ToShortDateString() ) From 3468695ffb41e57709350764a2e5063a426b5f12 Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:59:12 -1000 Subject: [PATCH 21/41] use IsDevelopment --- .../WebApi/MinimalOpenApiExample/Program.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs index acf998ed..7ca38f15 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs @@ -264,18 +264,20 @@ .Produces( 400 ); app.UseSwagger(); -app.UseSwaggerUI( - options => - { - var descriptions = app.DescribeApiVersions(); - - // build a swagger endpoint for each discovered API version - foreach ( var description in descriptions ) +if ( builder.Environment.IsDevelopment() ) +{ + app.UseSwaggerUI( + options => { - var url = $"/swagger/{description.GroupName}/swagger.json"; - var name = description.GroupName.ToUpperInvariant(); - options.SwaggerEndpoint( url, name ); - } - } ); + var descriptions = app.DescribeApiVersions(); + // build a swagger endpoint for each discovered API version + foreach ( var description in descriptions ) + { + var url = $"/swagger/{description.GroupName}/swagger.json"; + var name = description.GroupName.ToUpperInvariant(); + options.SwaggerEndpoint( url, name ); + } + } ); +} app.Run(); \ No newline at end of file From 84b1b8083287b9fc448ffe02a55bca50d7d0e7c5 Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:19:01 -1000 Subject: [PATCH 22/41] use IsDevelopment --- .../AspNetCore/OData/SomeODataOpenApiExample/Program.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs index 7dc6c5c0..eefc3dca 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs @@ -64,7 +64,9 @@ // Configure the HTTP request pipeline. app.UseSwagger(); -app.UseSwaggerUI( +if ( builder.Environment.IsDevelopment() ) +{ + app.UseSwaggerUI( options => { var descriptions = app.DescribeApiVersions(); @@ -77,7 +79,7 @@ options.SwaggerEndpoint( url, name ); } } ); - +} app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); From 06c47d5599f925e92cd3263741d9e918dd9d8a34 Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:07:34 -1000 Subject: [PATCH 23/41] use IsDevelopment --- examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs | 2 +- examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs index eefc3dca..953e3bf5 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs @@ -64,7 +64,7 @@ // Configure the HTTP request pipeline. app.UseSwagger(); -if ( builder.Environment.IsDevelopment() ) +if ( app.Environment.IsDevelopment() ) { app.UseSwaggerUI( options => diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs index 7ca38f15..909d8261 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs @@ -264,7 +264,7 @@ .Produces( 400 ); app.UseSwagger(); -if ( builder.Environment.IsDevelopment() ) +if ( app.Environment.IsDevelopment() ) { app.UseSwaggerUI( options => From bb62b0bd721022ed097ce096211c8f27d7b006db Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:41:57 -1000 Subject: [PATCH 24/41] use IsDevelopment --- examples/AspNetCore/OData/ODataOpenApiExample/Program.cs | 3 +++ examples/AspNetCore/WebApi/OpenApiExample/Program.cs | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs b/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs index 1fd344e5..5afe9e5a 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs @@ -89,6 +89,8 @@ } app.UseSwagger(); +if ( app.Environment.IsDevelopment() ) +{ app.UseSwaggerUI( options => { @@ -102,6 +104,7 @@ options.SwaggerEndpoint( url, name ); } } ); +} app.UseHttpsRedirection(); app.UseAuthorization(); diff --git a/examples/AspNetCore/WebApi/OpenApiExample/Program.cs b/examples/AspNetCore/WebApi/OpenApiExample/Program.cs index 64ad0ad1..8e1bc9a8 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/Program.cs @@ -57,7 +57,9 @@ // Configure the HTTP request pipeline. app.UseSwagger(); -app.UseSwaggerUI( +if ( app.Environment.IsDevelopment() ) +{ + app.UseSwaggerUI( options => { var descriptions = app.DescribeApiVersions(); @@ -70,6 +72,7 @@ options.SwaggerEndpoint( url, name ); } } ); +} app.UseHttpsRedirection(); app.UseAuthorization(); From 99e527f595e1267a8a27d47f4b47171c47c698cc Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:52:13 -1000 Subject: [PATCH 25/41] use IsDevelopment --- examples/AspNetCore/OData/ODataOpenApiExample/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs b/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs index 5afe9e5a..4dd5e683 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs @@ -80,11 +80,11 @@ var app = builder.Build(); -// Configure the HTTP request pipeline. +// Configure HTTP request pipeline. if ( app.Environment.IsDevelopment() ) { - // navigate to ~/$odata to determine whether any endpoints did not match an odata route template + // Access ~/$odata to identify OData endpoints that failed to match a route template. app.UseODataRouteDebug(); } From 3fc071913dcded23eeb5ebe55bca44f3828488bf Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:40:36 -1000 Subject: [PATCH 26/41] use IsDevelopment --- .../OData/SomeODataOpenApiExample/Program.cs | 22 ++++++++--------- .../WebApi/OpenApiExample/Program.cs | 24 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs index 953e3bf5..55e70b17 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs @@ -68,17 +68,17 @@ { app.UseSwaggerUI( options => - { - var descriptions = app.DescribeApiVersions(); - - // build a swagger endpoint for each discovered API version - foreach ( var description in descriptions ) - { - var url = $"/swagger/{description.GroupName}/swagger.json"; - var name = description.GroupName.ToUpperInvariant(); - options.SwaggerEndpoint( url, name ); - } - } ); + { + var descriptions = app.DescribeApiVersions(); + + // build a swagger endpoint for each discovered API version + foreach ( var description in descriptions ) + { + var url = $"/swagger/{description.GroupName}/swagger.json"; + var name = description.GroupName.ToUpperInvariant(); + options.SwaggerEndpoint( url, name ); + } + } ); } app.UseHttpsRedirection(); app.UseAuthorization(); diff --git a/examples/AspNetCore/WebApi/OpenApiExample/Program.cs b/examples/AspNetCore/WebApi/OpenApiExample/Program.cs index 8e1bc9a8..454c60d7 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/Program.cs @@ -60,18 +60,18 @@ if ( app.Environment.IsDevelopment() ) { app.UseSwaggerUI( - options => - { - var descriptions = app.DescribeApiVersions(); - - // build a swagger endpoint for each discovered API version - foreach ( var description in descriptions ) - { - var url = $"/swagger/{description.GroupName}/swagger.json"; - var name = description.GroupName.ToUpperInvariant(); - options.SwaggerEndpoint( url, name ); - } - } ); + options => + { + var descriptions = app.DescribeApiVersions(); + + // build a swagger endpoint for each discovered API version + foreach ( var description in descriptions ) + { + var url = $"/swagger/{description.GroupName}/swagger.json"; + var name = description.GroupName.ToUpperInvariant(); + options.SwaggerEndpoint( url, name ); + } + } ); } app.UseHttpsRedirection(); From 470dafdec490f23da486a721bfe583346aa91afd Mon Sep 17 00:00:00 2001 From: Xavier Date: Fri, 11 Apr 2025 10:41:20 -0700 Subject: [PATCH 27/41] Update Microsoft.AspNetCore.OData package reference to support v 9.2.1 --- .../OData/ODataAdvancedExample/ODataAdvancedExample.csproj | 4 ++++ .../OData/ODataBasicExample/ODataBasicExample.csproj | 4 ++++ .../ODataConventionsExample/ODataConventionsExample.csproj | 4 ++++ .../OData/ODataOpenApiExample/ODataOpenApiExample.csproj | 4 ++++ .../SomeODataOpenApiExample/SomeODataOpenApiExample.csproj | 4 ++++ .../Conventions/ODataAttributeVisitor.cs | 2 +- .../Asp.Versioning.OData.ApiExplorer.csproj | 4 ++-- .../src/Asp.Versioning.OData/Asp.Versioning.OData.csproj | 6 +++--- .../OData/ODataApplicationModelProvider.cs | 2 +- .../ApiExplorer/ODataApiDescriptionProviderTest.cs | 4 ++-- .../ApplicationModels/DefaultApiControllerFilter.cs | 4 ++-- .../Conventions/ODataAttributeVisitor.cs | 4 ++-- 12 files changed, 33 insertions(+), 13 deletions(-) diff --git a/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj b/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj index ecccb3a9..f990c588 100644 --- a/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj +++ b/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj @@ -4,4 +4,8 @@ + + + + \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj b/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj index 6bdc8cef..389b3dc6 100644 --- a/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj +++ b/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj @@ -4,4 +4,8 @@ + + + + \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj b/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj index ecccb3a9..f990c588 100644 --- a/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj +++ b/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj @@ -4,4 +4,8 @@ + + + + \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj b/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj index 3bffcf5e..af705328 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj +++ b/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj @@ -12,4 +12,8 @@ + + + + \ No newline at end of file diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj b/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj index 3bffcf5e..af705328 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj @@ -12,4 +12,8 @@ + + + + \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs index 1c1a2f49..410f48ff 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs @@ -13,6 +13,6 @@ private void VisitAction( HttpActionDescriptor action ) var attributes = new List( controller.GetCustomAttributes( inherit: true ) ); attributes.AddRange( action.GetCustomAttributes( inherit: true ) ); - VisitEnableQuery( attributes ); + VisitEnableQuery( attributes.ToArray() ); } } \ 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 c78ccdc9..a443fa16 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 @@  - 8.1.0 - 8.1.0.0 + 8.2.0 + 8.2.0.0 $(DefaultTargetFramework) 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 cec744b0..1e87e465 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 @@  - 8.1.0 - 8.1.0.0 + 8.2.0 + 8.2.0.0 $(DefaultTargetFramework) Asp.Versioning ASP.NET Core API Versioning with OData v4.0 @@ -15,7 +15,7 @@ - + diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs index 402da34d..9ef71b7b 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs @@ -141,7 +141,7 @@ private static return (metadataControllers, supported, deprecated); } - private static ControllerModel? SelectBestMetadataController( IReadOnlyList controllers ) + private static ControllerModel? SelectBestMetadataController( List controllers ) { // note: there should be at least 2 metadata controllers, but there could be 3+ // if a developer defines their own custom controller. ultimately, there can be 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 511513b4..532abb00 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 @@ -256,9 +256,9 @@ private static void AssertQueryOptionWithoutOData( ApiDescription description, s parameter.ModelMetadata.Description.Should().EndWith( suffix + '.' ); } - private void PrintGroup( IReadOnlyList items ) + private void PrintGroup( ApiDescription[] items ) { - for ( var i = 0; i < items.Count; i++ ) + for ( var i = 0; i < items.Length; i++ ) { var item = items[i]; console.WriteLine( $"[{item.GroupName}] {item.HttpMethod} {item.RelativePath}" ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/DefaultApiControllerFilter.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/DefaultApiControllerFilter.cs index e8d851be..a8a1d46e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/DefaultApiControllerFilter.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/DefaultApiControllerFilter.cs @@ -10,7 +10,7 @@ namespace Asp.Versioning.ApplicationModels; [CLSCompliant( false )] public sealed class DefaultApiControllerFilter : IApiControllerFilter { - private readonly IReadOnlyList specifications; + private readonly List specifications; /// /// Initializes a new instance of the class. @@ -19,7 +19,7 @@ public sealed class DefaultApiControllerFilter : IApiControllerFilter /// specifications used by the filter /// to identify API controllers. public DefaultApiControllerFilter( IEnumerable specifications ) => - this.specifications = specifications.ToArray(); + this.specifications = specifications.ToList(); /// public IList Apply( IList controllers ) diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs index 7cb50224..fd973fe6 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs @@ -76,11 +76,11 @@ private void VisitModel( IEdmStructuredType modelType ) VisitMaxTop( querySettings ); } - private void VisitEnableQuery( IReadOnlyList attributes ) + private void VisitEnableQuery( EnableQueryAttribute[] attributes ) { var @default = new EnableQueryAttribute(); - for ( var i = 0; i < attributes.Count; i++ ) + for ( var i = 0; i < attributes.Length; i++ ) { var attribute = attributes[i]; From a1990bda30cb5767696bb4a8e799290dd6676aaa Mon Sep 17 00:00:00 2001 From: Xavier Date: Fri, 11 Apr 2025 10:44:48 -0700 Subject: [PATCH 28/41] Remove direct reference to Odata 9.2.1 --- .../OData/ODataAdvancedExample/ODataAdvancedExample.csproj | 4 ---- .../OData/ODataBasicExample/ODataBasicExample.csproj | 4 ---- .../ODataConventionsExample/ODataConventionsExample.csproj | 4 ---- .../OData/ODataOpenApiExample/ODataOpenApiExample.csproj | 4 ---- .../SomeODataOpenApiExample/SomeODataOpenApiExample.csproj | 4 ---- 5 files changed, 20 deletions(-) diff --git a/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj b/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj index f990c588..ecccb3a9 100644 --- a/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj +++ b/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj @@ -4,8 +4,4 @@ - - - - \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj b/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj index 389b3dc6..6bdc8cef 100644 --- a/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj +++ b/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj @@ -4,8 +4,4 @@ - - - - \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj b/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj index f990c588..ecccb3a9 100644 --- a/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj +++ b/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj @@ -4,8 +4,4 @@ - - - - \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj b/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj index af705328..3bffcf5e 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj +++ b/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj @@ -12,8 +12,4 @@ - - - - \ No newline at end of file diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj b/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj index af705328..3bffcf5e 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj @@ -12,8 +12,4 @@ - - - - \ No newline at end of file From 39cfc87fd96972a8db19170d004ec89211e7161d Mon Sep 17 00:00:00 2001 From: Xavier Date: Fri, 11 Apr 2025 13:29:45 -0700 Subject: [PATCH 29/41] Update the readme text. --- examples/AspNetCore/OData/Directory.Build.props | 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 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/AspNetCore/OData/Directory.Build.props b/examples/AspNetCore/OData/Directory.Build.props index 302bad37..0c6cf639 100644 --- a/examples/AspNetCore/OData/Directory.Build.props +++ b/examples/AspNetCore/OData/Directory.Build.props @@ -4,7 +4,7 @@ - + \ No newline at end of file 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..b5318606 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 +Support OData 9.0 ([#1103](https://github.com/dotnet/aspnet-api-versioning/issues/1103)) \ 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 1e87e465..e69c80d7 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 @@ -15,7 +15,7 @@ - + diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData/ReleaseNotes.txt index 5f282702..b5318606 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 +Support OData 9.0 ([#1103](https://github.com/dotnet/aspnet-api-versioning/issues/1103)) \ No newline at end of file From e3bc03ae7e7f03194363f272d4ce99f2ddaa3e61 Mon Sep 17 00:00:00 2001 From: Pavel Voronin Date: Mon, 29 Apr 2024 20:44:42 +0200 Subject: [PATCH 30/41] chore: Add remarks to xmldoc for default constructor of HeaderApiVersionReader It's easy to make a mistake expecting some defaults which are simply absent. I'd prefer getting rid of the default constructor, but that would be a breaking change. It's worth to be more explicit in the docs, and explain, that headers must be set. --- src/Common/src/Common/HeaderApiVersionReader.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Common/src/Common/HeaderApiVersionReader.cs b/src/Common/src/Common/HeaderApiVersionReader.cs index 2bffbf31..e5bea98c 100644 --- a/src/Common/src/Common/HeaderApiVersionReader.cs +++ b/src/Common/src/Common/HeaderApiVersionReader.cs @@ -15,6 +15,10 @@ public partial class HeaderApiVersionReader : IApiVersionReader /// /// Initializes a new instance of the class. /// + /// + /// There is no default header. Either initialize headers using the collection property, + /// or call a constructor that accepts header names. Otherwise, this reader has no effect. + /// public HeaderApiVersionReader() { } /// From cd174474c011bbdb6ce244266caec3b156270847 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 30 Aug 2025 20:14:23 +1000 Subject: [PATCH 31/41] Update documentation of ApiVersionReader in ApiVersioningOptions The default value is a combination of QueryStringApiVersionReader and UrlSegmentApiVersionReader Resolves #1098 --- src/Common/src/Common/ApiVersioningOptions.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Common/src/Common/ApiVersioningOptions.cs b/src/Common/src/Common/ApiVersioningOptions.cs index 82c1cfce..75f22711 100644 --- a/src/Common/src/Common/ApiVersioningOptions.cs +++ b/src/Common/src/Common/ApiVersioningOptions.cs @@ -59,14 +59,16 @@ public partial class ApiVersioningOptions /// /// Gets or sets the API version reader. /// - /// An API version reader object. The default value - /// is an instance of the . - /// The API version reader is used to read the - /// API version specified by a client. The default value is the - /// , which only reads the API version from - /// the "api-version" query string parameter. Replace the default value with an alternate - /// implementation, such as the , which - /// can read the API version from additional information like HTTP headers. + /// An API version reader object. The default value is a combined reader + /// with both and . + /// + /// The API version reader is used to read the API version specified by a + /// client. The default value consist of both and + /// , which reads the API version from the "api-version" query string + /// parameter and a path segment in the request URL respectively. + /// Replace the default value with an alternate implementation, such as the , + /// which can read the API version from additional information like HTTP headers. + /// #if !NETFRAMEWORK [CLSCompliant( false )] #endif From 371eda4864f6d48e69e71682d378672d6b9c6c69 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 30 Aug 2025 20:24:57 +1000 Subject: [PATCH 32/41] Fix Contributor Guide link in Pull Request Template --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d3130c5b..e3c10e68 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ -- [ ] You've read the [Contributor Guide](https://github.com/dotnet/aspnet-api-versioning/blob/main/CONTRIBUTING.md) and [Code of Conduct](https://dotnetfoundation.org/code-of-conduct). +- [ ] You've read the [Contributor Guide](https://github.com/dotnet/aspnet-api-versioning/blob/main/docs/CONTRIBUTING.md) and [Code of Conduct](https://dotnetfoundation.org/code-of-conduct). - [ ] You've included unit or integration tests for your change, where applicable. - [ ] You've included inline docs for your change, where applicable. - [ ] There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue. From 5ba0e73cdb4a2a7215db19b8439ffe1b3208fa79 Mon Sep 17 00:00:00 2001 From: Andrea Cuneo Date: Wed, 28 Aug 2024 08:36:26 +0000 Subject: [PATCH 33/41] fix(ApiExplorer): SubstitutedType have invalid property setter The PropertySetter of the SubstitutedType have 0 parameters: they had the same signature of the Getter. resolves: #1104 --- .../OData/DefaultModelTypeBuilder.cs | 6 +++- .../OData/DefaultModelTypeBuilderTest.cs | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs index ce5a1010..f8a836ec 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs @@ -417,7 +417,11 @@ private static PropertyBuilder AddProperty( var field = addTo.DefineField( "_" + name, shouldBeAdded, FieldAttributes.Private ); var propertyBuilder = addTo.DefineProperty( name, PropertyAttributes.HasDefault, shouldBeAdded, null ); var getter = addTo.DefineMethod( "get_" + name, propertyMethodAttributes, shouldBeAdded, Type.EmptyTypes ); - var setter = addTo.DefineMethod( "set_" + name, propertyMethodAttributes, shouldBeAdded, Type.EmptyTypes ); + + /* returnType is 'null' instead of type(void) as per docs + * see: https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.propertybuilder?view=net-9.0 + */ + var setter = addTo.DefineMethod( "set_" + name, propertyMethodAttributes, null, [shouldBeAdded] ); var il = getter.GetILGenerator(); il.Emit( OpCodes.Ldarg_0 ); diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs index 100af41f..2a0fc160 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs @@ -409,6 +409,41 @@ public void substitute_should_resolve_types_that_reference_a_model_that_match_th substitutionType.Should().NotBeOfType(); } + [Fact] + public void substituted_type_should_have_valid_runtime_properties__issue1104() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + + var address = modelBuilder.EntitySet
( nameof( Address ) ).EntityType; + address.Ignore( x => x.City ); // force substitution + var addressType = typeof( Address ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + + // act + var substitutedType = addressType.SubstituteIfNecessary( context ); + + // assert + substitutedType.Should().NotBe( addressType ); +#if NET452 + substitutedType.GetRuntimeProperties().Should().HaveCount( 5 ); + foreach ( var substitutedProperty in substitutedType.GetRuntimeProperties() ) + { + substitutedProperty.Should().NotBeNull(); + substitutedProperty.GetSetMethod( true ).Should().NotBeNull() + .And.Match( p => p.ReturnType == typeof( void ) ) + .And.Match( p => p.GetParameters().Length == 1 ); + } +#else + substitutedType.GetRuntimeProperties().Should().HaveCount( 5 ) + .And.AllSatisfy( prop => prop.GetSetMethod( true ).Should() + .NotBeNull() + .And.ReturnVoid() + .And.Match( setter => setter.GetParameters().Length == 1 ) ); +#endif + } + public static IEnumerable SubstitutionNotRequiredData { get From 084d7411228c59db5bd75467d693fce7aa2f09af Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 16 Nov 2025 10:33:14 -0800 Subject: [PATCH 34/41] Ensure 'code' extension is set. Fixes #1091 --- .../when using a query string.cs | 1 + .../src/Asp.Versioning.Http/Routing/EndpointProblem.cs | 2 +- .../Common.Acceptance.Tests/HttpContentExtensions.cs | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) 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 7cea36c0..3e52096b 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 @@ -49,6 +49,7 @@ public async Task then_get_should_return_400_for_an_unspecified_version() // assert response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type ); + problem.Extensions["code"].ToString().Should().Be( "ApiVersionUnspecified" ); } public when_using_a_query_string( BasicFixture fixture, ITestOutputHelper console ) 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 9a8b539b..4de5aa52 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs @@ -25,7 +25,7 @@ internal static ProblemDetailsContext New( HttpContext context, ProblemDetailsIn }, }; - if ( string.IsNullOrEmpty( code ) ) + if ( !string.IsNullOrEmpty( code ) ) { newContext.ProblemDetails.Extensions[nameof( code )] = code; } diff --git a/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs b/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs index 43ffe5b9..20418181 100644 --- a/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs +++ b/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs @@ -6,20 +6,30 @@ namespace Asp.Versioning; using Microsoft.AspNetCore.Mvc; #endif using System.Net.Http; +#if NETFRAMEWORK using System.Net.Http.Formatting; +#else +using System.Net.Http.Json; +#endif internal static class HttpContentExtensions { +#if NETFRAMEWORK private static readonly JsonMediaTypeFormatter ProblemDetailsMediaTypeFormatter = new() { SupportedMediaTypes = { new( ProblemDetailsDefaults.MediaType.Json ) }, }; private static readonly IEnumerable MediaTypeFormatters = new[] { ProblemDetailsMediaTypeFormatter }; +#endif public static Task ReadAsProblemDetailsAsync( this HttpContent content, CancellationToken cancellationToken = default ) => +#if NETFRAMEWORK content.ReadAsAsync( MediaTypeFormatters, cancellationToken ); +#else + content.ReadFromJsonAsync( cancellationToken ); +#endif #pragma warning disable IDE0060 // Remove unused parameter #pragma warning disable IDE0079 // Remove unnecessary suppression From 4c9229d7439a986e8a13f992640fd9ba891a55d0 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 16 Nov 2025 13:43:50 -0800 Subject: [PATCH 35/41] Add editor guidelines --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index 68505e1c..6e6a94e7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,7 @@ root = true # don't use tabs for indentation [*] indent_style = space +guidelines = 120 1px solid yellow vsspell_section_id = 41b65011239a40959ccaae2a4ec7044a vsspell_ignored_words_41b65011239a40959ccaae2a4ec7044a = Accessor|app|clr|Edm|inline|middleware|Mvc|odata|Validator|Deconstruct From 2271993b78ae6be7fa3d649c59749e401649bfdd Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 16 Nov 2025 13:44:56 -0800 Subject: [PATCH 36/41] Report API versions when unspecified or malformed. Fixes #1120 --- .../Http/MinimalApiFixture.cs | 1 + .../when using a query string.cs | 50 +++++++++++++++++++ ...a query string and split into two types.cs | 4 ++ .../Routing/ApiVersionMatcherPolicy.cs | 4 +- .../Routing/ApiVersionPolicyJumpTable.cs | 4 ++ .../Routing/ClientErrorEndpointBuilder.cs | 2 +- .../Routing/EdgeBuilder.cs | 12 ++--- .../Routing/EndpointProblem.cs | 22 +++++--- .../Routing/MalformedApiVersionEndpoint.cs | 8 +-- .../Routing/UnspecifiedApiVersionEndpoint.cs | 17 +++++-- 10 files changed, 102 insertions(+), 22 deletions(-) diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs index 20789427..e971646f 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs @@ -57,6 +57,7 @@ protected override void OnConfigureEndpoints( IEndpointRouteBuilder endpoints ) protected override void OnAddApiVersioning( ApiVersioningOptions options ) { + options.ReportApiVersions = true; options.ApiVersionReader = ApiVersionReader.Combine( new QueryStringApiVersionReader(), new UrlSegmentApiVersionReader(), diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs index 2823e74b..85016516 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs @@ -4,6 +4,7 @@ namespace given_a_versioned_minimal_API; using Asp.Versioning; using Asp.Versioning.Http; +using static System.Net.HttpStatusCode; [Collection( nameof( MinimalApiTestCollection ) )] public class when_using_a_query_string : AcceptanceTest @@ -34,9 +35,58 @@ public async Task then_get_should_report_api_versions() var response = await GetAsync( "api/values?api-version=1.0" ); // assert + response.StatusCode.Should().Be( OK ); response.Headers.GetValues( "api-supported-versions" ).Should().Equal( "1.0, 2.0" ); } + [Fact] + public async Task then_get_should_return_400_for_an_unsupported_version() + { + // arrange + + + // act + var response = await GetAsync( "api/values?api-version=3.0" ); + var problem = await response.Content.ReadAsProblemDetailsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" ); + problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); + } + + [Fact] + public async Task then_get_should_return_400_for_an_unspecified_version() + { + // arrange + + + // act + var response = await GetAsync( "api/values" ); + var problem = await response.Content.ReadAsProblemDetailsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" ); + problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type ); + } + + [Fact] + public async Task then_get_should_return_400_for_a_malformed_version() + { + // arrange + + + // act + var response = await GetAsync( "api/values?api-version=abc" ); + var problem = await response.Content.ReadAsProblemDetailsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + response.Headers.GetValues( "api-supported-versions" ).Should().Equal( "1.0, 2.0" ); + problem.Type.Should().Be( ProblemDetailsDefaults.Invalid.Type ); + } + public when_using_a_query_string( MinimalApiFixture fixture, ITestOutputHelper console ) : base( fixture ) => console.WriteLine( fixture.DirectedGraphVisualizationUrl ); } \ No newline at end of file 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 7c78efd4..7b1a868c 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 @@ -94,9 +94,12 @@ public async Task then_get_should_return_400_for_an_unsupported_version() // act var response = await GetAsync( "api/values?api-version=3.0" ); + var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert response.StatusCode.Should().Be( BadRequest ); + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" ); + problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } [Fact] @@ -111,6 +114,7 @@ public async Task then_get_should_return_400_for_an_unspecified_version() // assert response.StatusCode.Should().Be( BadRequest ); + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" ); problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type ); } 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 e43223aa..8530e5a4 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs @@ -337,14 +337,14 @@ private static void Collate( { // 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 + // impossible to be 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 + // added to document that is discovered ApiVersionModel model; if ( supported == null ) 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 24bb4a23..e2d32a26 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs @@ -76,6 +76,8 @@ public override int GetDestination( HttpContext httpContext ) return rejection.AssumeDefault; } + httpContext.Features.Set( policyFeature ); + // 3. unspecified return versionsByUrlOnly /* 404 */ ? rejection.Exit @@ -86,6 +88,8 @@ public override int GetDestination( HttpContext httpContext ) if ( !parser.TryParse( rawApiVersion, out var apiVersion ) ) { + httpContext.Features.Set( policyFeature ); + if ( versionsByUrl ) { feature.RawRequestedApiVersion = rawApiVersion; 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 cad20584..9b594f04 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs @@ -30,7 +30,7 @@ public Endpoint Build() { if ( feature.RawRequestedApiVersions.Count == 0 ) { - return new UnspecifiedApiVersionEndpoint( logger, GetDisplayNames() ); + return new UnspecifiedApiVersionEndpoint( logger, options, GetDisplayNames() ); } return new UnsupportedApiVersionEndpoint( options ); 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 8fb60798..621cf185 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs @@ -32,12 +32,12 @@ public EdgeBuilder( keys = new( capacity + 1 ); 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.Unsupported] = new( capacity: 1 ) { new UnsupportedApiVersionEndpoint( options ) }, - [EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint( options ) }, - [EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint( options ) }, + [EdgeKey.Malformed] = [new MalformedApiVersionEndpoint( logger, options )], + [EdgeKey.Ambiguous] = [new AmbiguousApiVersionEndpoint( logger )], + [EdgeKey.Unspecified] = [new UnspecifiedApiVersionEndpoint( logger, options )], + [EdgeKey.Unsupported] = [new UnsupportedApiVersionEndpoint( options )], + [EdgeKey.UnsupportedMediaType] = [new UnsupportedMediaTypeEndpoint( options )], + [EdgeKey.NotAcceptable] = [new NotAcceptableEndpoint( options )], }; } 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 4de5aa52..61fccfd3 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs @@ -33,20 +33,30 @@ internal static ProblemDetailsContext New( HttpContext context, ProblemDetailsIn return newContext; } - internal static Task UnsupportedApiVersion( - HttpContext context, - ApiVersioningOptions options, - int statusCode ) + internal static bool TryReportApiVersions( HttpContext context, ApiVersioningOptions options ) { - context.Response.StatusCode = statusCode; - if ( options.ReportApiVersions && context.Features.Get() is ApiVersionPolicyFeature feature ) { var reporter = context.RequestServices.GetRequiredService(); var model = feature.Metadata.Map( reporter.Mapping ); context.Response.OnStarting( ReportApiVersions, (reporter, context.Response, model) ); + return true; + } + else + { + return false; } + } + + internal static Task UnsupportedApiVersion( + HttpContext context, + ApiVersioningOptions options, + int statusCode ) + { + context.Response.StatusCode = statusCode; + + TryReportApiVersions( context, options ); if ( context.TryGetProblemDetailsService( out var problemDetails ) ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs index c129794a..5c777eb6 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs @@ -12,16 +12,18 @@ internal sealed class MalformedApiVersionEndpoint : Endpoint { private const string Name = "400 Invalid API Version"; - internal MalformedApiVersionEndpoint( ILogger logger ) - : base( c => OnExecute( c, logger ), Empty, Name ) { } + internal MalformedApiVersionEndpoint( ILogger logger, ApiVersioningOptions options ) + : base( context => OnExecute( context, options, logger ), Empty, Name ) { } - private static Task OnExecute( HttpContext context, ILogger logger ) + private static Task OnExecute( HttpContext context, ApiVersioningOptions options, ILogger logger ) { var requestedVersion = context.ApiVersioningFeature().RawRequestedApiVersion; logger.ApiVersionInvalid( requestedVersion ); context.Response.StatusCode = StatusCodes.Status400BadRequest; + EndpointProblem.TryReportApiVersions( context, options ); + if ( !context.TryGetProblemDetailsService( out var problemDetails ) ) { return Task.CompletedTask; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs index 6597aec9..7c861b88 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs @@ -10,10 +10,17 @@ internal sealed class UnspecifiedApiVersionEndpoint : Endpoint { private const string Name = "400 Unspecified API Version"; - internal UnspecifiedApiVersionEndpoint( ILogger logger, string[]? displayNames = default ) - : base( c => OnExecute( c, displayNames, logger ), Empty, Name ) { } - - private static Task OnExecute( HttpContext context, string[]? candidateEndpoints, ILogger logger ) + internal UnspecifiedApiVersionEndpoint( + ILogger logger, + ApiVersioningOptions options, + string[]? displayNames = default ) + : base( context => OnExecute( context, options, displayNames, logger ), Empty, Name ) { } + + private static Task OnExecute( + HttpContext context, + ApiVersioningOptions options, + string[]? candidateEndpoints, + ILogger logger ) { if ( candidateEndpoints == null || candidateEndpoints.Length == 0 ) { @@ -26,6 +33,8 @@ private static Task OnExecute( HttpContext context, string[]? candidateEndpoints context.Response.StatusCode = StatusCodes.Status400BadRequest; + EndpointProblem.TryReportApiVersions( context, options ); + if ( context.TryGetProblemDetailsService( out var problemDetails ) ) { return problemDetails.TryWriteAsync( From 7b4cb603a179296ea735eefeff5738f57fec3851 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 21 Nov 2025 07:55:38 -0800 Subject: [PATCH 37/41] Replace registered IProblemDetailsWriter in-place at the same index. Fixes #1135 --- .../DependencyInjection/IServiceCollectionExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 89d6d51c..f8948ab4 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -214,7 +214,7 @@ LinkGenerator NewFactory( IServiceProvider serviceProvider ) } } - // TODO: Remove in .NET 9.0 or .NET 8.0 patch + // TODO: Fixed and released; remove in .NET 10.0 // BUG: https://github.com/dotnet/aspnetcore/issues/52577 private static void TryAddProblemDetailsRfc7231Compliance( IServiceCollection services ) { @@ -225,11 +225,12 @@ private static void TryAddProblemDetailsRfc7231Compliance( IServiceCollection se return; } + var index = services.IndexOf( descriptor ); var decoratedType = descriptor.ImplementationType!; var lifetime = descriptor.Lifetime; + services[index] = Describe( typeof( IProblemDetailsWriter ), sp => NewProblemDetailsWriter( sp, decoratedType ), lifetime ); services.Add( Describe( decoratedType, decoratedType, lifetime ) ); - services.Replace( Describe( typeof( IProblemDetailsWriter ), sp => NewProblemDetailsWriter( sp, decoratedType ), lifetime ) ); static bool IsDefaultProblemDetailsWriter( ServiceDescriptor serviceDescriptor ) => serviceDescriptor.ServiceType == typeof( IProblemDetailsWriter ) && From d59ecae02f24ece26462ad62bd8eaac71f922e59 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 23 Nov 2025 10:42:28 -0800 Subject: [PATCH 38/41] Suppress warnings for use of .NET Standard older than 2.0. Required while legacy Web API is still supported. --- build/assets.msbuildproj | 1 + .../Asp.Versioning.Abstractions.csproj | 4 ++++ .../Asp.Versioning.Http.Client.csproj | 4 ++++ src/Common/src/Common.Backport/Common.Backport.msbuildproj | 1 + 4 files changed, 10 insertions(+) diff --git a/build/assets.msbuildproj b/build/assets.msbuildproj index 8d3dba2a..0ae99dc5 100644 --- a/build/assets.msbuildproj +++ b/build/assets.msbuildproj @@ -2,6 +2,7 @@ netstandard1.0 false + $(NoWarn);NETSDK1215 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 68dd7991..861b642c 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj @@ -14,6 +14,10 @@ true + + false + + diff --git a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj index b704593d..08fc140f 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj +++ b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj @@ -14,6 +14,10 @@ true + + false + + diff --git a/src/Common/src/Common.Backport/Common.Backport.msbuildproj b/src/Common/src/Common.Backport/Common.Backport.msbuildproj index 9dd66e3a..0da069a9 100644 --- a/src/Common/src/Common.Backport/Common.Backport.msbuildproj +++ b/src/Common/src/Common.Backport/Common.Backport.msbuildproj @@ -1,6 +1,7 @@ netstandard1.0 + $(NoWarn);NETSDK1215 From dd2b6437a5aec01b441e55c252c6e87a87e8ca27 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 23 Nov 2025 10:44:10 -0800 Subject: [PATCH 39/41] Enable trimming support with appropriate diagnostic and suppression annotations. Fixes #1094 --- .../PartialODataDescriptionProvider.cs | 2 ++ .../Asp.Versioning.OData.ApiExplorer.csproj | 1 + .../Asp.Versioning.OData.csproj | 1 + .../IServiceCollectionExtensions.cs | 1 + ...ODataMultiModelApplicationModelProvider.cs | 3 +- .../ApiExplorerOptionsFactory{T}.cs | 4 ++- .../Asp.Versioning.Mvc.ApiExplorer.csproj | 1 + .../Asp.Versioning.Mvc.csproj | 1 + .../IApiVersioningBuilderExtensions.cs | 7 ++++- .../Routing/ApiVersionUrlHelper.cs | 25 +++++++++++++-- .../ActionConventionBuilderExtensions.cs | 3 ++ .../Conventions/ActionMethodResolver.cs | 11 +++++-- .../ControllerConventionBuilderExtensions.cs | 3 ++ ...QueryOptionsConventionBuilderExtensions.cs | 6 +++- .../Microsoft.OData.Edm/EdmExtensions.cs | 3 ++ .../OData/ClassProperty.cs | 3 ++ .../OData/DefaultModelTypeBuilder.cs | 27 +++++++++++++--- .../OData/IModelTypeBuilder.cs | 18 ++++++++++- .../OData/TypeExtensions.cs | 31 +++++++++++++++++-- .../OData/DefaultModelTypeBuilderTest.cs | 9 +++--- 20 files changed, 139 insertions(+), 21 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 47eac901..4099958b 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 @@ -201,6 +201,8 @@ public ModelMetadata GetMetadataForType( Type modelType ) => throw new NotImplementedException(); } + [UnconditionalSuppressMessage( "ILLink", "IL2092" )] + [UnconditionalSuppressMessage( "ILLink", "IL2093" )] private sealed class StubModelTypeBuilder : IModelTypeBuilder { public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ) => 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 a443fa16..bf0661c6 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 @@ -8,6 +8,7 @@ ASP.NET Core API Versioning API Explorer for OData v4.0 The API Explorer extensions for ASP.NET Core API Versioning and OData v4.0. Asp;AspNet;AspNetCore;Versioning;ApiExplorer;OData + true 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 e69c80d7..64df816f 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 @@ -8,6 +8,7 @@ ASP.NET Core API Versioning with OData v4.0 A service API versioning library for Microsoft ASP.NET Core with OData v4.0. Asp;AspNet;AspNetCore;Versioning;OData + true 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 16432665..0f17e2eb 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs @@ -31,6 +31,7 @@ internal static ApplicationPartManager GetOrCreateApplicationPartManager( this I return partManager; } + [UnconditionalSuppressMessage( "ILLink", "IL2072", Justification = "Model configuration types are never trimmed" )] internal static void AddModelConfigurationsAsServices( this IServiceCollection services, ApplicationPartManager partManager ) { var feature = new ModelConfigurationFeature(); 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 cc798280..5676e28f 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs @@ -135,7 +135,8 @@ private static Type GetDefaultApplicationModelProviderType() return Type.GetType( $"{TypeName}, {assemblyName}", throwOnError: true, ignoreCase: false )!; } - private static Func, IApplicationModelProvider> CreateActivator( Type type ) + private static Func, IApplicationModelProvider> CreateActivator( + [DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.PublicConstructors )] Type type ) { var options = Parameter( typeof( IOptions ), "options" ); var @new = New( type.GetConstructors()[0], options ); 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 1a79e0a3..6c94a430 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 @@ -3,13 +3,15 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.Extensions.Options; +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; /// /// Represents a factory to create API explorer options. /// /// The type of options to create. [CLSCompliant( false )] -public class ApiExplorerOptionsFactory : OptionsFactory where T : ApiExplorerOptions +public class ApiExplorerOptionsFactory<[DynamicallyAccessedMembers( PublicParameterlessConstructor )] T> + : OptionsFactory where T : ApiExplorerOptions { private readonly IOptions optionsHolder; 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 528a203a..5845752b 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 @@ -8,6 +8,7 @@ ASP.NET Core API Versioning API Explorer The API Explorer extensions for ASP.NET Core API Versioning. Asp;AspNet;AspNetCore;Versioning;ApiExplorer + true 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 52963338..21ca9fe8 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 @@ -9,6 +9,7 @@ A service API versioning library for Microsoft ASP.NET Core MVC. Asp;AspNet;AspNetCore;MVC;Versioning true + true 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 eef6b507..21e66163 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -16,6 +16,7 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System.Runtime.CompilerServices; using static ServiceDescriptor; +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; /// /// Provides ASP.NET Core MVC specific extension methods for . @@ -85,7 +86,11 @@ private static object CreateInstance( this IServiceProvider services, ServiceDes return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType! ); } - private static void TryReplace( this IServiceCollection services ) + private static void TryReplace< + TService, + TImplementation, + [DynamicallyAccessedMembers( NonPublicConstructors | PublicConstructors )] + TReplacement>( this IServiceCollection services ) { var serviceType = typeof( TService ); var implementationType = typeof( TImplementation ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs index bbf03793..bd3e398e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs @@ -98,9 +98,30 @@ public ApiVersionUrlHelper( ActionContext actionContext, IUrlHelper url ) return current; } - if ( current is not RouteValueDictionary values ) + RouteValueDictionary values; + + if ( current is null ) + { + values = new() { { key, value } }; + return values; + } + + if ( current is RouteValueDictionary dictionary ) + { + values = dictionary; + } + else if ( current is IEnumerable> kvps ) { - values = current == null ? new() : new( current ); + values = []; + + foreach ( var kvp in kvps ) + { + values.Add( kvp.Key, kvp.Value ); + } + } + else + { + return current; } if ( !values.ContainsKey( key ) ) diff --git a/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs b/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs index 98cb8aa3..63464548 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs @@ -73,6 +73,9 @@ public static IActionConventionBuilder Action /// If there is only one corresponding match found, then the argument types are ignored; /// otherwise, the argument types are used for method overload resolution. Action /// methods that have the applied will also be ignored. +#if !NETFRAMEWORK + [UnconditionalSuppressMessage( "ILLink", "IL2072", Justification = "Controller types are never trimmed" )] +#endif public static IActionConventionBuilder Action( this IActionConventionBuilder builder, string methodName, params Type[] argumentTypes ) { ArgumentNullException.ThrowIfNull( builder ); diff --git a/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs b/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs index 3c0fa420..8feb6bfd 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs @@ -10,12 +10,19 @@ namespace Asp.Versioning.Conventions; #if NETFRAMEWORK using System.Web.Http; #endif +using static System.Reflection.BindingFlags; internal static class ActionMethodResolver { - internal static MethodInfo Resolve( Type controllerType, string methodName, Type[] argumentTypes ) + internal static MethodInfo Resolve( +#if !NETFRAMEWORK + [DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.PublicMethods )] +#endif + Type controllerType, + string methodName, + Type[] argumentTypes ) { - var methods = controllerType.GetRuntimeMethods().Where( m => m.Name == methodName && IsAction( m ) ).ToArray(); + var methods = controllerType.GetMethods( Instance | Public ).Where( m => m.Name == methodName && IsAction( m ) ).ToArray(); switch ( methods.Length ) { diff --git a/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs b/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs index 328b8b70..47e7a491 100644 --- a/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs +++ b/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs @@ -73,6 +73,9 @@ public static IActionConventionBuilder Action /// If there is only one corresponding match found, then the argument types are ignored; /// otherwise, the argument types are used for method overload resolution. Action /// methods that have the applied will also be ignored. +#if !NETFRAMEWORK + [UnconditionalSuppressMessage( "ILLink", "IL2072", Justification = "Controller types are never trimmed" )] +#endif public static IActionConventionBuilder Action( this IControllerConventionBuilder builder, string methodName, params Type[] argumentTypes ) { ArgumentNullException.ThrowIfNull( builder ); diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs index bcd941a0..33ab2471 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs @@ -16,6 +16,7 @@ namespace Asp.Versioning.Conventions; using System.Web.Http; using System.Web.Http.Controllers; #endif +using static System.Reflection.BindingFlags; /// /// Provides extension methods for the @@ -187,6 +188,9 @@ public static ODataActionQueryOptionsConventionBuilder Actionargument types are ignored; /// otherwise, the argument types are used for method overload resolution. Action /// methods that have the applied will also be ignored. +#if !NETFRAMEWORK + [UnconditionalSuppressMessage( "ILLink", "IL2075", Justification = "Controller types and actions are never trimmed" )] +#endif public static ODataActionQueryOptionsConventionBuilder Action( this IODataActionQueryOptionsConventionBuilder builder, string methodName, @@ -197,7 +201,7 @@ public static ODataActionQueryOptionsConventionBuilder Action( string message; var methods = builder.ControllerType - .GetRuntimeMethods() + .GetMethods( Instance | Public ) .Where( m => m.Name == methodName && IsAction( m ) ) .ToArray(); diff --git a/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs index 16ef9c9c..f9c60670 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs @@ -37,6 +37,9 @@ internal static class EdmExtensions return null; } +#if !NETFRAMEWORK + [UnconditionalSuppressMessage( "ILLink", "IL2057", Justification = "The types being referenced are well-known and will not be trimmed." )] +#endif private static Type? DeriveFromWellKnowPrimitive( string edmFullName ) => edmFullName switch { "Edm.String" or "Edm.Byte" or "Edm.SByte" or "Edm.Int16" or "Edm.Int32" or "Edm.Int64" or diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs b/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs index 98784a2f..f5959f92 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs @@ -21,6 +21,9 @@ internal ClassProperty( PropertyInfo clrProperty, Type propertyType ) Attributes = clrProperty.DeclaredAttributes().ToArray(); } +#if !NETFRAMEWORK + [UnconditionalSuppressMessage( "ILLink", "IL2072" )] +#endif internal ClassProperty( IEdmOperationParameter parameter, TypeSubstitutionContext context ) { Name = parameter.Name; diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs index f8a836ec..76fec0ff 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs @@ -14,6 +14,9 @@ namespace Asp.Versioning.OData; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Text; +#if !NETFRAMEWORK +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; +#endif using static System.Globalization.CultureInfo; using static System.Guid; using static System.Reflection.BindingFlags; @@ -22,6 +25,11 @@ namespace Asp.Versioning.OData; /// /// Represents the default model type builder. /// +#if !NETFRAMEWORK +[UnconditionalSuppressMessage( "ILLink", "IL2055")] +[UnconditionalSuppressMessage( "ILLink", "IL2070")] +[UnconditionalSuppressMessage( "ILLink", "IL2073")] +#endif public sealed class DefaultModelTypeBuilder : IModelTypeBuilder { /* design: there is typically a 1:1 relationship between an edm and api version. odata model bound settings @@ -59,7 +67,17 @@ private DefaultModelTypeBuilder( bool excludeAdHocModels, bool adHoc ) public DefaultModelTypeBuilder( bool includeAdHocModels = false ) => excludeAdHocModels = !includeAdHocModels; /// - public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) +#if !NETFRAMEWORK + [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif + public Type NewStructuredType( + IEdmModel model, + IEdmStructuredType structuredType, +#if !NETFRAMEWORK + [DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif + Type clrType, + ApiVersion apiVersion ) { ArgumentNullException.ThrowIfNull( model ); @@ -88,6 +106,9 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp } /// +#if !NETFRAMEWORK + [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ) { ArgumentNullException.ThrowIfNull( model ); @@ -417,10 +438,6 @@ private static PropertyBuilder AddProperty( var field = addTo.DefineField( "_" + name, shouldBeAdded, FieldAttributes.Private ); var propertyBuilder = addTo.DefineProperty( name, PropertyAttributes.HasDefault, shouldBeAdded, null ); var getter = addTo.DefineMethod( "get_" + name, propertyMethodAttributes, shouldBeAdded, Type.EmptyTypes ); - - /* returnType is 'null' instead of type(void) as per docs - * see: https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.propertybuilder?view=net-9.0 - */ var setter = addTo.DefineMethod( "set_" + name, propertyMethodAttributes, null, [shouldBeAdded] ); var il = getter.GetILGenerator(); diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs index 96b80e9f..cfc73a43 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs @@ -8,6 +8,9 @@ namespace Asp.Versioning.OData; using Microsoft.AspNetCore.OData.Formatter; #endif using Microsoft.OData.Edm; +#if !NETFRAMEWORK +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; +#endif /// /// Defines the behavior of a model type builder. @@ -26,7 +29,17 @@ public interface IModelTypeBuilder /// structured type. /// If a substitution is not required, the original CLR type is returned. When a substitution /// type is generated, it is performed only once per API version. - Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ); +#if !NETFRAMEWORK + [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif + Type NewStructuredType( + IEdmModel model, + IEdmStructuredType structuredType, +#if !NETFRAMEWORK + [DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif + Type clrType, + ApiVersion apiVersion ); /// /// Creates an returns a strongly-typed definition for OData action parameters. @@ -39,5 +52,8 @@ public interface IModelTypeBuilder /// OData action parameters are modeled as a dictionary, /// which is difficult to use effectively by documentation tools such as the API Explorer. The corresponding type is generated only once per /// API version. +#if !NETFRAMEWORK + [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ); } \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs index a32cd16a..72c76457 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs @@ -16,6 +16,8 @@ namespace Asp.Versioning.OData; using System.Runtime.CompilerServices; #if NETFRAMEWORK using IActionResult = System.Web.Http.IHttpActionResult; +#else +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; #endif /// @@ -41,7 +43,17 @@ public static partial class TypeExtensions /// The current type substitution context. /// The original or a substitution type based on the /// provided . - public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContext context ) +#if !NETFRAMEWORK + [UnconditionalSuppressMessage( "ILLink", "IL2026" )] + [UnconditionalSuppressMessage( "ILLink", "IL2073" )] + [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif + public static Type SubstituteIfNecessary( +#if !NETFRAMEWORK + [DynamicallyAccessedMembers( Interfaces | PublicProperties )] +#endif + this Type type, + TypeSubstitutionContext context ) { ArgumentNullException.ThrowIfNull( type ); ArgumentNullException.ThrowIfNull( context ); @@ -151,7 +163,13 @@ internal static Type ExtractInnerType( this Type type ) return type; } - private static bool IsSubstitutableGeneric( Type type, Stack openTypes, out Type? innerType ) + private static bool IsSubstitutableGeneric( +#if !NETFRAMEWORK + [DynamicallyAccessedMembers( Interfaces )] +#endif + Type type, + Stack openTypes, + out Type? innerType ) { innerType = default; @@ -212,6 +230,10 @@ private static bool IsSubstitutableGeneric( Type type, Stack openTypes, ou return true; } +#if !NETFRAMEWORK + [RequiresDynamicCode( "Might not be available at runtime" )] + [RequiresUnreferencedCode( "Cannot be validated by trim analysis" )] +#endif private static Type CloseGeneric( Stack openTypes, Type innerType ) { var type = openTypes.Pop(); @@ -241,7 +263,10 @@ private static bool CanBeSubstituted( Type type ) => #endif !type.IsODataActionParameters(); - internal static bool IsEnumerable( this Type type, [NotNullWhen( true )] out Type? itemType ) +#if !NETFRAMEWORK + [UnconditionalSuppressMessage( "ILLink", "IL2070" )] +#endif + internal static bool IsEnumerable(this Type type, [NotNullWhen( true )] out Type? itemType ) { var types = new Queue(); diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs index 2a0fc160..d5295031 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs @@ -410,16 +410,16 @@ public void substitute_should_resolve_types_that_reference_a_model_that_match_th } [Fact] - public void substituted_type_should_have_valid_runtime_properties__issue1104() + public void ignoring_property_should_force_substitution_with_valid_runtime_properties() { // arrange var modelBuilder = new ODataConventionModelBuilder(); - var address = modelBuilder.EntitySet
( nameof( Address ) ).EntityType; - address.Ignore( x => x.City ); // force substitution - var addressType = typeof( Address ); + + address.Ignore( x => x.City ); var context = NewContext( modelBuilder.GetEdmModel() ); + var addressType = typeof( Address ); // act var substitutedType = addressType.SubstituteIfNecessary( context ); @@ -428,6 +428,7 @@ public void substituted_type_should_have_valid_runtime_properties__issue1104() substitutedType.Should().NotBe( addressType ); #if NET452 substitutedType.GetRuntimeProperties().Should().HaveCount( 5 ); + foreach ( var substitutedProperty in substitutedType.GetRuntimeProperties() ) { substitutedProperty.Should().NotBeNull(); From 9d10504aa3fb3addabf85aecde944a3596e04154 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 23 Nov 2025 12:20:10 -0800 Subject: [PATCH 40/41] Add support for IDE route expression syntax --- .../Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs index 575a0a3a..567b0c2e 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs @@ -52,7 +52,7 @@ public ODataApiVersioningOptions( VersionedODataModelBuilder modelBuilder ) => ///
/// The associated OData prefix. /// The original options. - public virtual ODataApiVersioningOptions AddRouteComponents( string prefix ) => + public virtual ODataApiVersioningOptions AddRouteComponents( [StringSyntax( "Route" )] string prefix ) => AddRouteComponents( prefix, static _ => { } ); /// @@ -70,7 +70,7 @@ public virtual ODataApiVersioningOptions AddRouteComponents( ActionThe $batch handler. /// The original options. [CLSCompliant( false )] - public ODataApiVersioningOptions AddRouteComponents( string prefix, ODataBatchHandler batchHandler ) => + public ODataApiVersioningOptions AddRouteComponents( [StringSyntax( "Route" )] string prefix, ODataBatchHandler batchHandler ) => AddRouteComponents( prefix, builder => builder.AddSingleton( batchHandler ) ); /// @@ -89,7 +89,7 @@ public ODataApiVersioningOptions AddRouteComponents( ODataBatchHandler batchHand /// The configuration action. /// The original options. public virtual ODataApiVersioningOptions AddRouteComponents( - string prefix, + [StringSyntax( "Route" )] string prefix, Action configureAction ) { configurations ??= new( StringComparer.OrdinalIgnoreCase ); From bd469de0990a8c3e20bf96d511038755928b5dc5 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 23 Nov 2025 12:21:49 -0800 Subject: [PATCH 41/41] API version parameter must be explored in URL segment, even when version-neutral. Fixes #1149 --- .../ApiVersionParameterDescriptionContext.cs | 5 ++ ...iVersionParameterDescriptionContextTest.cs | 59 +++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) 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 da70b954..8ebe5895 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs @@ -117,6 +117,11 @@ public virtual void AddParameter( string name, ApiVersionParameterLocation locat { if ( IsApiVersionNeutral && !Options.AddApiVersionParametersWhenVersionNeutral ) { + if ( location == Path ) + { + UpdateUrlSegment(); + } + return; } diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs index ec26b099..8d2e90c7 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs @@ -82,7 +82,7 @@ public void add_parameter_should_add_descriptor_for_path() Name = "api-version", RouteInfo = new() { - Constraints = new[] { new ApiVersionRouteConstraint() }, + Constraints = [new ApiVersionRouteConstraint()], }, Source = BindingSource.Path, }; @@ -119,6 +119,55 @@ public void add_parameter_should_add_descriptor_for_path() o => o.ExcludingMissingMembers() ); } + [Fact] + public void add_parameter_should_add_descriptor_for_path_when_version_neutral() + { + // arrange + var version = new ApiVersion( 1, 0 ); + var description = new ApiDescription() + { + ActionDescriptor = new ActionDescriptor() { EndpointMetadata = [ApiVersionMetadata.Neutral] }, + ParameterDescriptions = + { + new() + { + Name = "api-version", + RouteInfo = new() { Constraints = [new ApiVersionRouteConstraint()] }, + Source = BindingSource.Path, + }, + }, + }; + var modelMetadata = new Mock( ModelMetadataIdentity.ForType( typeof( string ) ) ).Object; + var options = new ApiExplorerOptions() + { + DefaultApiVersion = version, + ApiVersionParameterSource = new UrlSegmentApiVersionReader(), + }; + var context = new ApiVersionParameterDescriptionContext( description, version, modelMetadata, options ); + + // act + context.AddParameter( "api-version", Path ); + + // assert + description.ParameterDescriptions.Single().Should().BeEquivalentTo( + new + { + Name = "api-version", + ModelMetadata = modelMetadata, + Source = BindingSource.Path, + DefaultValue = (object) "1.0", + IsRequired = true, + RouteInfo = new ApiParameterRouteInfo() + { + DefaultValue = "1.0", + IsOptional = false, + Constraints = description.ParameterDescriptions[0].RouteInfo.Constraints, + }, + Type = typeof( string ), + }, + o => o.ExcludingMissingMembers() ); + } + [Fact] public void add_parameter_should_remove_other_descriptors_after_path_parameter_is_added() { @@ -128,7 +177,7 @@ public void add_parameter_should_remove_other_descriptors_after_path_parameter_i Name = "api-version", RouteInfo = new() { - Constraints = new[] { new ApiVersionRouteConstraint() }, + Constraints = [new ApiVersionRouteConstraint()], }, Source = BindingSource.Path, }; @@ -179,7 +228,7 @@ public void add_parameter_should_not_add_query_parameter_after_path_parameter_ha Name = "api-version", RouteInfo = new() { - Constraints = new[] { new ApiVersionRouteConstraint() }, + Constraints = [new ApiVersionRouteConstraint()], }, Source = BindingSource.Path, }; @@ -214,7 +263,7 @@ public void add_parameter_should_add_descriptor_for_media_type_parameter() var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( version ) ); var description = new ApiDescription() { - ActionDescriptor = new() { EndpointMetadata = new[] { metadata } }, + ActionDescriptor = new() { EndpointMetadata = [metadata] }, SupportedRequestFormats = { new() { MediaType = Json } }, SupportedResponseTypes = { new() { ApiResponseFormats = { new() { MediaType = Json } } } }, }; @@ -304,7 +353,7 @@ public void add_parameter_should_make_parameters_optional_after_first_parameter( private static ApiDescription NewApiDescription( ApiVersion apiVersion, params ApiParameterDescription[] parameters ) { var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( apiVersion ) ); - var action = new ActionDescriptor() { EndpointMetadata = new[] { metadata } }; + var action = new ActionDescriptor() { EndpointMetadata = [metadata] }; var description = new ApiDescription() { ActionDescriptor = action }; for ( var i = 0; i < parameters.Length; i++ )